1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-09-20 17:53:11 +02:00

Merge remote-tracking branch 'jf/main' into issue-492

This commit is contained in:
slock 2022-06-17 17:30:51 +10:00
commit 4cf9dc50cf
45 changed files with 1622 additions and 364 deletions

2
.gitignore vendored
View File

@ -31,3 +31,5 @@ test_output/
# Nix build output # Nix build output
result result
.direnv/ .direnv/
integration_test/etc/config.dump.yaml

View File

@ -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,11 @@
- 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)
- Fix nodes being shown as 'offline' in `tailscale status` [648](https://github.com/juanfont/headscale/pull/648)
## 0.15.0 (2022-03-20) ## 0.15.0 (2022-03-20)

View File

@ -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))

View File

@ -195,6 +195,15 @@ make build
<sub style="font-size:14px"><b>Nico</b></sub> <sub style="font-size:14px"><b>Nico</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>
</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/e-zk> <a href=https://github.com/e-zk>
<img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/> <img src=https://avatars.githubusercontent.com/u/58356365?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=e-zk/>
@ -202,8 +211,6 @@ make build
<sub style="font-size:14px"><b>e-zk</b></sub> <sub style="font-size:14px"><b>e-zk</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/arch4ngel> <a href=https://github.com/arch4ngel>
<img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/> <img src=https://avatars.githubusercontent.com/u/11574161?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Justin Angel/>
@ -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,15 @@ 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">
<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/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 +306,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 +327,15 @@ 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>
</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,8 @@ 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"> <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kundel> <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/> <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/>
@ -422,8 +431,6 @@ make build
<sub style="font-size:14px"><b>kundel</b></sub> <sub style="font-size:14px"><b>kundel</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/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/>
@ -452,13 +459,6 @@ make build
<sub style="font-size:14px"><b>Jamie Greeff</b></sub> <sub style="font-size:14px"><b>Jamie Greeff</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/jimt> <a href=https://github.com/jimt>
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/> <img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>

107
acls.go
View File

@ -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...)
} }
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
if err != nil {
log.Error().
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
return nil, err
}
destPorts := []tailcfg.NetPortRange{} destPorts := []tailcfg.NetPortRange{}
for innerIndex, ports := range acl.Ports { for innerIndex, dest := range acl.Destinations {
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports) dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
if err != nil { if err != nil {
log.Error(). log.Error().
Msgf("Error parsing ACL %d, Port %d", index, innerIndex) 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, "-")

View File

@ -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()
@ -287,9 +287,9 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
TagOwners: TagOwners{"tag:webapp": []string{"user1"}}, TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
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)
@ -628,7 +642,8 @@ 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)

View File

@ -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,omitempty" yaml:"Groups,omitempty"` Groups Groups `json:"groups" yaml:"groups"`
Hosts Hosts `json:"Hosts,omitempty" yaml:"Hosts,omitempty"` Hosts Hosts `json:"hosts" yaml:"hosts"`
TagOwners TagOwners `json:"TagOwners,omitempty" yaml:"TagOwners,omitempty"` TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
ACLs []ACL `json:"ACLs,omitempty" yaml:"ACLs,omitempty"` ACLs []ACL `json:"acls" yaml:"acls"`
Tests []ACLTest `json:"Tests,omitempty" yaml:"Tests,omitempty"` 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.

3
api.go
View File

@ -279,7 +279,8 @@ func (h *Headscale) getMapResponse(
DERPMap: h.DERPMap, DERPMap: h.DERPMap,
UserProfiles: profiles, UserProfiles: profiles,
Debug: &tailcfg.Debug{ Debug: &tailcfg.Debug{
DisableLogTail: !h.cfg.LogTail.Enabled, DisableLogTail: !h.cfg.LogTail.Enabled,
RandomizeClientPort: h.cfg.RandomizeClientPort,
}, },
} }

View File

@ -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,

28
app.go
View File

@ -168,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
@ -657,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:
@ -756,13 +758,25 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
} }
} }
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 h.lastStateChange == nil { if len(namespaces) == 0 {
h.lastStateChange = xsync.NewMapOf[time.Time]() 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 {
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 {

View 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")
}
},
}

View File

@ -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",

View File

@ -7,12 +7,10 @@ import (
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"time"
"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"
@ -29,21 +27,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err) return nil, fmt.Errorf("failed to load configuration while creating headscale instance: %w", err)
} }
// 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 {
// 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
}
app, err := headscale.NewHeadscale(cfg) app, err := headscale.NewHeadscale(cfg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -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"
"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/tcnksm/go-latest"
) )
func main() { func main() {
@ -43,39 +39,5 @@ func main() {
NoColor: !colors, NoColor: !colors,
}) })
cfg, err := headscale.GetHeadscaleConfig()
if err != nil {
log.Fatal().Caller().Err(err)
}
machineOutput := cli.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") &&
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()
} }

