Compare commits

..

41 Commits

Author SHA1 Message Date
Juan Font
f491db232b Merge pull request #55 from cure/letsencrypt-more-flexible-config
Turn the combination of TLS-ALPN-01 and listen_addr on a port other than
2021-07-17 11:01:08 +02:00
Ward Vandewege
9a24340bd4 Turn the combination of TLS-ALPN-01 and listen_addr on a port other than
443 into a warning, not an error, refs #53.
2021-07-16 22:02:05 -04:00
Juan Font
6f20a1fc68 Merge pull request #51 from tianon/typo
Fix minor typo
2021-07-16 18:04:46 +02:00
Tianon Gravi
243b961cbe Fix minor typo
> Error: unknown command "namespace" for "headscale"
2021-07-16 15:07:13 +00:00
Juan Font Alonso
5748744134 Use ubuntu 18.04 as build env 2021-07-12 17:04:28 +02:00
Juan Font
31556e1ac0 Merge pull request #48 from juanfont/better-profile-info
Improving namespace/user support
2021-07-11 16:44:16 +02:00
Juan Font Alonso
0159649d0a Send the namespace name as user to the clients 2021-07-11 16:39:19 +02:00
Juan Font Alonso
cf9d920e4a Minor typo 2021-07-11 15:10:37 +02:00
Juan Font Alonso
7d46dfe012 Only load ACLs if a path is present 2021-07-11 15:10:11 +02:00
Juan Font Alonso
eabb1ce881 Fix minor typo on the register webpage 2021-07-11 15:05:32 +02:00
Juan Font Alonso
db20985b06 Show N/A in reusable when key is ephemeral 2021-07-11 13:14:25 +02:00
Juan Font Alonso
29b80e3ca1 Fix debug mode enabled by default in db 2021-07-11 13:13:36 +02:00
Juan Font Alonso
a16a763283 Update README.md with info on ACLs 2021-07-11 13:04:33 +02:00
Juan Font
ad7f03c9dd Merge pull request #47 from juanfont/handle-ephemeral-reconnect
Added HTTP responses on map errors
2021-07-11 11:41:23 +02:00
Juan Font Alonso
bff3d2d613 Added HTTP responses on errors 2021-07-11 11:37:17 +02:00
Juan Font
f66c283756 Merge pull request #46 from Teteros/update-derp-servers
Update DERP server definitions
2021-07-10 23:29:54 +02:00
Teteros
ad454d95b9 Update DERP server definitions 2021-07-10 09:00:35 +01:00
Juan Font
e67a98b758 Merge pull request #44 from juanfont/acls
Add support for Policy ACLs
2021-07-07 16:19:45 +02:00
Juan Font Alonso
ecf258f995 Use gorm connection pool 2021-07-04 21:56:13 +02:00
Juan Font Alonso
d4b27fd54b Merge branch 'main' into acls 2021-07-04 21:54:55 +02:00
Juan Font
90e9ad9a0e Merge pull request #45 from juanfont/reuse-gorm-connection
Use gorm connection pool
2021-07-04 21:51:43 +02:00
Juan Font Alonso
ff9d99b9ea Use gorm connection pool 2021-07-04 21:40:46 +02:00
Juan Font
7590dee1f2 Removed unnecessary prints 2021-07-04 13:47:59 +02:00
Juan Font
315bc6b677 Added acl path key in example config 2021-07-04 13:41:38 +02:00
Juan Font
a1b8f77b1b Fixed tests 2021-07-04 13:40:45 +02:00
Juan Font
19443669bf Fixed linting issues 2021-07-04 13:33:00 +02:00
Juan Font
d446e8a2fb More stuff in go.sum 2021-07-04 13:24:27 +02:00
Juan Font
202d6b506f Load ACL policy on headscale startup 2021-07-04 13:24:05 +02:00
Juan Font
401e6aec32 And more tests 2021-07-04 13:23:31 +02:00
Juan Font
bd86975d10 Added missing go.mod 2021-07-04 13:10:15 +02:00
Juan Font
d0e970f21d Added more unit tests 2021-07-04 13:01:41 +02:00
Juan Font
07e95393b3 Rule generation kinda working, missing tests 2021-07-04 12:35:18 +02:00
Juan Font
136aab9dc8 Work in progress in rule generation 2021-07-03 17:31:32 +02:00
Juan Font
bbd6a67c46 Added more acl test hujsons 2021-07-03 17:31:08 +02:00
Juan Font
31ea67bcaf Minor addenda to README.md 2021-07-03 16:10:22 +02:00
Juan Font
5644dadaf9 Added dependency on hujson 2021-07-03 12:02:46 +02:00
Juan Font
874aa4277d Minor changes in the README.md 2021-07-03 12:01:19 +02:00
Juan Font
b161a92e58 Initial work on ACLs 2021-07-03 11:55:32 +02:00
Juan Font
95fee5aa6f Merge pull request #43 from juanfont/use-plurals-for-cmds
Change all commands to plural words
2021-06-29 23:38:03 +02:00
Juan Font Alonso
f5b8a3f710 Make all commands a plural word 2021-06-28 20:04:05 +02:00
Juan Font
ba87ade9c5 Merge pull request #42 from juanfont/tailscale-1.8.x
Update Headscale to Tailscale 1.10
2021-06-26 18:36:46 +02:00
40 changed files with 1098 additions and 210 deletions

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-18.04 # due to CGO we need to user an older version
steps: steps:
- -
name: Checkout name: Checkout

View File

@@ -62,8 +62,7 @@ archives:
- linux-armhf - linux-armhf
- linux-amd64 - linux-amd64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: zip format: binary
# wrap_in_directory: true
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'

View File