View File

@ -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 = headscale.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
@ -68,6 +113,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
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,7 +138,7 @@ 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 = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
dnsConfig, baseDomain := headscale.GetDNSConfig() dnsConfig, baseDomain := headscale.GetDNSConfig()
@ -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 = headscale.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 = headscale.LoadConfig(tmpDir) err = headscale.LoadConfig(tmpDir, false)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }

View File

@ -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

View File

@ -54,7 +54,8 @@ type Config struct {
OIDC OIDCConfig OIDC OIDCConfig
LogTail LogTailConfig LogTail LogTailConfig
RandomizeClientPort bool
CLI CLIConfig CLI CLIConfig
@ -114,15 +115,19 @@ type ACLConfig struct {
PolicyPath string PolicyPath string
} }
func LoadConfig(path string) error { func LoadConfig(path string, isFile bool) error {
viper.SetConfigName("config") if isFile {
if path == "" { viper.SetConfigFile(path)
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else { } else {
// For testing viper.SetConfigName("config")
viper.AddConfigPath(path) if path == "" {
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
viper.AddConfigPath(".")
} else {
// For testing
viper.AddConfigPath(path)
}
} }
viper.SetEnvPrefix("headscale") viper.SetEnvPrefix("headscale")
@ -153,8 +158,13 @@ func LoadConfig(path string) error {
viper.SetDefault("oidc.strip_email_domain", true) viper.SetDefault("oidc.strip_email_domain", true)
viper.SetDefault("logtail.enabled", false) viper.SetDefault("logtail.enabled", false)
viper.SetDefault("randomize_client_port", false)
viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
if err := viper.ReadInConfig(); err != nil { 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) return fmt.Errorf("fatal error reading config file: %w", err)
} }
@ -196,6 +206,17 @@ func LoadConfig(path string) error {
EnforcedClientAuth) 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 != "" { if errorText != "" {
//nolint //nolint
return errors.New(strings.TrimSuffix(errorText, "\n")) return errors.New(strings.TrimSuffix(errorText, "\n"))
@ -297,7 +318,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
nameserversStr := viper.GetStringSlice("dns_config.nameservers") nameserversStr := viper.GetStringSlice("dns_config.nameservers")
nameservers := make([]netaddr.IP, len(nameserversStr)) nameservers := make([]netaddr.IP, len(nameserversStr))
resolvers := make([]dnstype.Resolver, len(nameserversStr)) resolvers := make([]*dnstype.Resolver, len(nameserversStr))
for index, nameserverStr := range nameserversStr { for index, nameserverStr := range nameserversStr {
nameserver, err := netaddr.ParseIP(nameserverStr) nameserver, err := netaddr.ParseIP(nameserverStr)
@ -309,7 +330,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
nameservers[index] = nameserver nameservers[index] = nameserver
resolvers[index] = dnstype.Resolver{ resolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(), Addr: nameserver.String(),
} }
} }
@ -320,13 +341,13 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.restricted_nameservers") { if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 { if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver) dnsConfig.Routes = make(map[string][]*dnstype.Resolver)
restrictedDNS := viper.GetStringMapStringSlice( restrictedDNS := viper.GetStringMapStringSlice(
"dns_config.restricted_nameservers", "dns_config.restricted_nameservers",
) )
for domain, restrictedNameservers := range restrictedDNS { for domain, restrictedNameservers := range restrictedDNS {
restrictedResolvers := make( restrictedResolvers := make(
[]dnstype.Resolver, []*dnstype.Resolver,
len(restrictedNameservers), len(restrictedNameservers),
) )
for index, nameserverStr := range restrictedNameservers { for index, nameserverStr := range restrictedNameservers {
@ -337,7 +358,7 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
Err(err). Err(err).
Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) Msgf("Could not parse restricted nameserver IP: %s", nameserverStr)
} }
restrictedResolvers[index] = dnstype.Resolver{ restrictedResolvers[index] = &dnstype.Resolver{
Addr: nameserver.String(), Addr: nameserver.String(),
} }
} }
@ -377,14 +398,10 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
} }
func GetHeadscaleConfig() (*Config, error) { func GetHeadscaleConfig() (*Config, error) {
err := LoadConfig("")
if err != nil {
return nil, err
}
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig() derpConfig := GetDERPConfig()
logConfig := GetLogTailConfig() logConfig := GetLogTailConfig()
randomizeClientPort := viper.GetBool("randomize_client_port")
configuredPrefixes := viper.GetStringSlice("ip_prefixes") configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1) parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1)
@ -490,7 +507,8 @@ func GetHeadscaleConfig() (*Config, error) {
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"), StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
}, },
LogTail: logConfig, LogTail: logConfig,
RandomizeClientPort: randomizeClientPort,
CLI: CLIConfig{ CLI: CLIConfig{
Address: viper.GetString("cli.address"), Address: viper.GetString("cli.address"),

11
derp.go
View File

@ -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)
}
} }
} }
} }