@@ -2,7 +2,7 @@
[![Join the chat at https://gitter.im/headscale-dev/community](https://badges.gitter.im/headscale-dev/community.svg)](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg) [![Join the chat at https://gitter.im/headscale-dev/community](https://badges.gitter.im/headscale-dev/community.svg)](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
An open source implementation of the Tailscale coordination server. An open source, self-hosted implementation of the Tailscale coordination server.
## Overview ## Overview
@@ -10,28 +10,27 @@ Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'. Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
The control server works as an exchange point of cryptographic public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes. The control server works as an exchange point of Wireguard public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.
Headscale implements this coordination server. Headscale implements this coordination server.
## Status ## Status
- [x] Basic functionality (nodes can communicate with each other) - [x] Base functionality (nodes can communicate with each other)
- [x] Node registration through the web flow - [x] Node registration through the web flow
- [x] Network changes are relied to the nodes - [x] Network changes are relied to the nodes
- [x] ~~Multiuser~~ Namespace support - [x] Namespace support (~equivalent to multi-user in Tailscale.com)
- [x] Basic routing (advertise & accept) - [x] Routing (advertise & accept, including exit nodes)
- [ ] Share nodes between ~~users~~ namespaces - [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
- [x] Node registration via pre-auth keys (including reusable keys and ephemeral node support)
- [X] JSON-formatted output - [X] JSON-formatted output
- [ ] ACLs - [X] ACLs
- [ ] Share nodes between ~~users~~ namespaces
- [ ] DNS - [ ] DNS
... and probably lots of stuff missing
## Roadmap 🤷 ## Roadmap 🤷
Basic multiuser support (multinamespace, actually) is now implemented. No node sharing or ACLs between namespaces yet though... We are now focusing on adding integration tests with the official clients.
Suggestions/PRs welcomed! Suggestions/PRs welcomed!
@@ -63,7 +62,7 @@ Suggestions/PRs welcomed!
4. Create a namespace (a namespace is a 'tailnet', a group of Tailscale nodes that can talk to each other) 4. Create a namespace (a namespace is a 'tailnet', a group of Tailscale nodes that can talk to each other)
```shell ```shell
headscale namespace create myfirstnamespace headscale namespaces create myfirstnamespace
``` ```
5. Run the server 5. Run the server
@@ -146,6 +145,7 @@ Headscale's configuration file is named `config.json` or `config.yaml`. Headscal
The fields starting with `db_` are used for the PostgreSQL connection information. The fields starting with `db_` are used for the PostgreSQL connection information.
### Running the service via TLS (optional) ### Running the service via TLS (optional)
``` ```
@@ -163,6 +163,17 @@ Headscale can be configured to expose its web service via TLS. To configure the
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) Headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed. The default challenge type HTTP-01 requires that Headscale listens on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, Headscale must be reachable via port 443, but port 80 is not required. To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) Headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from. The certificate will automatically be renewed as needed. The default challenge type HTTP-01 requires that Headscale listens on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in `listen_addr`. Alternatively, `tls_letsencrypt_challenge_type` can be set to `TLS-ALPN-01`. In this configuration, Headscale must be reachable via port 443, but port 80 is not required.
### Policy ACLs
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
For instance, instead of referring to users when defining groups you must
use namespaces (which are the equivalent to user/logins in Tailscale.com).
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
## Disclaimer ## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc. 1. We have nothing to do with Tailscale, or Tailscale Inc.

263
acls.go Normal file
View File

@@ -0,0 +1,263 @@
package headscale
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"github.com/tailscale/hujson"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const errorEmptyPolicy = Error("empty policy")
const errorInvalidAction = Error("invalid action")
const errorInvalidUserSection = Error("invalid user section")
const errorInvalidGroup = Error("invalid group")
const errorInvalidTag = Error("invalid tag")
const errorInvalidNamespace = Error("invalid namespace")
const errorInvalidPortFormat = Error("invalid port format")
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules
func (h *Headscale) LoadACLPolicy(path string) error {
policyFile, err := os.Open(path)
if err != nil {
return err
}
defer policyFile.Close()
var policy ACLPolicy
b, err := io.ReadAll(policyFile)
if err != nil {
return err
}
err = hujson.Unmarshal(b, &policy)
if err != nil {
return err
}
if policy.IsZero() {
return errorEmptyPolicy
}
h.aclPolicy = &policy
rules, err := h.generateACLRules()
if err != nil {
return err
}
h.aclRules = rules
return nil
}
func (h *Headscale) generateACLRules() (*[]tailcfg.FilterRule, error) {
rules := []tailcfg.FilterRule{}
for i, a := range h.aclPolicy.ACLs {
if a.Action != "accept" {
return nil, errorInvalidAction
}
r := tailcfg.FilterRule{}
srcIPs := []string{}
for j, u := range a.Users {
srcs, err := h.generateACLPolicySrcIP(u)
if err != nil {
log.Printf("Error parsing ACL %d, User %d", i, j)
return nil, err
}
srcIPs = append(srcIPs, *srcs...)
}
r.SrcIPs = srcIPs
destPorts := []tailcfg.NetPortRange{}
for j, d := range a.Ports {
dests, err := h.generateACLPolicyDestPorts(d)
if err != nil {
log.Printf("Error parsing ACL %d, Port %d", i, j)
return nil, err
}
destPorts = append(destPorts, *dests...)
}
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPs,
DstPorts: destPorts,
})
}
return &rules, nil
}
func (h *Headscale) generateACLPolicySrcIP(u string) (*[]string, error) {
return h.expandAlias(u)
}
func (h *Headscale) generateACLPolicyDestPorts(d string) (*[]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
if len(tokens) < 2 || len(tokens) > 3 {
return nil, errorInvalidPortFormat
}
var alias string
// We can have here stuff like:
// git-server:*
// 192.168.1.0/24:22
// tag:montreal-webserver:80,443
// tag:api-server:443
// example-host-1:*
if len(tokens) == 2 {
alias = tokens[0]
} else {
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
}
expanded, err := h.expandAlias(alias)
if err != nil {
return nil, err
}
ports, err := h.expandPorts(tokens[len(tokens)-1])
if err != nil {
return nil, err
}
dests := []tailcfg.NetPortRange{}
for _, d := range *expanded {
for _, p := range *ports {
pr := tailcfg.NetPortRange{
IP: d,
Ports: p,
}
dests = append(dests, pr)
}
}
return &dests, nil
}
func (h *Headscale) expandAlias(s string) (*[]string, error) {
if s == "*" {
return &[]string{"*"}, nil
}
if strings.HasPrefix(s, "group:") {
if _, ok := h.aclPolicy.Groups[s]; !ok {
return nil, errorInvalidGroup
}
ips := []string{}
for _, n := range h.aclPolicy.Groups[s] {
nodes, err := h.ListMachinesInNamespace(n)
if err != nil {
return nil, errorInvalidNamespace
}
for _, node := range *nodes {
ips = append(ips, node.IPAddress)
}
}
return &ips, nil
}
if strings.HasPrefix(s, "tag:") {
if _, ok := h.aclPolicy.TagOwners[s]; !ok {
return nil, errorInvalidTag
}
// This will have HORRIBLE performance.
// We need to change the data model to better store tags
machines := []Machine{}
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
return nil, err
}
ips := []string{}
for _, m := range machines {
hostinfo := tailcfg.Hostinfo{}
if len(m.HostInfo) != 0 {
hi, err := m.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(hi, &hostinfo)
if err != nil {
return nil, err
}
// FIXME: Check TagOwners allows this
for _, t := range hostinfo.RequestTags {
if s[4:] == t {
ips = append(ips, m.IPAddress)
break
}
}
}
}
return &ips, nil
}
n, err := h.GetNamespace(s)
if err == nil {
nodes, err := h.ListMachinesInNamespace(n.Name)
if err != nil {
return nil, err
}
ips := []string{}
for _, n := range *nodes {
ips = append(ips, n.IPAddress)
}
return &ips, nil
}
if h, ok := h.aclPolicy.Hosts[s]; ok {
return &[]string{h.String()}, nil
}
ip, err := netaddr.ParseIP(s)
if err == nil {
return &[]string{ip.String()}, nil
}
cidr, err := netaddr.ParseIPPrefix(s)
if err == nil {
return &[]string{cidr.String()}, nil
}
return nil, errorInvalidUserSection
}
func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) {
if s == "*" {
return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil
}
ports := []tailcfg.PortRange{}
for _, p := range strings.Split(s, ",") {
rang := strings.Split(p, "-")
if len(rang) == 1 {
pi, err := strconv.ParseUint(rang[0], 10, 16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(pi),
Last: uint16(pi),
})
} else if len(rang) == 2 {
start, err := strconv.ParseUint(rang[0], 10, 16)
if err != nil {
return nil, err
}
last, err := strconv.ParseUint(rang[1], 10, 16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
First: uint16(start),
Last: uint16(last),
})
} else {
return nil, errorInvalidPortFormat
}
}
return &ports, nil
}

160
acls_test.go Normal file
View File

@@ -0,0 +1,160 @@
package headscale
import (
"gopkg.in/check.v1"
)
func (s *Suite) TestWrongPath(c *check.C) {
err := h.LoadACLPolicy("asdfg")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestBrokenHuJson(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/broken.hujson")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/invalid.hujson")
c.Assert(err, check.NotNil)
c.Assert(err, check.Equals, errorEmptyPolicy)
}
func (s *Suite) TestParseHosts(c *check.C) {
var hs Hosts
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`))
c.Assert(hs, check.NotNil)
c.Assert(err, check.IsNil)
}
func (s *Suite) TestParseInvalidCIDR(c *check.C) {
var hs Hosts
err := hs.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`))
c.Assert(hs, check.IsNil)
c.Assert(err, check.NotNil)
}
func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestBasicRule(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson")
c.Assert(err, check.IsNil)
rules, err := h.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
}
func (s *Suite) TestPortRange(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil)
rules, err := h.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
}
func (s *Suite) TestPortWildcard(c *check.C) {
err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil)
rules, err := h.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Equals, "*")
}
func (s *Suite) TestPortNamespace(c *check.C) {
n, err := h.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil)
ip, _ := h.getAvailableIP()
m := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: n.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: ip.String(),
AuthKeyID: uint(pak.ID),
}
h.db.Save(&m)
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson")
c.Assert(err, check.IsNil)
rules, err := h.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String())
}
func (s *Suite) TestPortGroup(c *check.C) {
n, err := h.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil)
ip, _ := h.getAvailableIP()
m := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: n.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: ip.String(),
AuthKeyID: uint(pak.ID),
}
h.db.Save(&m)
err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
c.Assert(err, check.IsNil)
rules, err := h.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(*rules, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts, check.HasLen, 1)
c.Assert((*rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert((*rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert((*rules)[0].SrcIPs, check.HasLen, 1)
c.Assert((*rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip")
c.Assert((*rules)[0].SrcIPs[0], check.Equals, ip.String())
}

70
acls_types.go Normal file
View File

@@ -0,0 +1,70 @@
package headscale
import (
"strings"
"github.com/tailscale/hujson"
"inet.af/netaddr"
)
// ACLPolicy represents a Tailscale ACL Policy
type ACLPolicy struct {
Groups Groups `json:"Groups"`
Hosts Hosts `json:"Hosts"`
TagOwners TagOwners `json:"TagOwners"`
ACLs []ACL `json:"ACLs"`
Tests []ACLTest `json:"Tests"`
}
// ACL is a basic rule for the ACL Policy
type ACL struct {
Action string `json:"Action"`
Users []string `json:"Users"`
Ports []string `json:"Ports"`
}
// Groups references a series of alias in the ACL rules
type Groups map[string][]string
// Hosts are alias for IP addresses or subnets
type Hosts map[string]netaddr.IPPrefix
// TagOwners specify what users (namespaces?) are allow to use certain tags
type TagOwners map[string][]string
// ACLTest is not implemented, but should be use to check if a certain rule is allowed
type ACLTest struct {
User string `json:"User"`
Allow []string `json:"Allow"`
Deny []string `json:"Deny,omitempty"`
}
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects
func (h *Hosts) UnmarshalJSON(data []byte) error {
hosts := Hosts{}
hs := make(map[string]string)
err := hujson.Unmarshal(data, &hs)
if err != nil {
return err
}
for k, v := range hs {
if !strings.Contains(v, "/") {
v = v + "/32"
}
prefix, err := netaddr.ParseIPPrefix(v)
if err != nil {
return err
}
hosts[k] = prefix
}
*h = hosts
return nil
}
// IsZero is perhaps a bit naive here
func (p ACLPolicy) IsZero() bool {
if len(p.Groups) == 0 && len(p.Hosts) == 0 && len(p.ACLs) == 0 {
return true
}
return false
}

48
api.go
View File

@@ -46,7 +46,7 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
<p> <p>
<code> <code>
<b>headscale -n NAMESPACE node register %s</b> <b>headscale -n NAMESPACE nodes register %s</b>
</code> </code>
</p> </p>
@@ -75,15 +75,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
return return
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
c.String(http.StatusInternalServerError, ":(")
return
}
var m Machine var m Machine
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Println("New Machine!") log.Println("New Machine!")
m = Machine{ m = Machine{
Expiry: &req.Expiry, Expiry: &req.Expiry,
@@ -91,14 +84,14 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
Name: req.Hostinfo.Hostname, Name: req.Hostinfo.Hostname,
NodeKey: wgkey.Key(req.NodeKey).HexString(), NodeKey: wgkey.Key(req.NodeKey).HexString(),
} }
if err := db.Create(&m).Error; err != nil { if err := h.db.Create(&m).Error; err != nil {
log.Printf("Could not create row: %s", err) log.Printf("Could not create row: %s", err)
return return
} }
} }
if !m.Registered && req.Auth.AuthKey != "" { if !m.Registered && req.Auth.AuthKey != "" {
h.handleAuthKey(c, db, mKey, req, m) h.handleAuthKey(c, h.db, mKey, req, m)
return return
} }
@@ -138,7 +131,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() { if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() {
log.Printf("[%s] We have the OldNodeKey in the database. This is a key refresh", m.Name) log.Printf("[%s] We have the OldNodeKey in the database. This is a key refresh", m.Name)
m.NodeKey = wgkey.Key(req.NodeKey).HexString() m.NodeKey = wgkey.Key(req.NodeKey).HexString()
db.Save(&m) h.db.Save(&m)
resp.AuthURL = "" resp.AuthURL = ""
resp.User = *m.Namespace.toUser() resp.User = *m.Namespace.toUser()
@@ -195,23 +188,21 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
mKey, err := wgkey.ParseHex(mKeyStr) mKey, err := wgkey.ParseHex(mKeyStr)
if err != nil { if err != nil {
log.Printf("Cannot parse client key: %s", err) log.Printf("Cannot parse client key: %s", err)
c.String(http.StatusBadRequest, "")
return return
} }
req := tailcfg.MapRequest{} req := tailcfg.MapRequest{}
err = decode(body, &req, &mKey, h.privateKey) err = decode(body, &req, &mKey, h.privateKey)
if err != nil { if err != nil {
log.Printf("Cannot decode message: %s", err) log.Printf("Cannot decode message: %s", err)
c.String(http.StatusBadRequest, "")
return return
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return
}
var m Machine var m Machine
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Printf("Ignoring request, cannot find machine with key %s", mKey.HexString()) log.Printf("Ignoring request, cannot find machine with key %s", mKey.HexString())
c.String(http.StatusUnauthorized, "")
return return
} }
@@ -234,7 +225,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
m.Endpoints = datatypes.JSON(endpoints) m.Endpoints = datatypes.JSON(endpoints)
m.LastSeen = &now m.LastSeen = &now
} }
db.Save(&m) h.db.Save(&m)
pollData := make(chan []byte, 1) pollData := make(chan []byte, 1)
update := make(chan []byte, 1) update := make(chan []byte, 1)
@@ -299,11 +290,11 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
log.Printf("[%s] Sending data (%d bytes)", m.Name, len(data)) log.Printf("[%s] Sending data (%d bytes)", m.Name, len(data))
_, err := w.Write(data) _, err := w.Write(data)
if err != nil { if err != nil {
log.Printf("[%s] 🤮 Cannot write data: %s", m.Name, err) log.Printf("[%s] Cannot write data: %s", m.Name, err)
} }
now := time.Now().UTC() now := time.Now().UTC()
m.LastSeen = &now m.LastSeen = &now
db.Save(&m) h.db.Save(&m)
return true return true
case <-update: case <-update:
@@ -322,7 +313,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
log.Printf("[%s] The client has closed the connection", m.Name) log.Printf("[%s] The client has closed the connection", m.Name)
now := time.Now().UTC() now := time.Now().UTC()
m.LastSeen = &now m.LastSeen = &now
db.Save(&m) h.db.Save(&m)
h.pollMu.Lock() h.pollMu.Lock()
cancelKeepAlive <- []byte{} cancelKeepAlive <- []byte{}
delete(h.clientsPolling, m.ID) delete(h.clientsPolling, m.ID)
@@ -366,16 +357,23 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
log.Printf("Cannot fetch peers: %s", err) log.Printf("Cannot fetch peers: %s", err)
return nil, err return nil, err
} }
profile := tailcfg.UserProfile{
ID: tailcfg.UserID(m.NamespaceID),
LoginName: m.Namespace.Name,
DisplayName: m.Namespace.Name,
}
resp := tailcfg.MapResponse{ resp := tailcfg.MapResponse{
KeepAlive: false, KeepAlive: false,
Node: node, Node: node,
Peers: *peers, Peers: *peers,
DNS: []netaddr.IP{}, DNS: []netaddr.IP{},
SearchPaths: []string{}, SearchPaths: []string{},
Domain: "foobar@example.com", Domain: "headscale.net",
PacketFilter: tailcfg.FilterAllowAll, PacketFilter: *h.aclRules,
DERPMap: h.cfg.DerpMap, DERPMap: h.cfg.DerpMap,
UserProfiles: []tailcfg.UserProfile{}, UserProfiles: []tailcfg.UserProfile{profile},
} }
var respBody []byte var respBody []byte