View File

@ -161,7 +161,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -178,7 +178,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -195,7 +195,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -212,7 +212,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -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,
} }
@ -304,7 +304,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66", DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Hostname: "test_get_shared_nodes_1", Hostname: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -321,7 +321,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_2", Hostname: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -338,7 +338,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_3", Hostname: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -355,7 +355,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863", DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Hostname: "test_get_shared_nodes_4", Hostname: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
@ -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,
} }

View File

@ -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

View File

@ -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:*"] }
] ]
} }
``` ```

View 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.

View File

@ -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": {

View File

@ -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
View File

@ -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

623
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -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))
}

View File

@ -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",

View 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

View 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

View 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

View File

@ -637,6 +637,10 @@ func (machine Machine) toNode(
hostInfo := machine.GetHostInfo() hostInfo := machine.GetHostInfo()
// A node is Online if it is connected to the control server,
// and we now we update LastSeen every keepAliveInterval duration at least.
online := machine.LastSeen.After(time.Now().Add(-keepAliveInterval))
node := tailcfg.Node{ node := tailcfg.Node{
ID: tailcfg.NodeID(machine.ID), // this is the actual ID ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID( StableID: tailcfg.StableNodeID(
@ -653,6 +657,7 @@ func (machine Machine) toNode(
Endpoints: machine.Endpoints, Endpoints: machine.Endpoints,
DERP: derp, DERP: derp,
Online: &online,
Hostinfo: hostInfo.View(), Hostinfo: hostInfo.View(),
Created: machine.CreatedAt, Created: machine.CreatedAt,
LastSeen: machine.LastSeen, LastSeen: machine.LastSeen,

View File

@ -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{},
} }

View File

@ -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)

View File

@ -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"
], ],
}, },

View File

@ -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:*",
], ],

View File

@ -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:*",
], ],
}, },

View File

@ -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:*",
], ],
}, },

View 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:*",
],
},
],
}

View File

@ -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",
], ],
}, },

View File

@ -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:*",
], ],
}, },

View File

@ -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:*

View File

@ -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"
], ],
}, },

View File

@ -325,11 +325,17 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
// number generator fails to function correctly, in which // number generator fails to function correctly, in which
// case the caller should not continue. // case the caller should not continue.
func GenerateRandomStringDNSSafe(n int) (string, error) { func GenerateRandomStringDNSSafe(n int) (string, error) {
str, err := GenerateRandomStringURLSafe(n) var str string
var err error
for len(str) < n {
str, err = GenerateRandomStringURLSafe(n)
if err != nil {
return "", err
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", ""))
}
str = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", "")) return str[:n], nil
return str[:n], err
} }
func IsStringInSlice(slice []string, str string) bool { func IsStringInSlice(slice []string, str string) bool {

View File

@ -34,7 +34,7 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@ -82,7 +82,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@ -172,7 +172,7 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
MachineKey: "foo", MachineKey: "foo",
NodeKey: "bar", NodeKey: "bar",
DiscoKey: "faa", DiscoKey: "faa",
Hostname: "testmachine", Hostname: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@ -185,3 +185,15 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
c.Assert(len(ips2), check.Equals, 1) c.Assert(len(ips2), check.Equals, 1)
c.Assert(ips2[0].String(), check.Equals, expected.String()) c.Assert(ips2[0].String(), check.Equals, expected.String())
} }
func (s *Suite) TestGenerateRandomStringDNSSafe(c *check.C) {
for i := 0; i < 100000; i++ {
str, err := GenerateRandomStringDNSSafe(8)
if err != nil {
c.Error(err)
}
if len(str) != 8 {
c.Error("invalid length", len(str), str)
}
}
}