20
app.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"gorm.io/gorm"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/wgkey" "tailscale.com/types/wgkey"
) )
@@ -43,12 +44,16 @@ type Config struct {
// Headscale represents the base app of the service // Headscale represents the base app of the service
type Headscale struct { type Headscale struct {
cfg Config cfg Config
db *gorm.DB
dbString string dbString string
dbType string dbType string
dbDebug bool dbDebug bool
publicKey *wgkey.Key publicKey *wgkey.Key
privateKey *wgkey.Private privateKey *wgkey.Private
aclPolicy *ACLPolicy
aclRules *[]tailcfg.FilterRule
pollMu sync.Mutex pollMu sync.Mutex
clientsPolling map[uint64]chan []byte // this is by all means a hackity hack clientsPolling map[uint64]chan []byte // this is by all means a hackity hack
} }
@@ -73,7 +78,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
case "sqlite3": case "sqlite3":
dbString = cfg.DBpath dbString = cfg.DBpath
default: default:
return nil, errors.New("Unsupported DB") return nil, errors.New("unsupported DB")
} }
h := Headscale{ h := Headscale{
@@ -82,11 +87,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
dbString: dbString, dbString: dbString,
privateKey: privKey, privateKey: privKey,
publicKey: &pubKey, publicKey: &pubKey,
aclRules: &tailcfg.FilterAllowAll, // default allowall
} }
err = h.initDB() err = h.initDB()
if err != nil { if err != nil {
return nil, err return nil, err
} }
h.clientsPolling = make(map[uint64]chan []byte) h.clientsPolling = make(map[uint64]chan []byte)
return &h, nil return &h, nil
} }
@@ -107,12 +115,6 @@ func (h *Headscale) ExpireEphemeralNodes(milliSeconds int64) {
} }
func (h *Headscale) expireEphemeralNodesWorker() { func (h *Headscale) expireEphemeralNodesWorker() {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return
}
namespaces, err := h.ListNamespaces() namespaces, err := h.ListNamespaces()
if err != nil { if err != nil {
log.Printf("Error listing namespaces: %s", err) log.Printf("Error listing namespaces: %s", err)
@@ -127,7 +129,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
for _, m := range *machines { for _, m := range *machines {
if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
log.Printf("[%s] Ephemeral client removed from database\n", m.Name) log.Printf("[%s] Ephemeral client removed from database\n", m.Name)
err = db.Unscoped().Delete(m).Error err = h.db.Unscoped().Delete(m).Error
if err != nil { if err != nil {
log.Printf("[%s] 🤮 Cannot delete ephemeral machine from the database: %s", m.Name, err) log.Printf("[%s] 🤮 Cannot delete ephemeral machine from the database: %s", m.Name, err)
} }
@@ -173,7 +175,7 @@ func (h *Headscale) Serve() error {
}() }()
err = s.ListenAndServeTLS("", "") err = s.ListenAndServeTLS("", "")
} else { } else {
return errors.New("Unknown value for TLSLetsEncryptChallengeType") return errors.New("unknown value for TLSLetsEncryptChallengeType")
} }
} else if h.cfg.TLSCertPath == "" { } else if h.cfg.TLSCertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") { if !strings.HasPrefix(h.cfg.ServerURL, "http://") {

View File

@@ -47,4 +47,9 @@ func (s *Suite) ResetDB(c *check.C) {
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
db, err := h.openDB()
if err != nil {
c.Fatal(err)
}
h.db = db
} }

11
cli.go
View File

@@ -2,7 +2,6 @@ package headscale
import ( import (
"errors" "errors"
"log"
"gorm.io/gorm" "gorm.io/gorm"
"tailscale.com/types/wgkey" "tailscale.com/types/wgkey"
@@ -18,13 +17,9 @@ func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
m := Machine{} m := Machine{}
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("Machine not found") return nil, errors.New("Machine not found")
} }
@@ -40,6 +35,6 @@ func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, err
m.NamespaceID = ns.ID m.NamespaceID = ns.ID
m.Registered = true m.Registered = true
m.RegisterMethod = "cli" m.RegisterMethod = "cli"
db.Save(&m) h.db.Save(&m)
return &m, nil return &m, nil
} }

View File

@@ -8,11 +8,6 @@ func (s *Suite) TestRegisterMachine(c *check.C) {
n, err := h.CreateNamespace("test") n, err := h.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
m := Machine{ m := Machine{
ID: 0, ID: 0,
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
@@ -21,7 +16,7 @@ func (s *Suite) TestRegisterMachine(c *check.C) {
Name: "testmachine", Name: "testmachine",
NamespaceID: n.ID, NamespaceID: n.ID,
} }
db.Save(&m) h.db.Save(&m)
_, err = h.GetMachine("test", "testmachine") _, err = h.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View File

@@ -9,7 +9,7 @@ import (
) )
var NamespaceCmd = &cobra.Command{ var NamespaceCmd = &cobra.Command{
Use: "namespace", Use: "namespaces",
Short: "Manage the namespaces of Headscale", Short: "Manage the namespaces of Headscale",
} }

View File

@@ -9,6 +9,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var NodeCmd = &cobra.Command{
Use: "nodes",
Short: "Manage the nodes of Headscale",
}
var RegisterCmd = &cobra.Command{ var RegisterCmd = &cobra.Command{
Use: "register machineID", Use: "register machineID",
Short: "Registers a machine to your network", Short: "Registers a machine to your network",
@@ -81,8 +86,3 @@ var ListNodesCmd = &cobra.Command{
}, },
} }
var NodeCmd = &cobra.Command{
Use: "node",
Short: "Manage the nodes of Headscale",
}

View File

@@ -11,7 +11,7 @@ import (
) )
var PreauthkeysCmd = &cobra.Command{ var PreauthkeysCmd = &cobra.Command{
Use: "preauthkey", Use: "preauthkeys",
Short: "Handle the preauthkeys in Headscale", Short: "Handle the preauthkeys in Headscale",
} }
@@ -44,11 +44,19 @@ var ListPreAuthKeys = &cobra.Command{
if k.Expiration != nil { if k.Expiration != nil {
expiration = k.Expiration.Format("2006-01-02 15:04:05") expiration = k.Expiration.Format("2006-01-02 15:04:05")
} }
var reusable string
if k.Ephemeral {
reusable = "N/A"
} else {
reusable = fmt.Sprintf("%v", k.Reusable)
}
fmt.Printf( fmt.Printf(
"key: %s, namespace: %s, reusable: %v, ephemeral: %v, expiration: %s, created_at: %s\n", "key: %s, namespace: %s, reusable: %s, ephemeral: %v, expiration: %s, created_at: %s\n",
k.Key, k.Key,
k.Namespace.Name, k.Namespace.Name,
k.Reusable, reusable,
k.Ephemeral, k.Ephemeral,
expiration, expiration,
k.CreatedAt.Format("2006-01-02 15:04:05"), k.CreatedAt.Format("2006-01-02 15:04:05"),

View File

@@ -48,7 +48,8 @@ func LoadConfig(path string) error {
} }
if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
errorText += "Fatal config error: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, listen_addr must end in :443\n" // this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
log.Println("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
} }
if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") { if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
@@ -119,6 +120,16 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// We are doing this here, as in the future could be cool to have it also hot-reload
if viper.GetString("acl_policy_path") != "" {
err = h.LoadACLPolicy(absPath(viper.GetString("acl_policy_path")))
if err != nil {
log.Printf("Could not load the ACL policy: %s", err)
}
}
return h, nil return h, nil
} }

View File

@@ -126,6 +126,5 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
configYaml = []byte("---\nserver_url: \"http://127.0.0.1:8000\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"") configYaml = []byte("---\nserver_url: \"http://127.0.0.1:8000\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"")
writeConfig(c, tmpDir, configYaml) writeConfig(c, tmpDir, configYaml)
err = cli.LoadConfig(tmpDir) err = cli.LoadConfig(tmpDir)
c.Assert(err, check.NotNil) c.Assert(err, check.IsNil)
c.Assert(err, check.ErrorMatches, "Fatal config error: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, listen_addr must end in :443.*")
} }

View File

@@ -14,5 +14,6 @@
"tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_cache_dir": ".cache",
"tls_letsencrypt_challenge_type": "HTTP-01", "tls_letsencrypt_challenge_type": "HTTP-01",
"tls_cert_path": "", "tls_cert_path": "",
"tls_key_path": "" "tls_key_path": "",
"acl_policy_path": ""
} }

View File

@@ -10,5 +10,6 @@
"tls_letsencrypt_cache_dir": ".cache", "tls_letsencrypt_cache_dir": ".cache",
"tls_letsencrypt_challenge_type": "HTTP-01", "tls_letsencrypt_challenge_type": "HTTP-01",
"tls_cert_path": "", "tls_cert_path": "",
"tls_key_path": "" "tls_key_path": "",
"acl_policy_path": ""
} }

38
db.go
View File

@@ -6,6 +6,7 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
const dbVersion = "1" const dbVersion = "1"
@@ -17,10 +18,12 @@ type KV struct {
} }
func (h *Headscale) initDB() error { func (h *Headscale) initDB() error {
db, err := h.db() db, err := h.openDB()
if err != nil { if err != nil {
return err return err
} }
h.db = db
if h.dbType == "postgres" { if h.dbType == "postgres" {
db.Exec("create extension if not exists \"uuid-ossp\";") db.Exec("create extension if not exists \"uuid-ossp\";")
} }
@@ -45,36 +48,40 @@ func (h *Headscale) initDB() error {
return err return err
} }
func (h *Headscale) db() (*gorm.DB, error) { func (h *Headscale) openDB() (*gorm.DB, error) {
var db *gorm.DB var db *gorm.DB
var err error var err error
var log logger.Interface
if h.dbDebug {
log = logger.Default
} else {
log = logger.Default.LogMode(logger.Silent)
}
switch h.dbType { switch h.dbType {
case "sqlite3": case "sqlite3":
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{ db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
}) })
case "postgres": case "postgres":
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{ db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
}) })
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if h.dbDebug {
db.Debug()
}
return db, nil return db, nil
} }
func (h *Headscale) getValue(key string) (string, error) { func (h *Headscale) getValue(key string) (string, error) {
db, err := h.db()
if err != nil {
return "", err
}
var row KV var row KV
if result := db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", errors.New("not found") return "", errors.New("not found")
} }
return row.Value, nil return row.Value, nil
@@ -85,16 +92,13 @@ func (h *Headscale) setValue(key string, value string) error {
Key: key, Key: key,
Value: value, Value: value,
} }
db, err := h.db()
if err != nil { _, err := h.getValue(key)
return err
}
_, err = h.getValue(key)
if err == nil { if err == nil {
db.Model(&kv).Where("key = ?", key).Update("value", value) h.db.Model(&kv).Where("key = ?", key).Update("value", value)
return nil return nil
} }
db.Create(kv) h.db.Create(kv)
return nil return nil
} }

View File

@@ -1,5 +1,5 @@
# This file contains some of the official Tailscale DERP servers, # This file contains some of the official Tailscale DERP servers,
# shamelessly taken from https://github.com/tailscale/tailscale/blob/main/derp/derpmap/derpmap.go # shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json
# #
# If you plan to somehow use headscale, please deploy your own DERP infra # If you plan to somehow use headscale, please deploy your own DERP infra
regions: regions:
@@ -16,6 +16,14 @@ regions:
stunport: 0 stunport: 0
stunonly: false stunonly: false
derptestport: 0 derptestport: 0
- name: 1b
regionid: 1
hostname: derp1b.tailscale.com
ipv4: 45.55.35.93
ipv6: "2604:a880:800:a1::f:2001"
stunport: 0
stunonly: false
derptestport: 0
2: 2:
regionid: 2 regionid: 2
regioncode: sfo regioncode: sfo
@@ -29,6 +37,14 @@ regions:
stunport: 0 stunport: 0
stunonly: false stunonly: false
derptestport: 0 derptestport: 0
- name: 2b
regionid: 2
hostname: derp2b.tailscale.com
ipv4: 64.227.106.23
ipv6: "2604:a880:4:1d0::29:9000"
stunport: 0
stunonly: false
derptestport: 0
3: 3:
regionid: 3 regionid: 3
regioncode: sin regioncode: sin
@@ -54,4 +70,77 @@ regions:
ipv6: "2a03:b0c0:3:e0::36e:900" ipv6: "2a03:b0c0:3:e0::36e:900"
stunport: 0 stunport: 0
stunonly: false stunonly: false
derptestport: 0 derptestport: 0
- name: 4b
regionid: 4
hostname: derp4b.tailscale.com
ipv4: 157.230.25.0
ipv6: "2a03:b0c0:3:e0::58f:3001"
stunport: 0
stunonly: false
derptestport: 0
5:
regionid: 5
regioncode: syd
regionname: Sydney
nodes:
- name: 5a
regionid: 5
hostname: derp5.tailscale.com
ipv4: 103.43.75.49
ipv6: "2001:19f0:5801:10b7:5400:2ff:feaa:284c"
stunport: 0
stunonly: false
derptestport: 0
6:
regionid: 6
regioncode: blr
regionname: Bangalore
nodes:
- name: 6a
regionid: 6
hostname: derp6.tailscale.com
ipv4: 68.183.90.120
ipv6: "2400:6180:100:d0::982:d001"
stunport: 0
stunonly: false
derptestport: 0
7:
regionid: 7
regioncode: tok
regionname: Tokyo
nodes:
- name: 7a
regionid: 7
hostname: derp7.tailscale.com
ipv4: 167.179.89.145
ipv6: "2401:c080:1000:467f:5400:2ff:feee:22aa"
stunport: 0
stunonly: false
derptestport: 0
8:
regionid: 8
regioncode: lhr
regionname: London
nodes:
- name: 8a
regionid: 8
hostname: derp8.tailscale.com
ipv4: 167.71.139.179
ipv6: "2a03:b0c0:1:e0::3cc:e001"
stunport: 0
stunonly: false
derptestport: 0
9:
regionid: 9
regioncode: sao
regionname: São Paulo
nodes:
- name: 9a
regionid: 9
hostname: derp9.tailscale.com
ipv4: 207.148.3.137
ipv6: "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"
stunport: 0
stunonly: false
derptestport: 0

1
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/mattn/go-sqlite3 v1.14.7 // indirect
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.8.1 github.com/spf13/viper v1.8.1
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

2
go.sum
View File

@@ -746,6 +746,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs= github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=

View File

@@ -154,14 +154,8 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
} }
func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
machines := []Machine{} machines := []Machine{}
if err = db.Where("namespace_id = ? AND machine_key <> ? AND registered", if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
log.Printf("Error accessing db: %s", err) log.Printf("Error accessing db: %s", err)
return nil, err return nil, err

View File

@@ -11,11 +11,6 @@ func (s *Suite) TestGetMachine(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
_, err = h.GetMachine("test", "testmachine") _, err = h.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
@@ -30,7 +25,7 @@ func (s *Suite) TestGetMachine(c *check.C) {
RegisterMethod: "authKey", RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.Save(&m) h.db.Save(&m)
m1, err := h.GetMachine("test", "testmachine") m1, err := h.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View File

@@ -25,18 +25,12 @@ type Namespace struct {
// CreateNamespace creates a new Namespace. Returns error if could not be created // CreateNamespace creates a new Namespace. Returns error if could not be created
// or another namespace already exists // or another namespace already exists
func (h *Headscale) CreateNamespace(name string) (*Namespace, error) { func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
n := Namespace{} n := Namespace{}
if err := db.Where("name = ?", name).First(&n).Error; err == nil { if err := h.db.Where("name = ?", name).First(&n).Error; err == nil {
return nil, errorNamespaceExists return nil, errorNamespaceExists
} }
n.Name = name n.Name = name
if err := db.Create(&n).Error; err != nil { if err := h.db.Create(&n).Error; err != nil {
log.Printf("Could not create row: %s", err) log.Printf("Could not create row: %s", err)
return nil, err return nil, err
} }
@@ -46,12 +40,6 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
// DestroyNamespace destroys a Namespace. Returns error if the Namespace does // DestroyNamespace destroys a Namespace. Returns error if the Namespace does
// not exist or if there are machines associated with it. // not exist or if there are machines associated with it.
func (h *Headscale) DestroyNamespace(name string) error { func (h *Headscale) DestroyNamespace(name string) error {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return err
}
n, err := h.GetNamespace(name) n, err := h.GetNamespace(name)
if err != nil { if err != nil {
return errorNamespaceNotFound return errorNamespaceNotFound
@@ -65,7 +53,7 @@ func (h *Headscale) DestroyNamespace(name string) error {
return errorNamespaceNotEmpty return errorNamespaceNotEmpty
} }
if result := db.Unscoped().Delete(&n); result.Error != nil { if result := h.db.Unscoped().Delete(&n); result.Error != nil {
return err return err
} }
@@ -74,14 +62,8 @@ func (h *Headscale) DestroyNamespace(name string) error {
// GetNamespace fetches a namespace by name // GetNamespace fetches a namespace by name
func (h *Headscale) GetNamespace(name string) (*Namespace, error) { func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
n := Namespace{} n := Namespace{}
if result := db.First(&n, "name = ?", name); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.First(&n, "name = ?", name); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errorNamespaceNotFound return nil, errorNamespaceNotFound
} }
return &n, nil return &n, nil
@@ -89,13 +71,8 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
// ListNamespaces gets all the existing namespaces // ListNamespaces gets all the existing namespaces
func (h *Headscale) ListNamespaces() (*[]Namespace, error) { func (h *Headscale) ListNamespaces() (*[]Namespace, error) {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
namespaces := []Namespace{} namespaces := []Namespace{}
if err := db.Find(&namespaces).Error; err != nil { if err := h.db.Find(&namespaces).Error; err != nil {
return nil, err return nil, err
} }
return &namespaces, nil return &namespaces, nil
@@ -107,14 +84,9 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
machines := []Machine{} machines := []Machine{}
if err := db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { if err := h.db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
return nil, err return nil, err
} }
return &machines, nil return &machines, nil
@@ -126,23 +98,18 @@ func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error
if err != nil { if err != nil {
return err return err
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return err
}
m.NamespaceID = n.ID m.NamespaceID = n.ID
db.Save(&m) h.db.Save(&m)
return nil return nil
} }
func (n *Namespace) toUser() *tailcfg.User { func (n *Namespace) toUser() *tailcfg.User {
u := tailcfg.User{ u := tailcfg.User{
ID: tailcfg.UserID(n.ID), ID: tailcfg.UserID(n.ID),
LoginName: "", LoginName: n.Name,
DisplayName: n.Name, DisplayName: n.Name,
ProfilePicURL: "", ProfilePicURL: "",
Domain: "", Domain: "headscale.net",
Logins: []tailcfg.LoginID{}, Logins: []tailcfg.LoginID{},
Created: time.Time{}, Created: time.Time{},
} }

View File

@@ -30,10 +30,6 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
m := Machine{ m := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
@@ -45,7 +41,7 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
RegisterMethod: "authKey", RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.Save(&m) h.db.Save(&m)
err = h.DestroyNamespace("test") err = h.DestroyNamespace("test")
c.Assert(err, check.Equals, errorNamespaceNotEmpty) c.Assert(err, check.Equals, errorNamespaceNotEmpty)

View File

@@ -4,7 +4,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors" "errors"
"log"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -34,12 +33,6 @@ func (h *Headscale) CreatePreAuthKey(namespaceName string, reusable bool, epheme
return nil, err return nil, err
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
now := time.Now().UTC() now := time.Now().UTC()
kstr, err := h.generateKey() kstr, err := h.generateKey()
if err != nil { if err != nil {
@@ -55,7 +48,7 @@ func (h *Headscale) CreatePreAuthKey(namespaceName string, reusable bool, epheme
CreatedAt: &now, CreatedAt: &now,
Expiration: expiration, Expiration: expiration,
} }
db.Save(&k) h.db.Save(&k)
return &k, nil return &k, nil
} }
@@ -66,14 +59,9 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
keys := []PreAuthKey{} keys := []PreAuthKey{}
if err := db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: n.ID}).Find(&keys).Error; err != nil { if err := h.db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: n.ID}).Find(&keys).Error; err != nil {
return nil, err return nil, err
} }
return &keys, nil return &keys, nil
@@ -82,13 +70,8 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error)
// checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node // checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node
// If returns no error and a PreAuthKey, it can be used // If returns no error and a PreAuthKey, it can be used
func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
db, err := h.db()
if err != nil {
return nil, err
}
pak := PreAuthKey{} pak := PreAuthKey{}
if result := db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errorAuthKeyNotFound return nil, errorAuthKeyNotFound
} }
@@ -101,7 +84,7 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
} }
machines := []Machine{} machines := []Machine{}
if err := db.Preload("AuthKey").Where(&Machine{AuthKeyID: uint(pak.ID)}).Find(&machines).Error; err != nil { if err := h.db.Preload("AuthKey").Where(&Machine{AuthKeyID: uint(pak.ID)}).Find(&machines).Error; err != nil {
return nil, err return nil, err
} }

View File

@@ -73,10 +73,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
m := Machine{ m := Machine{
ID: 0, ID: 0,
MachineKey: "foo", MachineKey: "foo",
@@ -88,7 +84,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
RegisterMethod: "authKey", RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.Save(&m) h.db.Save(&m)
p, err := h.checkKeyValidity(pak.Key) p, err := h.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed) c.Assert(err, check.Equals, errorAuthKeyNotReusableAlreadyUsed)
@@ -102,10 +98,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, true, false, nil) pak, err := h.CreatePreAuthKey(n.Name, true, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
m := Machine{ m := Machine{
ID: 1, ID: 1,
MachineKey: "foo", MachineKey: "foo",
@@ -117,7 +109,7 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
RegisterMethod: "authKey", RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.Save(&m) h.db.Save(&m)
p, err := h.checkKeyValidity(pak.Key) p, err := h.checkKeyValidity(pak.Key)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -143,10 +135,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, false, true, nil) pak, err := h.CreatePreAuthKey(n.Name, false, true, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
now := time.Now() now := time.Now()
m := Machine{ m := Machine{
ID: 0, ID: 0,
@@ -160,7 +148,7 @@ func (*Suite) TestEphemeralKey(c *check.C) {
LastSeen: &now, LastSeen: &now,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
db.Save(&m) h.db.Save(&m)
_, err = h.checkKeyValidity(pak.Key) _, err = h.checkKeyValidity(pak.Key)
// Ephemeral keys are by definition reusable // Ephemeral keys are by definition reusable

View File

@@ -3,7 +3,6 @@ package headscale
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"log"
"gorm.io/datatypes" "gorm.io/datatypes"
"inet.af/netaddr" "inet.af/netaddr"
@@ -42,15 +41,9 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
for _, rIP := range hi.RoutableIPs { for _, rIP := range hi.RoutableIPs {
if rIP == route { if rIP == route {
db, err := h.db()
if err != nil {
log.Printf("Cannot open DB: %s", err)
return nil, err
}
routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest
m.EnabledRoutes = datatypes.JSON(routes) m.EnabledRoutes = datatypes.JSON(routes)
db.Save(&m) h.db.Save(&m)
// THIS IS COMPLETELY USELESS. // THIS IS COMPLETELY USELESS.
// The peers map is stored in memory in the server process. // The peers map is stored in memory in the server process.

View File

@@ -16,11 +16,6 @@ func (s *Suite) TestGetRoutes(c *check.C) {
pak, err := h.CreatePreAuthKey(n.Name, false, false, nil) pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
db, err := h.db()
if err != nil {
c.Fatal(err)
}
_, err = h.GetMachine("test", "testmachine") _, err = h.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
@@ -45,7 +40,7 @@ func (s *Suite) TestGetRoutes(c *check.C) {
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostinfo), HostInfo: datatypes.JSON(hostinfo),
} }
db.Save(&m) h.db.Save(&m)
r, err := h.GetNodeRoutes("test", "testmachine") r, err := h.GetNodeRoutes("test", "testmachine")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View File

@@ -0,0 +1,127 @@
{
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [
"user1@example.com",
"user2@example.com",
],
"group:example2": [
"user1@example.com",
"user2@example.com",
],
},
// Declare hostname aliases to use in place of IP addresses or subnets.
"Hosts": {
"example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24",
},
// Define who is allowed to use which tags.
"TagOwners": {
// Everyone in the montreal-admins or global-admins group are
// allowed to tag servers as montreal-webserver.
"tag:montreal-webserver": [
"group:example",
],
// Only a few admins are allowed to create API servers.
"tag:production": [
"group:example",
"president@example.com",
],
},
// Access control lists.
"ACLs": [
// Engineering users, plus the president, can access port 22 (ssh)
// and port 3389 (remote desktop protocol) on all servers, and all
// ports on git-server or ci-server.
{
"Action": "accept",
"Users": [
"group:example2",
"192.168.1.0/24"
],
"Ports": [
"*:22,3389",
"git-server:*",
"ci-server:*"
],
},
// Allow engineer users to access any port on a device tagged with
// tag:production.
{
"Action": "accept",
"Users": [
"group:example"
],
"Ports": [
"tag:production:*"
],
},
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks.
{
"Action": "accept",
"Users": [
"example-host-2",
],
"Ports": [
"example-host-1:*",
"192.168.1.0/24:*"
],
},
// Allow every user of your network to access anything on the network.
// Comment out this section if you want to define specific ACL
// restrictions above.
{
"Action": "accept",
"Users": [
"*"
],
"Ports": [
"*:*"
],
},
// All users in Montreal are allowed to access the Montreal web
// servers.
{
"Action": "accept",
"Users": [
"example-host-1"
],
"Ports": [
"tag:montreal-webserver:80,443"
],
},
// Montreal web servers are allowed to make outgoing connections to
// the API servers, but only on https port 443.
// In contrast, this doesn't grant API servers the right to initiate
// any connections.
{
"Action": "accept",
"Users": [
"tag:montreal-webserver"
],
"Ports": [
"tag:api-server:443"
],
},
],
// Declare tests to check functionality of ACL rules
"Tests": [
{
"User": "user1@example.com",
"Allow": [
"example-host-1:22",
"example-host-2:80"
],
"Deny": [
"exapmle-host-2:100"
],
},
{
"User": "user2@example.com",
"Allow": [
"100.60.3.4:22"
],
},
],
}

View File

@@ -0,0 +1,24 @@
// This ACL is a very basic example to validate the
// expansion of hosts
{
"Hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"ACLs": [
{
"Action": "accept",
"Users": [
"subnet-1",
"192.168.1.0/24"
],
"Ports": [
"*:22,3389",
"host-1:*",
],
},
],
}

View File

@@ -0,0 +1,26 @@
// This ACL is used to test group expansion
{
"Groups": {
"group:example": [
"testnamespace",
],
},
"Hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"ACLs": [
{
"Action": "accept",
"Users": [
"group:example",
],
"Ports": [
"host-1:*",
],
},
],
}

View File

@@ -0,0 +1,20 @@
// This ACL is used to test namespace expansion
{
"Hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"ACLs": [
{
"Action": "accept",
"Users": [
"testnamespace",
],
"Ports": [
"host-1:*",
],
},
],
}

View File

@@ -0,0 +1,20 @@
// This ACL is used to test the port range expansion
{
"Hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"ACLs": [
{
"Action": "accept",
"Users": [
"subnet-1",
],
"Ports": [
"host-1:5400-5500",
],
},
],
}

View File

@@ -0,0 +1,20 @@
// This ACL is used to test wildcards
{
"Hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"ACLs": [
{
"Action": "accept",
"Users": [
"*",
],
"Ports": [
"host-1:*",
],
},
],
}

View File

@@ -0,0 +1,125 @@
{
// Declare static groups of users beyond those in the identity service.
"Groups": {
"group:example": [
"user1@example.com",
"user2@example.com",
],
},
// Declare hostname aliases to use in place of IP addresses or subnets.
"Hosts": {
"example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24",
},
// Define who is allowed to use which tags.
"TagOwners": {
// Everyone in the montreal-admins or global-admins group are
// allowed to tag servers as montreal-webserver.
"tag:montreal-webserver": [
"group:montreal-admins",
"group:global-admins",
],
// Only a few admins are allowed to create API servers.
"tag:api-server": [
"group:global-admins",
"example-host-1",
],
},
// Access control lists.
"ACLs": [
// Engineering users, plus the president, can access port 22 (ssh)
// and port 3389 (remote desktop protocol) on all servers, and all
// ports on git-server or ci-server.
{
"Action": "accept",
"Users": [
"group:engineering",
"president@example.com"
],
"Ports": [
"*:22,3389",
"git-server:*",
"ci-server:*"
],
},
// Allow engineer users to access any port on a device tagged with
// tag:production.
{
"Action": "accept",
"Users": [
"group:engineers"
],
"Ports": [
"tag:production:*"
],
},
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
// on both networks.
{
"Action": "accept",
"Users": [
"my-subnet",
"192.168.1.0/24"
],
"Ports": [
"my-subnet:*",
"192.168.1.0/24:*"
],
},
// Allow every user of your network to access anything on the network.
// Comment out this section if you want to define specific ACL
// restrictions above.
{
"Action": "accept",
"Users": [
"*"
],
"Ports": [
"*:*"
],
},
// All users in Montreal are allowed to access the Montreal web
// servers.
{
"Action": "accept",
"Users": [
"group:montreal-users"
],
"Ports": [
"tag:montreal-webserver:80,443"
],
},
// Montreal web servers are allowed to make outgoing connections to
// the API servers, but only on https port 443.
// In contrast, this doesn't grant API servers the right to initiate
// any connections.
{
"Action": "accept",
"Users": [
"tag:montreal-webserver"
],
"Ports": [
"tag:api-server:443"
],
},
],
// Declare tests to check functionality of ACL rules
"Tests": [
{
"User": "user1@example.com",
"Allow": [
"example-host-1:22",
"example-host-2:80"
],
"Deny": [
"exapmle-host-2:100"
],
},
{
"User": "user2@example.com",
"Allow": [
"100.60.3.4:22"
],
},
],
}

1
tests/acls/broken.hujson Normal file
View File

@@ -0,0 +1 @@
{

View File

@@ -0,0 +1,4 @@
{
"valid_json": true,
"but_a_policy_though": false
}

View File

@@ -78,10 +78,6 @@ func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, err
} }
func (h *Headscale) getAvailableIP() (*net.IP, error) { func (h *Headscale) getAvailableIP() (*net.IP, error) {
db, err := h.db()
if err != nil {
return nil, err
}
i := 0 i := 0
for { for {
ip, err := getRandomIP() ip, err := getRandomIP()
@@ -89,7 +85,7 @@ func (h *Headscale) getAvailableIP() (*net.IP, error) {
return nil, err return nil, err
} }
m := Machine{} m := Machine{}
if result := db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) { if result := h.db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
return ip, nil return ip, nil
} }
i++ i++