diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ca4d4cf2..963663a7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,12 +18,11 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
- go-version: "1.16.3"
+ go-version: "1.17.3"
- name: Install dependencies
run: |
go version
- go install golang.org/x/lint/golint@latest
sudo apt update
sudo apt install -y make
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6b561d24..b3c64006 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,20 +1,37 @@
+---
name: CI
on: [push, pull_request]
jobs:
- # The "build" workflow
- lint:
- # The type of runner that the job will run on
+ golangci-lint:
runs-on: ubuntu-latest
-
- # Steps represent a sequence of tasks that will be executed as part of the job
steps:
- # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- # Install and run golangci-lint as a separate step, it's much faster this
- # way because this action has caching. It'll get run again in `make lint`
- # below, but it's still much faster in the end than installing
- # golangci-lint manually in the `Run lint` step.
- - uses: golangci/golangci-lint-action@v2
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v2
+ with:
+ version: latest
+
+ prettier-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Prettify code
+ uses: creyD/prettier_action@v4.0
+ with:
+ prettier_options: >-
+ --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
+ only_changed: false
+ dry: true
+
+ proto-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: bufbuild/buf-setup-action@v0.7.0
+ - uses: bufbuild/buf-lint-action@v1
+ with:
+ input: "proto"
diff --git a/.golangci.yaml b/.golangci.yaml
index a97c2bb5..2defdc88 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,7 +1,53 @@
---
run:
- timeout: 5m
+ timeout: 10m
issues:
skip-dirs:
- gen
+linters:
+ enable-all: true
+ disable:
+ - exhaustivestruct
+ - revive
+ - lll
+ - interfacer
+ - scopelint
+ - maligned
+ - golint
+ - gofmt
+ - gochecknoglobals
+ - gochecknoinits
+ - gocognit
+ - funlen
+ - exhaustivestruct
+ - tagliatelle
+ - godox
+ - ireturn
+
+ # In progress
+ - gocritic
+
+ # We should strive to enable these:
+ - wrapcheck
+ - dupl
+ - makezero
+
+ # We might want to enable this, but it might be a lot of work
+ - cyclop
+ - nestif
+ - wsl # might be incompatible with gofumpt
+ - testpackage
+ - paralleltest
+
+linters-settings:
+ varnamelen:
+ ignore-type-assert-ok: true
+ ignore-map-index-ok: true
+ ignore-names:
+ - err
+ - db
+ - id
+ - ip
+ - ok
+ - c
diff --git a/Makefile b/Makefile
index 92beaefb..060d3b9c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,14 @@
# Calculate version
version = $(shell ./scripts/version-at-commit.sh)
+rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
+
+# GO_SOURCES = $(wildcard *.go)
+# PROTO_SOURCES = $(wildcard **/*.proto)
+GO_SOURCES = $(call rwildcard,,*.go)
+PROTO_SOURCES = $(call rwildcard,,*.proto)
+
+
build:
go build -ldflags "-s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=$(version)" cmd/headscale/headscale.go
@@ -19,7 +27,12 @@ coverprofile_html:
go tool cover -html=coverage.out
lint:
- golangci-lint run --fix
+ golangci-lint run --fix --timeout 10m
+
+fmt:
+ prettier --write '**/**.{ts,js,md,yaml,yml,sass,css,scss,html}'
+ golines --max-len=88 --base-formatter=gofumpt -w $(GO_SOURCES)
+ clang-format -style="{BasedOnStyle: Google, IndentWidth: 4, AlignConsecutiveDeclarations: true, AlignConsecutiveAssignments: true, ColumnLimit: 0}" -i $(PROTO_SOURCES)
proto-lint:
cd proto/ && buf lint
diff --git a/README.md b/README.md
index e9f7990f..41f388ea 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,6 @@ Suggestions/PRs welcomed!
Please have a look at the documentation under [`docs/`](docs/).
-
## Disclaimer
1. We have nothing to do with Tailscale, or Tailscale Inc.
@@ -64,13 +63,30 @@ Please have a look at the documentation under [`docs/`](docs/).
To contribute to Headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator).
+### Code style
+
+To ensure we have some consistency with a growing number of contributes, this project has adopted linting and style/formatting rules:
+
+The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
+formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
+[`gofumpt`](https://github.com/mvdan/gofumpt).
+Please configure your editor to run the tools while developing and make sure to
+run `make lint` and `make fmt` before committing any code.
+
+The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and
+formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
+
+The **rest** (markdown, yaml, etc) is formatted with [`prettier`](https://prettier.io).
+
+Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
+
### Install development tools
- Go
- Buf
- Protobuf tools:
-```shell
+```shell
make install-protobuf-plugins
```
@@ -81,6 +97,7 @@ Some parts of the project requires the generation of Go code from Protobuf (if c
```shell
make generate
```
+
**Note**: Please check in changes from `gen/` in a separate commit to make it easier to review.
To run the tests:
@@ -261,5 +278,3 @@ make build
-
-
diff --git a/acls.go b/acls.go
index cb02e044..1550c340 100644
--- a/acls.go
+++ b/acls.go
@@ -9,23 +9,30 @@ import (
"strings"
"github.com/rs/zerolog/log"
-
"github.com/tailscale/hujson"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
const (
- errorEmptyPolicy = Error("empty policy")
- errorInvalidAction = Error("invalid action")
- errorInvalidUserSection = Error("invalid user section")
- errorInvalidGroup = Error("invalid group")
- errorInvalidTag = Error("invalid tag")
- errorInvalidNamespace = Error("invalid namespace")
- errorInvalidPortFormat = Error("invalid port format")
+ errEmptyPolicy = Error("empty policy")
+ errInvalidAction = Error("invalid action")
+ errInvalidUserSection = Error("invalid user section")
+ errInvalidGroup = Error("invalid group")
+ errInvalidTag = Error("invalid tag")
+ errInvalidNamespace = Error("invalid namespace")
+ errInvalidPortFormat = Error("invalid port format")
)
-// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules
+const (
+ Base10 = 10
+ BitSize16 = 16
+ portRangeBegin = 0
+ portRangeEnd = 65535
+ expectedTokenItems = 2
+)
+
+// 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 {
@@ -34,23 +41,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
defer policyFile.Close()
var policy ACLPolicy
- b, err := io.ReadAll(policyFile)
+ policyBytes, err := io.ReadAll(policyFile)
if err != nil {
return err
}
- ast, err := hujson.Parse(b)
+ ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
- b = ast.Pack()
- err = json.Unmarshal(b, &policy)
+ policyBytes = ast.Pack()
+ err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
if policy.IsZero() {
- return errorEmptyPolicy
+ return errEmptyPolicy
}
h.aclPolicy = &policy
@@ -59,37 +66,40 @@ func (h *Headscale) LoadACLPolicy(path string) error {
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
+ for index, acl := range h.aclPolicy.ACLs {
+ if acl.Action != "accept" {
+ return nil, errInvalidAction
}
- r := tailcfg.FilterRule{}
+ filterRule := tailcfg.FilterRule{}
srcIPs := []string{}
- for j, u := range a.Users {
- srcs, err := h.generateACLPolicySrcIP(u)
+ for innerIndex, user := range acl.Users {
+ srcs, err := h.generateACLPolicySrcIP(user)
if err != nil {
log.Error().
- Msgf("Error parsing ACL %d, User %d", i, j)
+ Msgf("Error parsing ACL %d, User %d", index, innerIndex)
+
return nil, err
}
srcIPs = append(srcIPs, srcs...)
}
- r.SrcIPs = srcIPs
+ filterRule.SrcIPs = srcIPs
destPorts := []tailcfg.NetPortRange{}
- for j, d := range a.Ports {
- dests, err := h.generateACLPolicyDestPorts(d)
+ for innerIndex, ports := range acl.Ports {
+ dests, err := h.generateACLPolicyDestPorts(ports)
if err != nil {
log.Error().
- Msgf("Error parsing ACL %d, Port %d", i, j)
+ Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
+
return nil, err
}
destPorts = append(destPorts, dests...)
@@ -108,10 +118,12 @@ func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
return h.expandAlias(u)
}
-func (h *Headscale) generateACLPolicyDestPorts(d string) ([]tailcfg.NetPortRange, error) {
+func (h *Headscale) generateACLPolicyDestPorts(
+ d string,
+) ([]tailcfg.NetPortRange, error) {
tokens := strings.Split(d, ":")
- if len(tokens) < 2 || len(tokens) > 3 {
- return nil, errorInvalidPortFormat
+ if len(tokens) < expectedTokenItems || len(tokens) > 3 {
+ return nil, errInvalidPortFormat
}
var alias string
@@ -121,7 +133,7 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) ([]tailcfg.NetPortRange
// tag:montreal-webserver:80,443
// tag:api-server:443
// example-host-1:*
- if len(tokens) == 2 {
+ if len(tokens) == expectedTokenItems {
alias = tokens[0]
} else {
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
@@ -146,34 +158,36 @@ func (h *Headscale) generateACLPolicyDestPorts(d string) ([]tailcfg.NetPortRange
dests = append(dests, pr)
}
}
+
return dests, nil
}
-func (h *Headscale) expandAlias(s string) ([]string, error) {
- if s == "*" {
+func (h *Headscale) expandAlias(alias string) ([]string, error) {
+ if alias == "*" {
return []string{"*"}, nil
}
- if strings.HasPrefix(s, "group:") {
- if _, ok := h.aclPolicy.Groups[s]; !ok {
- return nil, errorInvalidGroup
+ if strings.HasPrefix(alias, "group:") {
+ if _, ok := h.aclPolicy.Groups[alias]; !ok {
+ return nil, errInvalidGroup
}
ips := []string{}
- for _, n := range h.aclPolicy.Groups[s] {
+ for _, n := range h.aclPolicy.Groups[alias] {
nodes, err := h.ListMachinesInNamespace(n)
if err != nil {
- return nil, errorInvalidNamespace
+ return nil, errInvalidNamespace
}
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
+ if strings.HasPrefix(alias, "tag:") {
+ if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
+ return nil, errInvalidTag
}
// This will have HORRIBLE performance.
@@ -183,10 +197,10 @@ func (h *Headscale) expandAlias(s string) ([]string, error) {
return nil, err
}
ips := []string{}
- for _, m := range machines {
+ for _, machine := range machines {
hostinfo := tailcfg.Hostinfo{}
- if len(m.HostInfo) != 0 {
- hi, err := m.HostInfo.MarshalJSON()
+ if len(machine.HostInfo) != 0 {
+ hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
@@ -197,17 +211,19 @@ func (h *Headscale) expandAlias(s string) ([]string, error) {
// FIXME: Check TagOwners allows this
for _, t := range hostinfo.RequestTags {
- if s[4:] == t {
- ips = append(ips, m.IPAddress)
+ if alias[4:] == t {
+ ips = append(ips, machine.IPAddress)
+
break
}
}
}
}
+
return ips, nil
}
- n, err := h.GetNamespace(s)
+ n, err := h.GetNamespace(alias)
if err == nil {
nodes, err := h.ListMachinesInNamespace(n.Name)
if err != nil {
@@ -217,49 +233,54 @@ func (h *Headscale) expandAlias(s string) ([]string, error) {
for _, n := range nodes {
ips = append(ips, n.IPAddress)
}
+
return ips, nil
}
- if h, ok := h.aclPolicy.Hosts[s]; ok {
+ if h, ok := h.aclPolicy.Hosts[alias]; ok {
return []string{h.String()}, nil
}
- ip, err := netaddr.ParseIP(s)
+ ip, err := netaddr.ParseIP(alias)
if err == nil {
return []string{ip.String()}, nil
}
- cidr, err := netaddr.ParseIPPrefix(s)
+ cidr, err := netaddr.ParseIPPrefix(alias)
if err == nil {
return []string{cidr.String()}, nil
}
- return nil, errorInvalidUserSection
+ return nil, errInvalidUserSection
}
-func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) {
- if s == "*" {
- return &[]tailcfg.PortRange{{First: 0, Last: 65535}}, nil
+func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
+ if portsStr == "*" {
+ return &[]tailcfg.PortRange{
+ {First: portRangeBegin, Last: portRangeEnd},
+ }, 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)
+ for _, portStr := range strings.Split(portsStr, ",") {
+ rang := strings.Split(portStr, "-")
+ switch len(rang) {
+ case 1:
+ port, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
ports = append(ports, tailcfg.PortRange{
- First: uint16(pi),
- Last: uint16(pi),
+ First: uint16(port),
+ Last: uint16(port),
})
- } else if len(rang) == 2 {
- start, err := strconv.ParseUint(rang[0], 10, 16)
+
+ case expectedTokenItems:
+ start, err := strconv.ParseUint(rang[0], Base10, BitSize16)
if err != nil {
return nil, err
}
- last, err := strconv.ParseUint(rang[1], 10, 16)
+ last, err := strconv.ParseUint(rang[1], Base10, BitSize16)
if err != nil {
return nil, err
}
@@ -267,9 +288,11 @@ func (h *Headscale) expandPorts(s string) (*[]tailcfg.PortRange, error) {
First: uint16(start),
Last: uint16(last),
})
- } else {
- return nil, errorInvalidPortFormat
+
+ default:
+ return nil, errInvalidPortFormat
}
}
+
return &ports, nil
}
diff --git a/acls_test.go b/acls_test.go
index da7f3ecc..3e051f5f 100644
--- a/acls_test.go
+++ b/acls_test.go
@@ -5,54 +5,58 @@ import (
)
func (s *Suite) TestWrongPath(c *check.C) {
- err := h.LoadACLPolicy("asdfg")
+ err := app.LoadACLPolicy("asdfg")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestBrokenHuJson(c *check.C) {
- err := h.LoadACLPolicy("./tests/acls/broken.hujson")
+ err := app.LoadACLPolicy("./tests/acls/broken.hujson")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
- err := h.LoadACLPolicy("./tests/acls/invalid.hujson")
+ err := app.LoadACLPolicy("./tests/acls/invalid.hujson")
c.Assert(err, check.NotNil)
- c.Assert(err, check.Equals, errorEmptyPolicy)
+ c.Assert(err, check.Equals, errEmptyPolicy)
}
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)
+ var hosts Hosts
+ err := hosts.UnmarshalJSON(
+ []byte(
+ `{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`,
+ ),
+ )
+ c.Assert(hosts, 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)
+ var hosts Hosts
+ err := hosts.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`))
+ c.Assert(hosts, check.IsNil)
c.Assert(err, check.NotNil)
}
func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
- err := h.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson")
+ err := app.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")
+ err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson")
c.Assert(err, check.IsNil)
- rules, err := h.generateACLRules()
+ rules, err := app.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")
+ err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
c.Assert(err, check.IsNil)
- rules, err := h.generateACLRules()
+ rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
@@ -63,10 +67,10 @@ func (s *Suite) TestPortRange(c *check.C) {
}
func (s *Suite) TestPortWildcard(c *check.C) {
- err := h.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
+ err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
c.Assert(err, check.IsNil)
- rules, err := h.generateACLRules()
+ rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
@@ -79,33 +83,35 @@ func (s *Suite) TestPortWildcard(c *check.C) {
}
func (s *Suite) TestPortNamespace(c *check.C) {
- n, err := h.CreateNamespace("testnamespace")
+ namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("testnamespace", "testmachine")
+ _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil)
- ip, _ := h.getAvailableIP()
- m := Machine{
+ ip, _ := app.getAvailableIP()
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: ip.String(),
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_namespace_as_user.hujson")
+ err = app.LoadACLPolicy(
+ "./tests/acls/acl_policy_basic_namespace_as_user.hujson",
+ )
c.Assert(err, check.IsNil)
- rules, err := h.generateACLRules()
+ rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
@@ -119,33 +125,33 @@ func (s *Suite) TestPortNamespace(c *check.C) {
}
func (s *Suite) TestPortGroup(c *check.C) {
- n, err := h.CreateNamespace("testnamespace")
+ namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("testnamespace", "testmachine")
+ _, err = app.GetMachine("testnamespace", "testmachine")
c.Assert(err, check.NotNil)
- ip, _ := h.getAvailableIP()
- m := Machine{
+ ip, _ := app.getAvailableIP()
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: ip.String(),
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- err = h.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
+ err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson")
c.Assert(err, check.IsNil)
- rules, err := h.generateACLRules()
+ rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
diff --git a/acls_types.go b/acls_types.go
index 67b74e73..08e650ff 100644
--- a/acls_types.go
+++ b/acls_types.go
@@ -8,7 +8,7 @@ import (
"inet.af/netaddr"
)
-// ACLPolicy represents a Tailscale ACL Policy
+// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"Groups"`
Hosts Hosts `json:"Hosts"`
@@ -17,61 +17,63 @@ type ACLPolicy struct {
Tests []ACLTest `json:"Tests"`
}
-// ACL is a basic rule for the ACL Policy
+// 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
+// Groups references a series of alias in the ACL rules.
type Groups map[string][]string
-// Hosts are alias for IP addresses or subnets
+// 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
+// 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
+// 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)
+// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
+func (hosts *Hosts) UnmarshalJSON(data []byte) error {
+ newHosts := Hosts{}
+ hostIPPrefixMap := make(map[string]string)
ast, err := hujson.Parse(data)
if err != nil {
return err
}
ast.Standardize()
data = ast.Pack()
- err = json.Unmarshal(data, &hs)
+ err = json.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
- for k, v := range hs {
- if !strings.Contains(v, "/") {
- v = v + "/32"
+ for host, prefixStr := range hostIPPrefixMap {
+ if !strings.Contains(prefixStr, "/") {
+ prefixStr += "/32"
}
- prefix, err := netaddr.ParseIPPrefix(v)
+ prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil {
return err
}
- hosts[k] = prefix
+ newHosts[host] = prefix
}
- *h = hosts
+ *hosts = newHosts
+
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 {
+// IsZero is perhaps a bit naive here.
+func (policy ACLPolicy) IsZero() bool {
+ if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
return true
}
+
return false
}
diff --git a/api.go b/api.go
index 490ce258..85c28e3e 100644
--- a/api.go
+++ b/api.go
@@ -10,31 +10,37 @@ import (
"strings"
"time"
- "github.com/rs/zerolog/log"
-
"github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd"
+ "github.com/rs/zerolog/log"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
)
+const reservedResponseHeaderSize = 4
+
// KeyHandler provides the Headscale pub key
-// Listens in /key
-func (h *Headscale) KeyHandler(c *gin.Context) {
- c.Data(200, "text/plain; charset=utf-8", []byte(h.publicKey.HexString()))
+// Listens in /key.
+func (h *Headscale) KeyHandler(ctx *gin.Context) {
+ ctx.Data(
+ http.StatusOK,
+ "text/plain; charset=utf-8",
+ []byte(h.publicKey.HexString()),
+ )
}
// RegisterWebAPI shows a simple message in the browser to point to the CLI
-// Listens in /register
-func (h *Headscale) RegisterWebAPI(c *gin.Context) {
- mKeyStr := c.Query("key")
- if mKeyStr == "" {
- c.String(http.StatusBadRequest, "Wrong params")
+// Listens in /register.
+func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
+ machineKeyStr := ctx.Query("key")
+ if machineKeyStr == "" {
+ ctx.String(http.StatusBadRequest, "Wrong params")
+
return
}
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
+ ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
headscale
@@ -51,43 +57,45 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
- `, mKeyStr)))
+ `, machineKeyStr)))
}
// RegistrationHandler handles the actual registration process of a machine
-// Endpoint /machine/:id
-func (h *Headscale) RegistrationHandler(c *gin.Context) {
- body, _ := io.ReadAll(c.Request.Body)
- mKeyStr := c.Param("id")
- mKey, err := wgkey.ParseHex(mKeyStr)
+// Endpoint /machine/:id.
+func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
+ body, _ := io.ReadAll(ctx.Request.Body)
+ machineKeyStr := ctx.Param("id")
+ machineKey, err := wgkey.ParseHex(machineKeyStr)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot parse machine key")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
- c.String(http.StatusInternalServerError, "Sad!")
+ ctx.String(http.StatusInternalServerError, "Sad!")
+
return
}
req := tailcfg.RegisterRequest{}
- err = decode(body, &req, &mKey, h.privateKey)
+ err = decode(body, &req, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot decode message")
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
- c.String(http.StatusInternalServerError, "Very sad!")
+ ctx.String(http.StatusInternalServerError, "Very sad!")
+
return
}
now := time.Now().UTC()
- m, err := h.GetMachineByMachineKey(mKey.HexString())
+ machine, err := h.GetMachineByMachineKey(machineKey.HexString())
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
newMachine := Machine{
Expiry: &time.Time{},
- MachineKey: mKey.HexString(),
+ MachineKey: machineKey.HexString(),
Name: req.Hostinfo.Hostname,
}
if err := h.db.Create(&newMachine).Error; err != nil {
@@ -95,88 +103,96 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
Str("handler", "Registration").
Err(err).
Msg("Could not create row")
- machineRegistrations.WithLabelValues("unknown", "web", "error", m.Namespace.Name).Inc()
+ machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
+ Inc()
+
return
}
- m = &newMachine
+ machine = &newMachine
}
- if !m.Registered && req.Auth.AuthKey != "" {
- h.handleAuthKey(c, h.db, mKey, req, *m)
+ if !machine.Registered && req.Auth.AuthKey != "" {
+ h.handleAuthKey(ctx, h.db, machineKey, req, *machine)
+
return
}
resp := tailcfg.RegisterResponse{}
// We have the updated key!
- if m.NodeKey == wgkey.Key(req.NodeKey).HexString() {
-
+ if machine.NodeKey == wgkey.Key(req.NodeKey).HexString() {
// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
log.Info().
Str("handler", "Registration").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Client requested logout")
- m.Expiry = &req.Expiry // save the expiry so that the machine is marked as expired
- h.db.Save(&m)
+ machine.Expiry = &req.Expiry // save the expiry so that the machine is marked as expired
+ h.db.Save(&machine)
resp.AuthURL = ""
resp.MachineAuthorized = false
- resp.User = *m.Namespace.toUser()
- respBody, err := encode(resp, &mKey, h.privateKey)
+ resp.User = *machine.Namespace.toUser()
+ respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
- c.String(http.StatusInternalServerError, "")
+ ctx.String(http.StatusInternalServerError, "")
+
return
}
- c.Data(200, "application/json; charset=utf-8", respBody)
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
+
return
}
- if m.Registered && m.Expiry.UTC().After(now) {
+ if machine.Registered && machine.Expiry.UTC().After(now) {
// The machine registration is valid, respond with redirect to /map
log.Debug().
Str("handler", "Registration").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Client is registered and we have the current NodeKey. All clear to /map")
resp.AuthURL = ""
resp.MachineAuthorized = true
- resp.User = *m.Namespace.toUser()
- resp.Login = *m.Namespace.toLogin()
+ resp.User = *machine.Namespace.toUser()
+ resp.Login = *machine.Namespace.toLogin()
- respBody, err := encode(resp, &mKey, h.privateKey)
+ respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
- machineRegistrations.WithLabelValues("update", "web", "error", m.Namespace.Name).Inc()
- c.String(http.StatusInternalServerError, "")
+ machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
+ Inc()
+ ctx.String(http.StatusInternalServerError, "")
+
return
}
- machineRegistrations.WithLabelValues("update", "web", "success", m.Namespace.Name).Inc()
- c.Data(200, "application/json; charset=utf-8", respBody)
+ machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
+ Inc()
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
+
return
}
// The client has registered before, but has expired
log.Debug().
Str("handler", "Registration").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Machine registration has expired. Sending a authurl to register")
if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
- strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString())
+ strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.HexString())
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
- strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString())
+ strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.HexString())
}
// When a client connects, it may request a specific expiry time in its
@@ -185,102 +201,120 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
// into two steps (which cant pass arbitrary data between them easily) and needs to be
// retrieved again after the user has authenticated. After the authentication flow
// completes, RequestedExpiry is copied into Expiry.
- m.RequestedExpiry = &req.Expiry
+ machine.RequestedExpiry = &req.Expiry
- h.db.Save(&m)
+ h.db.Save(&machine)
- respBody, err := encode(resp, &mKey, h.privateKey)
+ respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
- machineRegistrations.WithLabelValues("new", "web", "error", m.Namespace.Name).Inc()
- c.String(http.StatusInternalServerError, "")
+ machineRegistrations.WithLabelValues("new", "web", "error", machine.Namespace.Name).
+ Inc()
+ ctx.String(http.StatusInternalServerError, "")
+
return
}
- machineRegistrations.WithLabelValues("new", "web", "success", m.Namespace.Name).Inc()
- c.Data(200, "application/json; charset=utf-8", respBody)
+ machineRegistrations.WithLabelValues("new", "web", "success", machine.Namespace.Name).
+ Inc()
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
+
return
}
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
- if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() && m.Expiry.UTC().After(now) {
+ if machine.NodeKey == wgkey.Key(req.OldNodeKey).HexString() &&
+ machine.Expiry.UTC().After(now) {
log.Debug().
Str("handler", "Registration").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("We have the OldNodeKey in the database. This is a key refresh")
- m.NodeKey = wgkey.Key(req.NodeKey).HexString()
- h.db.Save(&m)
+ machine.NodeKey = wgkey.Key(req.NodeKey).HexString()
+ h.db.Save(&machine)
resp.AuthURL = ""
- resp.User = *m.Namespace.toUser()
- respBody, err := encode(resp, &mKey, h.privateKey)
+ resp.User = *machine.Namespace.toUser()
+ respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
- c.String(http.StatusInternalServerError, "Extremely sad!")
+ ctx.String(http.StatusInternalServerError, "Extremely sad!")
+
return
}
- c.Data(200, "application/json; charset=utf-8", respBody)
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
+
return
}
// The machine registration is new, redirect the client to the registration URL
log.Debug().
Str("handler", "Registration").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("The node is sending us a new NodeKey, sending auth url")
if h.cfg.OIDC.Issuer != "" {
- resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString())
+ resp.AuthURL = fmt.Sprintf(
+ "%s/oidc/register/%s",
+ strings.TrimSuffix(h.cfg.ServerURL, "/"),
+ machineKey.HexString(),
+ )
} else {
resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
- strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString())
+ strings.TrimSuffix(h.cfg.ServerURL, "/"), machineKey.HexString())
}
// save the requested expiry time for retrieval later in the authentication flow
- m.RequestedExpiry = &req.Expiry
- m.NodeKey = wgkey.Key(req.NodeKey).HexString() // save the NodeKey
- h.db.Save(&m)
+ machine.RequestedExpiry = &req.Expiry
+ machine.NodeKey = wgkey.Key(req.NodeKey).HexString() // save the NodeKey
+ h.db.Save(&machine)
- respBody, err := encode(resp, &mKey, h.privateKey)
+ respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil {
log.Error().
Str("handler", "Registration").
Err(err).
Msg("Cannot encode message")
- c.String(http.StatusInternalServerError, "")
+ ctx.String(http.StatusInternalServerError, "")
+
return
}
- c.Data(200, "application/json; charset=utf-8", respBody)
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
}
-func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) {
+func (h *Headscale) getMapResponse(
+ machineKey wgkey.Key,
+ req tailcfg.MapRequest,
+ machine *Machine,
+) ([]byte, error) {
log.Trace().
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
Msg("Creating Map response")
- node, err := m.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
+ node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot convert to node")
+
return nil, err
}
- peers, err := h.getPeers(m)
+ peers, err := h.getPeers(machine)
if err != nil {
log.Error().
Str("func", "getMapResponse").
Err(err).
Msg("Cannot fetch peers")
+
return nil, err
}
- profiles := getMapResponseUserProfiles(*m, peers)
+ profiles := getMapResponseUserProfiles(*machine, peers)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
@@ -288,17 +322,16 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
Str("func", "getMapResponse").
Err(err).
Msg("Failed to convert peers to Tailscale nodes")
+
return nil, err
}
- dnsConfig, err := getMapResponseDNSConfig(h.cfg.DNSConfig, h.cfg.BaseDomain, *m, peers)
- if err != nil {
- log.Error().
- Str("func", "getMapResponse").
- Err(err).
- Msg("Failed generate the DNSConfig")
- return nil, err
- }
+ dnsConfig := getMapResponseDNSConfig(
+ h.cfg.DNSConfig,
+ h.cfg.BaseDomain,
+ *machine,
+ peers,
+ )
resp := tailcfg.MapResponse{
KeepAlive: false,
@@ -323,66 +356,71 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
- respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey)
+ respBody, err = encodeMsg(srcCompressed, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
} else {
- respBody, err = encode(resp, &mKey, h.privateKey)
+ respBody, err = encode(resp, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
// declare the incoming size on the first 4 bytes
- data := make([]byte, 4)
+ data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
+
return data, nil
}
-func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Machine) ([]byte, error) {
- resp := tailcfg.MapResponse{
+func (h *Headscale) getMapKeepAliveResponse(
+ machineKey wgkey.Key,
+ mapRequest tailcfg.MapRequest,
+) ([]byte, error) {
+ mapResponse := tailcfg.MapResponse{
KeepAlive: true,
}
var respBody []byte
var err error
- if req.Compress == "zstd" {
- src, _ := json.Marshal(resp)
+ if mapRequest.Compress == "zstd" {
+ src, _ := json.Marshal(mapResponse)
encoder, _ := zstd.NewWriter(nil)
srcCompressed := encoder.EncodeAll(src, nil)
- respBody, err = encodeMsg(srcCompressed, &mKey, h.privateKey)
+ respBody, err = encodeMsg(srcCompressed, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
} else {
- respBody, err = encode(resp, &mKey, h.privateKey)
+ respBody, err = encode(mapResponse, &machineKey, h.privateKey)
if err != nil {
return nil, err
}
}
- data := make([]byte, 4)
+ data := make([]byte, reservedResponseHeaderSize)
binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
data = append(data, respBody...)
+
return data, nil
}
func (h *Headscale) handleAuthKey(
- c *gin.Context,
+ ctx *gin.Context,
db *gorm.DB,
idKey wgkey.Key,
- req tailcfg.RegisterRequest,
- m Machine,
+ reqisterRequest tailcfg.RegisterRequest,
+ machine Machine,
) {
log.Debug().
Str("func", "handleAuthKey").
- Str("machine", req.Hostinfo.Hostname).
- Msgf("Processing auth key for %s", req.Hostinfo.Hostname)
+ Str("machine", reqisterRequest.Hostinfo.Hostname).
+ Msgf("Processing auth key for %s", reqisterRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{}
- pak, err := h.checkKeyValidity(req.Auth.AuthKey)
+ pak, err := h.checkKeyValidity(reqisterRequest.Auth.AuthKey)
if err != nil {
log.Error().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
@@ -390,48 +428,56 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Err(err).
Msg("Cannot encode message")
- c.String(http.StatusInternalServerError, "")
- machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc()
+ ctx.String(http.StatusInternalServerError, "")
+ machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
+ Inc()
+
return
}
- c.Data(401, "application/json; charset=utf-8", respBody)
+ ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
log.Error().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Failed authentication via AuthKey")
- machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc()
+ machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
+ Inc()
+
return
}
log.Debug().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Authentication key was valid, proceeding to acquire an IP address")
ip, err := h.getAvailableIP()
if err != nil {
log.Error().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Failed to find an available IP")
- machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc()
+ machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
+ Inc()
+
return
}
log.Info().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("ip", ip.String()).
- Msgf("Assigning %s to %s", ip, m.Name)
+ Msgf("Assigning %s to %s", ip, machine.Name)
- m.AuthKeyID = uint(pak.ID)
- m.IPAddress = ip.String()
- m.NamespaceID = pak.NamespaceID
- m.NodeKey = wgkey.Key(req.NodeKey).HexString() // we update it just in case
- m.Registered = true
- m.RegisterMethod = "authKey"
- db.Save(&m)
+ machine.AuthKeyID = uint(pak.ID)
+ machine.IPAddress = ip.String()
+ machine.NamespaceID = pak.NamespaceID
+ machine.NodeKey = wgkey.Key(reqisterRequest.NodeKey).
+ HexString()
+ // we update it just in case
+ machine.Registered = true
+ machine.RegisterMethod = "authKey"
+ db.Save(&machine)
pak.Used = true
db.Save(&pak)
@@ -442,18 +488,21 @@ func (h *Headscale) handleAuthKey(
if err != nil {
log.Error().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Err(err).
Msg("Cannot encode message")
- machineRegistrations.WithLabelValues("new", "authkey", "error", m.Namespace.Name).Inc()
- c.String(http.StatusInternalServerError, "Extremely sad!")
+ machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
+ Inc()
+ ctx.String(http.StatusInternalServerError, "Extremely sad!")
+
return
}
- machineRegistrations.WithLabelValues("new", "authkey", "success", m.Namespace.Name).Inc()
- c.Data(200, "application/json; charset=utf-8", respBody)
+ machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name).
+ Inc()
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
log.Info().
Str("func", "handleAuthKey").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("ip", ip.String()).
Msg("Successfully authenticated via AuthKey")
}
diff --git a/app.go b/app.go
index c2260627..08b67fe0 100644
--- a/app.go
+++ b/app.go
@@ -18,20 +18,19 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
- "github.com/patrickmn/go-cache"
- "golang.org/x/oauth2"
-
"github.com/gin-gonic/gin"
- "github.com/grpc-ecosystem/go-grpc-middleware"
+ grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
- "github.com/philip-bui/grpc-zerolog"
+ "github.com/patrickmn/go-cache"
+ zerolog "github.com/philip-bui/grpc-zerolog"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/soheilhy/cmux"
ginprometheus "github.com/zsais/go-gin-prometheus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
+ "golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -48,7 +47,16 @@ import (
)
const (
- AUTH_PREFIX = "Bearer "
+ AuthPrefix = "Bearer "
+ Postgres = "postgresql"
+ Sqlite = "sqlite3"
+ updateInterval = 5000
+ HTTPReadTimeout = 30 * time.Second
+
+ errUnsupportedDatabase = Error("unsupported DB")
+ errUnsupportedLetsEncryptChallengeType = Error(
+ "unknown value for Lets Encrypt challenge type",
+ )
)
// Config contains the initial Headscale configuration.
@@ -151,16 +159,22 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
var dbString string
switch cfg.DBtype {
- case "postgres":
- dbString = fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable", cfg.DBhost,
- cfg.DBport, cfg.DBname, cfg.DBuser, cfg.DBpass)
- case "sqlite3":
+ case Postgres:
+ dbString = fmt.Sprintf(
+ "host=%s port=%d dbname=%s user=%s password=%s sslmode=disable",
+ cfg.DBhost,
+ cfg.DBport,
+ cfg.DBname,
+ cfg.DBuser,
+ cfg.DBpass,
+ )
+ case Sqlite:
dbString = cfg.DBpath
default:
- return nil, errors.New("unsupported DB")
+ return nil, errUnsupportedDatabase
}
- h := Headscale{
+ app := Headscale{
cfg: cfg,
dbType: cfg.DBtype,
dbString: dbString,
@@ -169,33 +183,32 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
aclRules: tailcfg.FilterAllowAll, // default allowall
}
- err = h.initDB()
+ err = app.initDB()
if err != nil {
return nil, err
}
if cfg.OIDC.Issuer != "" {
- err = h.initOIDC()
+ err = app.initOIDC()
if err != nil {
return nil, err
}
}
- if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS
- magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain)
- if err != nil {
- return nil, err
- }
+ if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
+ magicDNSDomains := generateMagicDNSRootDomains(
+ app.cfg.IPPrefix,
+ )
// we might have routes already from Split DNS
- if h.cfg.DNSConfig.Routes == nil {
- h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
+ if app.cfg.DNSConfig.Routes == nil {
+ app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
}
for _, d := range magicDNSDomains {
- h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
+ app.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
}
}
- return &h, nil
+ return &app, nil
}
// Redirect to our TLS url.
@@ -221,30 +234,37 @@ func (h *Headscale) expireEphemeralNodesWorker() {
return
}
- for _, ns := range namespaces {
- machines, err := h.ListMachinesInNamespace(ns.Name)
+ for _, namespace := range namespaces {
+ machines, err := h.ListMachinesInNamespace(namespace.Name)
if err != nil {
- log.Error().Err(err).Str("namespace", ns.Name).Msg("Error listing machines in namespace")
+ log.Error().
+ Err(err).
+ Str("namespace", namespace.Name).
+ Msg("Error listing machines in namespace")
return
}
- for _, m := range machines {
- if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral &&
- time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
- log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database")
+ for _, machine := range machines {
+ if machine.AuthKey != nil && machine.LastSeen != nil &&
+ machine.AuthKey.Ephemeral &&
+ time.Now().
+ After(machine.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) {
+ log.Info().
+ Str("machine", machine.Name).
+ Msg("Ephemeral client removed from database")
- err = h.db.Unscoped().Delete(m).Error
+ err = h.db.Unscoped().Delete(machine).Error
if err != nil {
log.Error().
Err(err).
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("🤮 Cannot delete ephemeral machine from the database")
}
}
}
- h.setLastStateChangeToNow(ns.Name)
+ h.setLastStateChangeToNow(namespace.Name)
}
}
@@ -266,36 +286,56 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
-
// Check if the request is coming from the on-server client.
// This is not secure, but it is to maintain maintainability
// with the "legacy" database-based client
// It is also neede for grpc-gateway to be able to connect to
// the server
- p, _ := peer.FromContext(ctx)
+ client, _ := peer.FromContext(ctx)
- log.Trace().Caller().Str("client_address", p.Addr.String()).Msg("Client is trying to authenticate")
+ log.Trace().
+ Caller().
+ Str("client_address", client.Addr.String()).
+ Msg("Client is trying to authenticate")
- md, ok := metadata.FromIncomingContext(ctx)
+ meta, ok := metadata.FromIncomingContext(ctx)
if !ok {
- log.Error().Caller().Str("client_address", p.Addr.String()).Msg("Retrieving metadata is failed")
- return ctx, status.Errorf(codes.InvalidArgument, "Retrieving metadata is failed")
+ log.Error().
+ Caller().
+ Str("client_address", client.Addr.String()).
+ Msg("Retrieving metadata is failed")
+
+ return ctx, status.Errorf(
+ codes.InvalidArgument,
+ "Retrieving metadata is failed",
+ )
}
- authHeader, ok := md["authorization"]
+ authHeader, ok := meta["authorization"]
if !ok {
- log.Error().Caller().Str("client_address", p.Addr.String()).Msg("Authorization token is not supplied")
- return ctx, status.Errorf(codes.Unauthenticated, "Authorization token is not supplied")
+ log.Error().
+ Caller().
+ Str("client_address", client.Addr.String()).
+ Msg("Authorization token is not supplied")
+
+ return ctx, status.Errorf(
+ codes.Unauthenticated,
+ "Authorization token is not supplied",
+ )
}
token := authHeader[0]
- if !strings.HasPrefix(token, AUTH_PREFIX) {
+ if !strings.HasPrefix(token, AuthPrefix) {
log.Error().
Caller().
- Str("client_address", p.Addr.String()).
+ Str("client_address", client.Addr.String()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
- return ctx, status.Error(codes.Unauthenticated, `missing "Bearer " prefix in "Authorization" header`)
+
+ return ctx, status.Error(
+ codes.Unauthenticated,
+ `missing "Bearer " prefix in "Authorization" header`,
+ )
}
// TODO(kradalby): Implement API key backend:
@@ -307,35 +347,38 @@ func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
// Currently all other than localhost traffic is unauthorized, this is intentional to allow
// us to make use of gRPC for our CLI, but not having to implement any of the remote capabilities
// and API key auth
- return ctx, status.Error(codes.Unauthenticated, "Authentication is not implemented yet")
+ return ctx, status.Error(
+ codes.Unauthenticated,
+ "Authentication is not implemented yet",
+ )
- //if strings.TrimPrefix(token, AUTH_PREFIX) != a.Token {
- // log.Error().Caller().Str("client_address", p.Addr.String()).Msg("invalid token")
- // return ctx, status.Error(codes.Unauthenticated, "invalid token")
- //}
+ // if strings.TrimPrefix(token, AUTH_PREFIX) != a.Token {
+ // log.Error().Caller().Str("client_address", p.Addr.String()).Msg("invalid token")
+ // return ctx, status.Error(codes.Unauthenticated, "invalid token")
+ // }
// return handler(ctx, req)
}
-func (h *Headscale) httpAuthenticationMiddleware(c *gin.Context) {
+func (h *Headscale) httpAuthenticationMiddleware(ctx *gin.Context) {
log.Trace().
Caller().
- Str("client_address", c.ClientIP()).
+ Str("client_address", ctx.ClientIP()).
Msg("HTTP authentication invoked")
- authHeader := c.GetHeader("authorization")
+ authHeader := ctx.GetHeader("authorization")
- if !strings.HasPrefix(authHeader, AUTH_PREFIX) {
+ if !strings.HasPrefix(authHeader, AuthPrefix) {
log.Error().
Caller().
- Str("client_address", c.ClientIP()).
+ Str("client_address", ctx.ClientIP()).
Msg(`missing "Bearer " prefix in "Authorization" header`)
- c.AbortWithStatus(http.StatusUnauthorized)
+ ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
- c.AbortWithStatus(http.StatusUnauthorized)
+ ctx.AbortWithStatus(http.StatusUnauthorized)
// TODO(kradalby): Implement API key backend
// Currently all traffic is unauthorized, this is intentional to allow
@@ -359,6 +402,7 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
if _, err := os.Stat(h.cfg.UnixSocket); errors.Is(err, os.ErrNotExist) {
return nil
}
+
return os.Remove(h.cfg.UnixSocket)
}
@@ -401,14 +445,17 @@ func (h *Headscale) Serve() error {
// Create the cmux object that will multiplex 2 protocols on the same port.
// The two following listeners will be served on the same port below gracefully.
- m := cmux.New(networkListener)
+ networkMutex := cmux.New(networkListener)
// Match gRPC requests here
- grpcListener := m.MatchWithWriters(
+ grpcListener := networkMutex.MatchWithWriters(
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
- cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc+proto"),
+ cmux.HTTP2MatchHeaderFieldSendSettings(
+ "content-type",
+ "application/grpc+proto",
+ ),
)
// Otherwise match regular http requests.
- httpListener := m.Match(cmux.Any())
+ httpListener := networkMutex.Match(cmux.Any())
grpcGatewayMux := runtime.NewServeMux()
@@ -431,30 +478,33 @@ func (h *Headscale) Serve() error {
return err
}
- r := gin.Default()
+ router := gin.Default()
- p := ginprometheus.NewPrometheus("gin")
- p.Use(r)
+ prometheus := ginprometheus.NewPrometheus("gin")
+ prometheus.Use(router)
- r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) })
- r.GET("/key", h.KeyHandler)
- r.GET("/register", h.RegisterWebAPI)
- r.POST("/machine/:id/map", h.PollNetMapHandler)
- r.POST("/machine/:id", h.RegistrationHandler)
- r.GET("/oidc/register/:mkey", h.RegisterOIDC)
- r.GET("/oidc/callback", h.OIDCCallback)
- r.GET("/apple", h.AppleMobileConfig)
- r.GET("/apple/:platform", h.ApplePlatformConfig)
- r.GET("/swagger", SwaggerUI)
- r.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
+ router.GET(
+ "/health",
+ func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": "ok"}) },
+ )
+ router.GET("/key", h.KeyHandler)
+ router.GET("/register", h.RegisterWebAPI)
+ router.POST("/machine/:id/map", h.PollNetMapHandler)
+ router.POST("/machine/:id", h.RegistrationHandler)
+ router.GET("/oidc/register/:mkey", h.RegisterOIDC)
+ router.GET("/oidc/callback", h.OIDCCallback)
+ router.GET("/apple", h.AppleMobileConfig)
+ router.GET("/apple/:platform", h.ApplePlatformConfig)
+ router.GET("/swagger", SwaggerUI)
+ router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
- api := r.Group("/api")
+ api := router.Group("/api")
api.Use(h.httpAuthenticationMiddleware)
{
api.Any("/v1/*any", gin.WrapF(grpcGatewayMux.ServeHTTP))
}
- r.NoRoute(stdoutHandler)
+ router.NoRoute(stdoutHandler)
// Fetch an initial DERP Map before we start serving
h.DERPMap = GetDERPMap(h.cfg.DERP)
@@ -466,14 +516,13 @@ func (h *Headscale) Serve() error {
}
// I HATE THIS
- updateMillisecondsWait := int64(5000)
- go h.watchForKVUpdates(updateMillisecondsWait)
- go h.expireEphemeralNodes(updateMillisecondsWait)
+ go h.watchForKVUpdates(updateInterval)
+ go h.expireEphemeralNodes(updateInterval)
httpServer := &http.Server{
Addr: h.cfg.Addr,
- Handler: r,
- ReadTimeout: 30 * time.Second,
+ Handler: router,
+ ReadTimeout: HTTPReadTimeout,
// Go does not handle timeouts in HTTP very well, and there is
// no good way to handle streaming timeouts, therefore we need to
// keep this at unlimited and be careful to clean up connections
@@ -519,36 +568,40 @@ func (h *Headscale) Serve() error {
reflection.Register(grpcServer)
reflection.Register(grpcSocket)
- g := new(errgroup.Group)
+ errorGroup := new(errgroup.Group)
- g.Go(func() error { return grpcSocket.Serve(socketListener) })
+ errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
// TODO(kradalby): Verify if we need the same TLS setup for gRPC as HTTP
- g.Go(func() error { return grpcServer.Serve(grpcListener) })
+ errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
if tlsConfig != nil {
- g.Go(func() error {
+ errorGroup.Go(func() error {
tlsl := tls.NewListener(httpListener, tlsConfig)
+
return httpServer.Serve(tlsl)
})
} else {
- g.Go(func() error { return httpServer.Serve(httpListener) })
+ errorGroup.Go(func() error { return httpServer.Serve(httpListener) })
}
- g.Go(func() error { return m.Serve() })
+ errorGroup.Go(func() error { return networkMutex.Serve() })
- log.Info().Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr)
+ log.Info().
+ Msgf("listening and serving (multiplexed HTTP and gRPC) on: %s", h.cfg.Addr)
- return g.Wait()
+ return errorGroup.Wait()
}
func (h *Headscale) getTLSSettings() (*tls.Config, error) {
+ var err error
if h.cfg.TLSLetsEncryptHostname != "" {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
- log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
+ log.Warn().
+ Msg("Listening with TLS but ServerURL does not start with https://")
}
- m := autocert.Manager{
+ certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(h.cfg.TLSLetsEncryptHostname),
Cache: autocert.DirCache(h.cfg.TLSLetsEncryptCacheDir),
@@ -558,40 +611,44 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
Email: h.cfg.ACMEEmail,
}
- if h.cfg.TLSLetsEncryptChallengeType == "TLS-ALPN-01" {
+ switch h.cfg.TLSLetsEncryptChallengeType {
+ case "TLS-ALPN-01":
// Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737)
// The RFC requires that the validation is done on port 443; in other words, headscale
// must be reachable on port 443.
- return m.TLSConfig(), nil
- } else if h.cfg.TLSLetsEncryptChallengeType == "HTTP-01" {
+ return certManager.TLSConfig(), nil
+
+ case "HTTP-01":
// Configuration via autocert with HTTP-01. This requires listening on
// port 80 for the certificate validation in addition to the headscale
// service, which can be configured to run on any other port.
go func() {
log.Fatal().
- Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, m.HTTPHandler(http.HandlerFunc(h.redirect)))).
+ Err(http.ListenAndServe(h.cfg.TLSLetsEncryptListen, certManager.HTTPHandler(http.HandlerFunc(h.redirect)))).
Msg("failed to set up a HTTP server")
}()
- return m.TLSConfig(), nil
- } else {
- return nil, errors.New("unknown value for TLSLetsEncryptChallengeType")
+ return certManager.TLSConfig(), nil
+
+ default:
+ return nil, errUnsupportedLetsEncryptChallengeType
}
} else if h.cfg.TLSCertPath == "" {
if !strings.HasPrefix(h.cfg.ServerURL, "http://") {
log.Warn().Msg("Listening without TLS but ServerURL does not start with http://")
}
- return nil, nil
+ return nil, err
} else {
if !strings.HasPrefix(h.cfg.ServerURL, "https://") {
log.Warn().Msg("Listening with TLS but ServerURL does not start with https://")
}
- var err error
- tlsConfig := &tls.Config{}
- tlsConfig.ClientAuth = tls.RequireAnyClientCert
- tlsConfig.NextProtos = []string{"http/1.1"}
- tlsConfig.Certificates = make([]tls.Certificate, 1)
+ tlsConfig := &tls.Config{
+ ClientAuth: tls.RequireAnyClientCert,
+ NextProtos: []string{"http/1.1"},
+ Certificates: make([]tls.Certificate, 1),
+ MinVersion: tls.VersionTLS12,
+ }
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLSCertPath, h.cfg.TLSKeyPath)
return tlsConfig, err
@@ -628,13 +685,13 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
}
}
-func stdoutHandler(c *gin.Context) {
- b, _ := io.ReadAll(c.Request.Body)
+func stdoutHandler(ctx *gin.Context) {
+ body, _ := io.ReadAll(ctx.Request.Body)
log.Trace().
- Interface("header", c.Request.Header).
- Interface("proto", c.Request.Proto).
- Interface("url", c.Request.URL).
- Bytes("body", b).
+ Interface("header", ctx.Request.Header).
+ Interface("proto", ctx.Request.Proto).
+ Interface("url", ctx.Request.URL).
+ Bytes("body", body).
Msg("Request did not match")
}
diff --git a/app_test.go b/app_test.go
index 5e53f1cc..947062bf 100644
--- a/app_test.go
+++ b/app_test.go
@@ -17,8 +17,10 @@ var _ = check.Suite(&Suite{})
type Suite struct{}
-var tmpDir string
-var h Headscale
+var (
+ tmpDir string
+ app Headscale
+)
func (s *Suite) SetUpTest(c *check.C) {
s.ResetDB(c)
@@ -41,18 +43,18 @@ func (s *Suite) ResetDB(c *check.C) {
IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"),
}
- h = Headscale{
+ app = Headscale{
cfg: cfg,
dbType: "sqlite3",
dbString: tmpDir + "/headscale_test.db",
}
- err = h.initDB()
+ err = app.initDB()
if err != nil {
c.Fatal(err)
}
- db, err := h.openDB()
+ db, err := app.openDB()
if err != nil {
c.Fatal(err)
}
- h.db = db
+ app.db = db
}
diff --git a/apple_mobileconfig.go b/apple_mobileconfig.go
index f3956e3c..2e454df8 100644
--- a/apple_mobileconfig.go
+++ b/apple_mobileconfig.go
@@ -5,16 +5,15 @@ import (
"net/http"
"text/template"
- "github.com/rs/zerolog/log"
-
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
+ "github.com/rs/zerolog/log"
)
// AppleMobileConfig shows a simple message in the browser to point to the CLI
-// Listens in /register
-func (h *Headscale) AppleMobileConfig(c *gin.Context) {
- t := template.Must(template.New("apple").Parse(`
+// Listens in /register.
+func (h *Headscale) AppleMobileConfig(ctx *gin.Context) {
+ appleTemplate := template.Must(template.New("apple").Parse(`
Apple configuration profiles
@@ -56,7 +55,7 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {
Or
Use your terminal to configure the default setting for Tailscale by issuing:
- defaults write io.tailscale.ipn.macos ControlURL {{.Url}}
+ defaults write io.tailscale.ipn.macos ControlURL {{.URL}}
Restart Tailscale.app and log in.
@@ -64,24 +63,29 @@ func (h *Headscale) AppleMobileConfig(c *gin.Context) {
`))
config := map[string]interface{}{
- "Url": h.cfg.ServerURL,
+ "URL": h.cfg.ServerURL,
}
var payload bytes.Buffer
- if err := t.Execute(&payload, config); err != nil {
+ if err := appleTemplate.Execute(&payload, config); err != nil {
log.Error().
Str("handler", "AppleMobileConfig").
Err(err).
Msg("Could not render Apple index template")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple index template"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Could not render Apple index template"),
+ )
+
return
}
- c.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
+ ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
}
-func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
- platform := c.Param("platform")
+func (h *Headscale) ApplePlatformConfig(ctx *gin.Context) {
+ platform := ctx.Param("platform")
id, err := uuid.NewV4()
if err != nil {
@@ -89,23 +93,33 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Failed not create UUID")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Failed to create UUID"),
+ )
+
return
}
- contentId, err := uuid.NewV4()
+ contentID, err := uuid.NewV4()
if err != nil {
log.Error().
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Failed not create UUID")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Failed to create UUID"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Failed to create UUID"),
+ )
+
return
}
platformConfig := AppleMobilePlatformConfig{
- UUID: contentId,
- Url: h.cfg.ServerURL,
+ UUID: contentID,
+ URL: h.cfg.ServerURL,
}
var payload bytes.Buffer
@@ -117,7 +131,12 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple macOS template")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple macOS template"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Could not render Apple macOS template"),
+ )
+
return
}
case "ios":
@@ -126,17 +145,27 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple iOS template")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple iOS template"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Could not render Apple iOS template"),
+ )
+
return
}
default:
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("Invalid platform, only ios and macos is supported"))
+ ctx.Data(
+ http.StatusOK,
+ "text/html; charset=utf-8",
+ []byte("Invalid platform, only ios and macos is supported"),
+ )
+
return
}
config := AppleMobileConfig{
UUID: id,
- Url: h.cfg.ServerURL,
+ URL: h.cfg.ServerURL,
Payload: payload.String(),
}
@@ -146,25 +175,35 @@ func (h *Headscale) ApplePlatformConfig(c *gin.Context) {
Str("handler", "ApplePlatformConfig").
Err(err).
Msg("Could not render Apple platform template")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Apple platform template"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Could not render Apple platform template"),
+ )
+
return
}
- c.Data(http.StatusOK, "application/x-apple-aspen-config; charset=utf-8", content.Bytes())
+ ctx.Data(
+ http.StatusOK,
+ "application/x-apple-aspen-config; charset=utf-8",
+ content.Bytes(),
+ )
}
type AppleMobileConfig struct {
UUID uuid.UUID
- Url string
+ URL string
Payload string
}
type AppleMobilePlatformConfig struct {
UUID uuid.UUID
- Url string
+ URL string
}
-var commonTemplate = template.Must(template.New("mobileconfig").Parse(`
+var commonTemplate = template.Must(
+ template.New("mobileconfig").Parse(`
@@ -173,7 +212,7 @@ var commonTemplate = template.Must(template.New("mobileconfig").Parse(`PayloadDisplayName
Headscale
PayloadDescription
- Configure Tailscale login server to: {{.Url}}
+ Configure Tailscale login server to: {{.URL}}
PayloadIdentifier
com.github.juanfont.headscale
PayloadRemovalDisallowed
@@ -187,7 +226,8 @@ var commonTemplate = template.Must(template.New("mobileconfig").Parse(`
-`))
+`),
+)
var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
@@ -203,7 +243,7 @@ var iosTemplate = template.Must(template.New("iosTemplate").Parse(`
ControlURL
- {{.Url}}
+ {{.URL}}
`))
@@ -221,6 +261,6 @@ var macosTemplate = template.Must(template.New("macosTemplate").Parse(`
ControlURL
- {{.Url}}
+ {{.URL}}
`))
diff --git a/cli_test.go b/cli_test.go
index 291b5df1..44ef9f08 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -7,31 +7,34 @@ import (
)
func (s *Suite) TestRegisterMachine(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
now := time.Now().UTC()
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
IPAddress: "10.0.0.1",
Expiry: &now,
RequestedExpiry: &now,
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- _, err = h.GetMachine("test", "testmachine")
+ _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
- m2, err := h.RegisterMachine("8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", n.Name)
+ machineAfterRegistering, err := app.RegisterMachine(
+ "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
+ namespace.Name,
+ )
c.Assert(err, check.IsNil)
- c.Assert(m2.Registered, check.Equals, true)
+ c.Assert(machineAfterRegistering.Registered, check.Equals, true)
- _, err = m2.GetHostInfo()
+ _, err = machineAfterRegistering.GetHostInfo()
c.Assert(err, check.IsNil)
}
diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go
index e140156b..46bdb9e8 100644
--- a/cmd/headscale/cli/debug.go
+++ b/cmd/headscale/cli/debug.go
@@ -27,7 +27,8 @@ func init() {
if err != nil {
log.Fatal().Err(err).Msg("")
}
- createNodeCmd.Flags().StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to advertise")
+ createNodeCmd.Flags().
+ StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to advertise")
debugCmd.AddCommand(createNodeCmd)
}
@@ -47,6 +48,7 @@ var createNodeCmd = &cobra.Command{
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -56,19 +58,34 @@ var createNodeCmd = &cobra.Command{
name, err := cmd.Flags().GetString("name")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting node from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting node from flag: %s", err),
+ output,
+ )
+
return
}
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting key from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting key from flag: %s", err),
+ output,
+ )
+
return
}
routes, err := cmd.Flags().GetStringSlice("route")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting routes from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting routes from flag: %s", err),
+ output,
+ )
+
return
}
@@ -81,7 +98,12 @@ var createNodeCmd = &cobra.Command{
response, err := client.DebugCreateMachine(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot create machine: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot create machine: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
diff --git a/cmd/headscale/cli/namespaces.go b/cmd/headscale/cli/namespaces.go
index 0e5eeb4c..361e9be3 100644
--- a/cmd/headscale/cli/namespaces.go
+++ b/cmd/headscale/cli/namespaces.go
@@ -4,6 +4,7 @@ import (
"fmt"
survey "github.com/AlecAivazis/survey/v2"
+ "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
@@ -19,6 +20,10 @@ func init() {
namespaceCmd.AddCommand(renameNamespaceCmd)
}
+const (
+ errMissingParameter = headscale.Error("missing parameters")
+)
+
var namespaceCmd = &cobra.Command{
Use: "namespaces",
Short: "Manage the namespaces of Headscale",
@@ -29,8 +34,9 @@ var createNamespaceCmd = &cobra.Command{
Short: "Creates a new namespace",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
- return fmt.Errorf("Missing parameters")
+ return errMissingParameter
}
+
return nil
},
Run: func(cmd *cobra.Command, args []string) {
@@ -49,7 +55,15 @@ var createNamespaceCmd = &cobra.Command{
log.Trace().Interface("request", request).Msg("Sending CreateNamespace request")
response, err := client.CreateNamespace(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot create namespace: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Cannot create namespace: %s",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
@@ -62,8 +76,9 @@ var destroyNamespaceCmd = &cobra.Command{
Short: "Destroys a namespace",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
- return fmt.Errorf("Missing parameters")
+ return errMissingParameter
}
+
return nil
},
Run: func(cmd *cobra.Command, args []string) {
@@ -81,7 +96,12 @@ var destroyNamespaceCmd = &cobra.Command{
_, err := client.GetNamespace(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
@@ -89,7 +109,10 @@ var destroyNamespaceCmd = &cobra.Command{
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
- Message: fmt.Sprintf("Do you want to remove the namespace '%s' and any associated preauthkeys?", namespaceName),
+ Message: fmt.Sprintf(
+ "Do you want to remove the namespace '%s' and any associated preauthkeys?",
+ namespaceName,
+ ),
}
err := survey.AskOne(prompt, &confirm)
if err != nil {
@@ -102,7 +125,15 @@ var destroyNamespaceCmd = &cobra.Command{
response, err := client.DeleteNamespace(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot destroy namespace: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Cannot destroy namespace: %s",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
SuccessOutput(response, "Namespace destroyed", output)
@@ -126,19 +157,25 @@ var listNamespacesCmd = &cobra.Command{
response, err := client.ListNamespaces(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot get namespaces: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
if output != "" {
SuccessOutput(response.Namespaces, "", output)
+
return
}
- d := pterm.TableData{{"ID", "Name", "Created"}}
+ tableData := pterm.TableData{{"ID", "Name", "Created"}}
for _, namespace := range response.GetNamespaces() {
- d = append(
- d,
+ tableData = append(
+ tableData,
[]string{
namespace.GetId(),
namespace.GetName(),
@@ -146,9 +183,14 @@ var listNamespacesCmd = &cobra.Command{
},
)
}
- err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
+ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to render pterm table: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to render pterm table: %s", err),
+ output,
+ )
+
return
}
},
@@ -158,9 +200,11 @@ var renameNamespaceCmd = &cobra.Command{
Use: "rename OLD_NAME NEW_NAME",
Short: "Renames a namespace",
Args: func(cmd *cobra.Command, args []string) error {
- if len(args) < 2 {
- return fmt.Errorf("Missing parameters")
+ expectedArguments := 2
+ if len(args) < expectedArguments {
+ return errMissingParameter
}
+
return nil
},
Run: func(cmd *cobra.Command, args []string) {
@@ -177,7 +221,15 @@ var renameNamespaceCmd = &cobra.Command{
response, err := client.RenameNamespace(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot rename namespace: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Cannot rename namespace: %s",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go
index 9b3f4243..218d25bd 100644
--- a/cmd/headscale/cli/nodes.go
+++ b/cmd/headscale/cli/nodes.go
@@ -7,6 +7,7 @@ import (
"time"
survey "github.com/AlecAivazis/survey/v2"
+ "github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
@@ -77,6 +78,7 @@ var registerNodeCmd = &cobra.Command{
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -86,7 +88,12 @@ var registerNodeCmd = &cobra.Command{
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting machine key from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting machine key from flag: %s", err),
+ output,
+ )
+
return
}
@@ -97,7 +104,15 @@ var registerNodeCmd = &cobra.Command{
response, err := client.RegisterMachine(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot register machine: %s\n", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Cannot register machine: %s\n",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
@@ -113,6 +128,7 @@ var listNodesCmd = &cobra.Command{
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -126,24 +142,36 @@ var listNodesCmd = &cobra.Command{
response, err := client.ListMachines(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
if output != "" {
SuccessOutput(response.Machines, "", output)
+
return
}
- d, err := nodesToPtables(namespace, response.Machines)
+ tableData, err := nodesToPtables(namespace, response.Machines)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
+
return
}
- err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
+ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to render pterm table: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to render pterm table: %s", err),
+ output,
+ )
+
return
}
},
@@ -155,9 +183,14 @@ var deleteNodeCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
- id, err := cmd.Flags().GetInt("identifier")
+ identifier, err := cmd.Flags().GetInt("identifier")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error converting ID to integer: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error converting ID to integer: %s", err),
+ output,
+ )
+
return
}
@@ -166,24 +199,35 @@ var deleteNodeCmd = &cobra.Command{
defer conn.Close()
getRequest := &v1.GetMachineRequest{
- MachineId: uint64(id),
+ MachineId: uint64(identifier),
}
getResponse, err := client.GetMachine(ctx, getRequest)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Error getting node node: %s",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
deleteRequest := &v1.DeleteMachineRequest{
- MachineId: uint64(id),
+ MachineId: uint64(identifier),
}
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
- Message: fmt.Sprintf("Do you want to remove the node %s?", getResponse.GetMachine().Name),
+ Message: fmt.Sprintf(
+ "Do you want to remove the node %s?",
+ getResponse.GetMachine().Name,
+ ),
}
err = survey.AskOne(prompt, &confirm)
if err != nil {
@@ -195,13 +239,26 @@ var deleteNodeCmd = &cobra.Command{
response, err := client.DeleteMachine(ctx, deleteRequest)
if output != "" {
SuccessOutput(response, "", output)
+
return
}
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error deleting node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Error deleting node: %s",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
- SuccessOutput(map[string]string{"Result": "Node deleted"}, "Node deleted", output)
+ SuccessOutput(
+ map[string]string{"Result": "Node deleted"},
+ "Node deleted",
+ output,
+ )
} else {
SuccessOutput(map[string]string{"Result": "Node not deleted"}, "Node not deleted", output)
}
@@ -210,12 +267,12 @@ var deleteNodeCmd = &cobra.Command{
func sharingWorker(
cmd *cobra.Command,
- args []string,
) (string, *v1.Machine, *v1.Namespace, error) {
output, _ := cmd.Flags().GetString("output")
namespaceStr, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return "", nil, nil, err
}
@@ -223,19 +280,25 @@ func sharingWorker(
defer cancel()
defer conn.Close()
- id, err := cmd.Flags().GetInt("identifier")
+ identifier, err := cmd.Flags().GetInt("identifier")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting ID to integer: %s", err), output)
+
return "", nil, nil, err
}
machineRequest := &v1.GetMachineRequest{
- MachineId: uint64(id),
+ MachineId: uint64(identifier),
}
machineResponse, err := client.GetMachine(ctx, machineRequest)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
+ output,
+ )
+
return "", nil, nil, err
}
@@ -245,7 +308,12 @@ func sharingWorker(
namespaceResponse, err := client.GetNamespace(ctx, namespaceRequest)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting node node: %s", status.Convert(err).Message()),
+ output,
+ )
+
return "", nil, nil, err
}
@@ -256,9 +324,14 @@ var shareMachineCmd = &cobra.Command{
Use: "share",
Short: "Shares a node from the current namespace to the specified one",
Run: func(cmd *cobra.Command, args []string) {
- output, machine, namespace, err := sharingWorker(cmd, args)
+ output, machine, namespace, err := sharingWorker(cmd)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to fetch namespace or machine: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
+ output,
+ )
+
return
}
@@ -273,7 +346,12 @@ var shareMachineCmd = &cobra.Command{
response, err := client.ShareMachine(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
@@ -285,9 +363,14 @@ var unshareMachineCmd = &cobra.Command{
Use: "unshare",
Short: "Unshares a node from the specified namespace",
Run: func(cmd *cobra.Command, args []string) {
- output, machine, namespace, err := sharingWorker(cmd, args)
+ output, machine, namespace, err := sharingWorker(cmd)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to fetch namespace or machine: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
+ output,
+ )
+
return
}
@@ -302,7 +385,12 @@ var unshareMachineCmd = &cobra.Command{
response, err := client.UnshareMachine(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
@@ -310,8 +398,22 @@ var unshareMachineCmd = &cobra.Command{
},
}
-func nodesToPtables(currentNamespace string, machines []*v1.Machine) (pterm.TableData, error) {
- d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}}
+func nodesToPtables(
+ currentNamespace string,
+ machines []*v1.Machine,
+) (pterm.TableData, error) {
+ tableData := pterm.TableData{
+ {
+ "ID",
+ "Name",
+ "NodeKey",
+ "Namespace",
+ "IP address",
+ "Ephemeral",
+ "Last seen",
+ "Online",
+ },
+ }
for _, machine := range machines {
var ephemeral bool
@@ -331,7 +433,9 @@ func nodesToPtables(currentNamespace string, machines []*v1.Machine) (pterm.Tabl
nodeKey := tailcfg.NodeKey(nKey)
var online string
- if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online
+ if lastSeen.After(
+ time.Now().Add(-5 * time.Minute),
+ ) { // TODO: Find a better way to reliably show if online
online = pterm.LightGreen("true")
} else {
online = pterm.LightRed("false")
@@ -344,10 +448,10 @@ func nodesToPtables(currentNamespace string, machines []*v1.Machine) (pterm.Tabl
// Shared into this namespace
namespace = pterm.LightYellow(machine.Namespace.Name)
}
- d = append(
- d,
+ tableData = append(
+ tableData,
[]string{
- strconv.FormatUint(machine.Id, 10),
+ strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
nodeKey.ShortString(),
namespace,
@@ -358,5 +462,6 @@ func nodesToPtables(currentNamespace string, machines []*v1.Machine) (pterm.Tabl
},
)
}
- return d, nil
+
+ return tableData, nil
}
diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go
index a07e9610..9d5c838f 100644
--- a/cmd/headscale/cli/preauthkeys.go
+++ b/cmd/headscale/cli/preauthkeys.go
@@ -12,6 +12,10 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
+const (
+ DefaultPreAuthKeyExpiry = 24 * time.Hour
+)
+
func init() {
rootCmd.AddCommand(preauthkeysCmd)
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "Namespace")
@@ -22,10 +26,12 @@ func init() {
preauthkeysCmd.AddCommand(listPreAuthKeys)
preauthkeysCmd.AddCommand(createPreAuthKeyCmd)
preauthkeysCmd.AddCommand(expirePreAuthKeyCmd)
- createPreAuthKeyCmd.PersistentFlags().Bool("reusable", false, "Make the preauthkey reusable")
- createPreAuthKeyCmd.PersistentFlags().Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
+ createPreAuthKeyCmd.PersistentFlags().
+ Bool("reusable", false, "Make the preauthkey reusable")
+ createPreAuthKeyCmd.PersistentFlags().
+ Bool("ephemeral", false, "Preauthkey for ephemeral nodes")
createPreAuthKeyCmd.Flags().
- DurationP("expiration", "e", 24*time.Hour, "Human-readable expiration of the key (30m, 24h, 365d...)")
+ DurationP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (30m, 24h, 365d...)")
}
var preauthkeysCmd = &cobra.Command{
@@ -39,9 +45,10 @@ var listPreAuthKeys = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
- n, err := cmd.Flags().GetString("namespace")
+ namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -50,48 +57,61 @@ var listPreAuthKeys = &cobra.Command{
defer conn.Close()
request := &v1.ListPreAuthKeysRequest{
- Namespace: n,
+ Namespace: namespace,
}
response, err := client.ListPreAuthKeys(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting the list of keys: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting the list of keys: %s", err),
+ output,
+ )
+
return
}
if output != "" {
SuccessOutput(response.PreAuthKeys, "", output)
+
return
}
- d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}}
- for _, k := range response.PreAuthKeys {
+ tableData := pterm.TableData{
+ {"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"},
+ }
+ for _, key := range response.PreAuthKeys {
expiration := "-"
- if k.GetExpiration() != nil {
- expiration = k.Expiration.AsTime().Format("2006-01-02 15:04:05")
+ if key.GetExpiration() != nil {
+ expiration = key.Expiration.AsTime().Format("2006-01-02 15:04:05")
}
var reusable string
- if k.GetEphemeral() {
+ if key.GetEphemeral() {
reusable = "N/A"
} else {
- reusable = fmt.Sprintf("%v", k.GetReusable())
+ reusable = fmt.Sprintf("%v", key.GetReusable())
}
- d = append(d, []string{
- k.GetId(),
- k.GetKey(),
+ tableData = append(tableData, []string{
+ key.GetId(),
+ key.GetKey(),
reusable,
- strconv.FormatBool(k.GetEphemeral()),
- strconv.FormatBool(k.GetUsed()),
+ strconv.FormatBool(key.GetEphemeral()),
+ strconv.FormatBool(key.GetUsed()),
expiration,
- k.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
+ key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"),
})
}
- err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
+ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to render pterm table: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to render pterm table: %s", err),
+ output,
+ )
+
return
}
},
@@ -106,6 +126,7 @@ var createPreAuthKeyCmd = &cobra.Command{
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -139,7 +160,12 @@ var createPreAuthKeyCmd = &cobra.Command{
response, err := client.CreatePreAuthKey(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err),
+ output,
+ )
+
return
}
@@ -152,8 +178,9 @@ var expirePreAuthKeyCmd = &cobra.Command{
Short: "Expire a preauthkey",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
- return fmt.Errorf("missing parameters")
+ return errMissingParameter
}
+
return nil
},
Run: func(cmd *cobra.Command, args []string) {
@@ -161,6 +188,7 @@ var expirePreAuthKeyCmd = &cobra.Command{
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
+
return
}
@@ -175,7 +203,12 @@ var expirePreAuthKeyCmd = &cobra.Command{
response, err := client.ExpirePreAuthKey(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err),
+ output,
+ )
+
return
}
diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go
index e5115f3d..99b15140 100644
--- a/cmd/headscale/cli/root.go
+++ b/cmd/headscale/cli/root.go
@@ -10,7 +10,8 @@ import (
func init() {
rootCmd.PersistentFlags().
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
- rootCmd.PersistentFlags().Bool("force", false, "Disable prompts and forces the execution")
+ rootCmd.PersistentFlags().
+ Bool("force", false, "Disable prompts and forces the execution")
}
var rootCmd = &cobra.Command{
diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go
index 9f2e4e24..ced1a0bf 100644
--- a/cmd/headscale/cli/routes.go
+++ b/cmd/headscale/cli/routes.go
@@ -21,7 +21,8 @@ func init() {
}
routesCmd.AddCommand(listRoutesCmd)
- enableRouteCmd.Flags().StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
+ enableRouteCmd.Flags().
+ StringSliceP("route", "r", []string{}, "List (or repeated flags) of routes to enable")
enableRouteCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = enableRouteCmd.MarkFlagRequired("identifier")
if err != nil {
@@ -44,9 +45,14 @@ var listRoutesCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
- machineId, err := cmd.Flags().GetUint64("identifier")
+ machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting machine id from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting machine id from flag: %s", err),
+ output,
+ )
+
return
}
@@ -55,29 +61,41 @@ var listRoutesCmd = &cobra.Command{
defer conn.Close()
request := &v1.GetMachineRouteRequest{
- MachineId: machineId,
+ MachineId: machineID,
}
response, err := client.GetMachineRoute(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
+ output,
+ )
+
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
+
return
}
- d := routesToPtables(response.Routes)
+ tableData := routesToPtables(response.Routes)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
+
return
}
- err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
+ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to render pterm table: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to render pterm table: %s", err),
+ output,
+ )
+
return
}
},
@@ -93,15 +111,26 @@ omit the route you do not want to enable.
`,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
- machineId, err := cmd.Flags().GetUint64("identifier")
+
+ machineID, err := cmd.Flags().GetUint64("identifier")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting machine id from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting machine id from flag: %s", err),
+ output,
+ )
+
return
}
routes, err := cmd.Flags().GetStringSlice("route")
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Error getting routes from flag: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Error getting routes from flag: %s", err),
+ output,
+ )
+
return
}
@@ -110,45 +139,61 @@ omit the route you do not want to enable.
defer conn.Close()
request := &v1.EnableMachineRoutesRequest{
- MachineId: machineId,
+ MachineId: machineID,
Routes: routes,
}
response, err := client.EnableMachineRoutes(ctx, request)
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Cannot register machine: %s\n", status.Convert(err).Message()), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf(
+ "Cannot register machine: %s\n",
+ status.Convert(err).Message(),
+ ),
+ output,
+ )
+
return
}
if output != "" {
SuccessOutput(response.Routes, "", output)
+
return
}
- d := routesToPtables(response.Routes)
+ tableData := routesToPtables(response.Routes)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
+
return
}
- err = pterm.DefaultTable.WithHasHeader().WithData(d).Render()
+ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
if err != nil {
- ErrorOutput(err, fmt.Sprintf("Failed to render pterm table: %s", err), output)
+ ErrorOutput(
+ err,
+ fmt.Sprintf("Failed to render pterm table: %s", err),
+ output,
+ )
+
return
}
},
}
-// routesToPtables converts the list of routes to a nice table
+// routesToPtables converts the list of routes to a nice table.
func routesToPtables(routes *v1.Routes) pterm.TableData {
- d := pterm.TableData{{"Route", "Enabled"}}
+ tableData := pterm.TableData{{"Route", "Enabled"}}
for _, route := range routes.GetAdvertisedRoutes() {
enabled := isStringInSlice(routes.EnabledRoutes, route)
- d = append(d, []string{route, strconv.FormatBool(enabled)})
+ tableData = append(tableData, []string{route, strconv.FormatBool(enabled)})
}
- return d
+
+ return tableData
}
func isStringInSlice(strs []string, s string) bool {
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index 30f1a789..958fb893 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -52,9 +52,8 @@ func LoadConfig(path string) error {
viper.SetDefault("cli.insecure", false)
viper.SetDefault("cli.timeout", "5s")
- err := viper.ReadInConfig()
- if err != nil {
- return fmt.Errorf("Fatal error reading config file: %s \n", err)
+ if err := viper.ReadInConfig(); err != nil {
+ return fmt.Errorf("fatal error reading config file: %w", err)
}
// Collect any validation errors and return them all at once
@@ -82,6 +81,7 @@ func LoadConfig(path string) error {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
}
if errorText != "" {
+ //nolint
return errors.New(strings.TrimSuffix(errorText, "\n"))
} else {
return nil
@@ -149,9 +149,14 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config.restricted_nameservers") {
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Routes = make(map[string][]dnstype.Resolver)
- restrictedDNS := viper.GetStringMapStringSlice("dns_config.restricted_nameservers")
+ restrictedDNS := viper.GetStringMapStringSlice(
+ "dns_config.restricted_nameservers",
+ )
for domain, restrictedNameservers := range restrictedDNS {
- restrictedResolvers := make([]dnstype.Resolver, len(restrictedNameservers))
+ restrictedResolvers := make(
+ []dnstype.Resolver,
+ len(restrictedNameservers),
+ )
for index, nameserverStr := range restrictedNameservers {
nameserver, err := netaddr.ParseIP(nameserverStr)
if err != nil {
@@ -208,6 +213,7 @@ func absPath(path string) string {
path = filepath.Join(dir, path)
}
}
+
return path
}
@@ -219,7 +225,9 @@ func getHeadscaleConfig() headscale.Config {
"10h",
) // use 10h here because it is the length of a standard business day plus a small amount of leeway
if viper.GetDuration("max_machine_registration_duration") >= time.Second {
- maxMachineRegistrationDuration = viper.GetDuration("max_machine_registration_duration")
+ maxMachineRegistrationDuration = viper.GetDuration(
+ "max_machine_registration_duration",
+ )
}
// defaultMachineRegistrationDuration is the default time assigned to a machine registration if one is not
@@ -229,7 +237,9 @@ func getHeadscaleConfig() headscale.Config {
"8h",
) // use 8h here because it's the length of a standard business day
if viper.GetDuration("default_machine_registration_duration") >= time.Second {
- defaultMachineRegistrationDuration = viper.GetDuration("default_machine_registration_duration")
+ defaultMachineRegistrationDuration = viper.GetDuration(
+ "default_machine_registration_duration",
+ )
}
dnsConfig, baseDomain := GetDNSConfig()
@@ -244,7 +254,9 @@ func getHeadscaleConfig() headscale.Config {
DERP: derpConfig,
- EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),
+ EphemeralNodeInactivityTimeout: viper.GetDuration(
+ "ephemeral_node_inactivity_timeout",
+ ),
DBtype: viper.GetString("db_type"),
DBpath: absPath(viper.GetString("db_path")),
@@ -254,9 +266,11 @@ func getHeadscaleConfig() headscale.Config {
DBuser: viper.GetString("db_user"),
DBpass: viper.GetString("db_pass"),
- TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
- TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"),
- TLSLetsEncryptCacheDir: absPath(viper.GetString("tls_letsencrypt_cache_dir")),
+ TLSLetsEncryptHostname: viper.GetString("tls_letsencrypt_hostname"),
+ TLSLetsEncryptListen: viper.GetString("tls_letsencrypt_listen"),
+ TLSLetsEncryptCacheDir: absPath(
+ viper.GetString("tls_letsencrypt_cache_dir"),
+ ),
TLSLetsEncryptChallengeType: viper.GetString("tls_letsencrypt_challenge_type"),
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
@@ -292,11 +306,14 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
// 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\n",
+ "ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s",
viper.GetString("ephemeral_node_inactivity_timeout"),
minInactivityTimeout,
)
+
return nil, err
}
@@ -304,7 +321,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
cfg.OIDC.MatchMap = loadOIDCMatchMap()
- h, err := headscale.NewHeadscale(cfg)
+ app, err := headscale.NewHeadscale(cfg)
if err != nil {
return nil, err
}
@@ -313,7 +330,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
if viper.GetString("acl_policy_path") != "" {
aclPath := absPath(viper.GetString("acl_policy_path"))
- err = h.LoadACLPolicy(aclPath)
+ err = app.LoadACLPolicy(aclPath)
if err != nil {
log.Error().
Str("path", aclPath).
@@ -322,7 +339,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
}
}
- return h, nil
+ return app, nil
}
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
@@ -342,7 +359,6 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.
// If the address is not set, we assume that we are on the server hosting headscale.
if address == "" {
-
log.Debug().
Str("socket", cfg.UnixSocket).
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
@@ -402,10 +418,13 @@ func SuccessOutput(result interface{}, override string, outputFormat string) {
log.Fatal().Err(err)
}
default:
+ //nolint
fmt.Println(override)
+
return
}
+ //nolint
fmt.Println(string(j))
}
@@ -423,6 +442,7 @@ func HasMachineOutputFlag() bool {
return true
}
}
+
return false
}
@@ -431,7 +451,10 @@ type tokenAuth struct {
}
// Return value is mapped to request headers.
-func (t tokenAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
+func (t tokenAuth) GetRequestMetadata(
+ ctx context.Context,
+ in ...string,
+) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
}, nil
diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go
index ed4644f3..d6bf2166 100644
--- a/cmd/headscale/headscale.go
+++ b/cmd/headscale/headscale.go
@@ -23,6 +23,8 @@ func main() {
colors = true
case termcolor.LevelBasic:
colors = true
+ case termcolor.LevelNone:
+ colors = false
default:
// no color, return text as is.
colors = false
@@ -41,8 +43,7 @@ func main() {
NoColor: !colors,
})
- err := cli.LoadConfig("")
- if err != nil {
+ if err := cli.LoadConfig(""); err != nil {
log.Fatal().Err(err)
}
@@ -63,13 +64,15 @@ func main() {
}
if !viper.GetBool("disable_check_updates") && !machineOutput {
- if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && cli.Version != "dev" {
+ 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,
diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go
index e3a5713f..1166d489 100644
--- a/cmd/headscale/headscale_test.go
+++ b/cmd/headscale/headscale_test.go
@@ -1,7 +1,6 @@
package main
import (
- "fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -40,7 +39,10 @@ func (*Suite) TestConfigLoading(c *check.C) {
}
// Symlink the example config file
- err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml"))
+ err = os.Symlink(
+ filepath.Clean(path+"/../../config-example.yaml"),
+ filepath.Join(tmpDir, "config.yaml"),
+ )
if err != nil {
c.Fatal(err)
}
@@ -74,7 +76,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
}
// Symlink the example config file
- err = os.Symlink(filepath.Clean(path+"/../../config-example.yaml"), filepath.Join(tmpDir, "config.yaml"))
+ err = os.Symlink(
+ filepath.Clean(path+"/../../config-example.yaml"),
+ filepath.Join(tmpDir, "config.yaml"),
+ )
if err != nil {
c.Fatal(err)
}
@@ -94,7 +99,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
// Populate a custom config file
configFile := filepath.Join(tmpDir, "config.yaml")
- err := ioutil.WriteFile(configFile, configYaml, 0o644)
+ err := ioutil.WriteFile(configFile, configYaml, 0o600)
if err != nil {
c.Fatalf("Couldn't write file %s", configFile)
}
@@ -106,7 +111,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
c.Fatal(err)
}
// defer os.RemoveAll(tmpDir)
- fmt.Println(tmpDir)
configYaml := []byte(
"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
@@ -128,8 +132,11 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
check.Matches,
".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*",
)
- c.Assert(tmp, check.Matches, ".*Fatal config error: server_url must start with https:// or http://.*")
- fmt.Println(tmp)
+ c.Assert(
+ tmp,
+ check.Matches,
+ ".*Fatal config error: server_url must start with https:// or http://.*",
+ )
// Check configuration validation errors (2)
configYaml = []byte(
diff --git a/db.go b/db.go
index 42c5eee9..51363256 100644
--- a/db.go
+++ b/db.go
@@ -9,7 +9,10 @@ import (
"gorm.io/gorm/logger"
)
-const dbVersion = "1"
+const (
+ dbVersion = "1"
+ errValueNotFound = Error("not found")
+)
// KV is a key-value store in a psql table. For future use...
type KV struct {
@@ -24,7 +27,7 @@ func (h *Headscale) initDB() error {
}
h.db = db
- if h.dbType == "postgres" {
+ if h.dbType == Postgres {
db.Exec("create extension if not exists \"uuid-ossp\";")
}
err = db.AutoMigrate(&Machine{})
@@ -50,6 +53,7 @@ func (h *Headscale) initDB() error {
}
err = h.setValue("db_version", dbVersion)
+
return err
}
@@ -65,12 +69,12 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
}
switch h.dbType {
- case "sqlite3":
+ case Sqlite:
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
})
- case "postgres":
+ case Postgres:
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: log,
@@ -84,28 +88,33 @@ func (h *Headscale) openDB() (*gorm.DB, error) {
return db, nil
}
-// getValue returns the value for the given key in KV
+// getValue returns the value for the given key in KV.
func (h *Headscale) getValue(key string) (string, error) {
var row KV
- if result := h.db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) {
- return "", errors.New("not found")
+ if result := h.db.First(&row, "key = ?", key); errors.Is(
+ result.Error,
+ gorm.ErrRecordNotFound,
+ ) {
+ return "", errValueNotFound
}
+
return row.Value, nil
}
-// setValue sets value for the given key in KV
+// setValue sets value for the given key in KV.
func (h *Headscale) setValue(key string, value string) error {
- kv := KV{
+ keyValue := KV{
Key: key,
Value: value,
}
- _, err := h.getValue(key)
- if err == nil {
- h.db.Model(&kv).Where("key = ?", key).Update("value", value)
+ if _, err := h.getValue(key); err == nil {
+ h.db.Model(&keyValue).Where("key = ?", key).Update("value", value)
+
return nil
}
- h.db.Create(kv)
+ h.db.Create(keyValue)
+
return nil
}
diff --git a/derp-example.yaml b/derp-example.yaml
index bbf7cc8d..a9901bef 100644
--- a/derp-example.yaml
+++ b/derp-example.yaml
@@ -1,15 +1,15 @@
# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
-regions:
+regions:
900:
regionid: 900
regioncode: custom
regionname: My Region
nodes:
- - name: 1a
- regionid: 1
- hostname: myderp.mydomain.no
- ipv4: 123.123.123.123
- ipv6: "2604:a880:400:d1::828:b001"
- stunport: 0
- stunonly: false
- derptestport: 0
+ - name: 1a
+ regionid: 1
+ hostname: myderp.mydomain.no
+ ipv4: 123.123.123.123
+ ipv6: "2604:a880:400:d1::828:b001"
+ stunport: 0
+ stunonly: false
+ derptestport: 0
diff --git a/derp.go b/derp.go
index 7f65832c..63e448db 100644
--- a/derp.go
+++ b/derp.go
@@ -1,6 +1,7 @@
package headscale
import (
+ "context"
"encoding/json"
"io"
"io/ioutil"
@@ -10,9 +11,7 @@ import (
"time"
"github.com/rs/zerolog/log"
-
"gopkg.in/yaml.v2"
-
"tailscale.com/tailcfg"
)
@@ -28,14 +27,24 @@ func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) {
return nil, err
}
err = yaml.Unmarshal(b, &derpMap)
+
return &derpMap, err
}
func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
- client := http.Client{
- Timeout: 10 * time.Second,
+ ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, "GET", addr.String(), nil)
+ if err != nil {
+ return nil, err
}
- resp, err := client.Get(addr.String())
+
+ client := http.Client{
+ Timeout: HTTPReadTimeout,
+ }
+
+ resp, err := client.Do(req)
if err != nil {
return nil, err
}
@@ -48,6 +57,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
err = json.Unmarshal(body, &derpMap)
+
return &derpMap, err
}
@@ -55,7 +65,7 @@ func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
// DERPMap, it will _only_ look at the Regions, an integer.
// If a region exists in two of the given DERPMaps, the region
// form the _last_ DERPMap will be preserved.
-// An empty DERPMap list will result in a DERPMap with no regions
+// An empty DERPMap list will result in a DERPMap with no regions.
func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
result := tailcfg.DERPMap{
OmitDefaultRegions: false,
@@ -86,6 +96,7 @@ func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap {
Str("path", path).
Err(err).
Msg("Could not load DERP map from path")
+
break
}
@@ -104,6 +115,7 @@ func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap {
Str("url", addr.String()).
Err(err).
Msg("Could not load DERP map from path")
+
break
}
diff --git a/dns.go b/dns.go
index c7ca32ac..af6f989d 100644
--- a/dns.go
+++ b/dns.go
@@ -10,6 +10,10 @@ import (
"tailscale.com/util/dnsname"
)
+const (
+ ByteSize = 8
+)
+
// generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`.
// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS
// server (listening in 100.100.100.100 udp/53) should be used for.
@@ -30,7 +34,9 @@ import (
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
-func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) ([]dnsname.FQDN, error) {
+func generateMagicDNSRootDomains(
+ ipPrefix netaddr.IPPrefix,
+) []dnsname.FQDN {
// TODO(juanfont): we are not handing out IPv6 addresses yet
// and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network)
ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.")
@@ -41,15 +47,15 @@ func generateMagicDNSRootDomains(ipPrefix netaddr.IPPrefix, baseDomain string) (
maskBits, _ := netRange.Mask.Size()
// lastOctet is the last IP byte covered by the mask
- lastOctet := maskBits / 8
+ lastOctet := maskBits / ByteSize
// wildcardBits is the number of bits not under the mask in the lastOctet
- wildcardBits := 8 - maskBits%8
+ wildcardBits := ByteSize - maskBits%ByteSize
// min is the value in the lastOctet byte of the IP
// max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1
min := uint(netRange.IP[lastOctet])
- max := uint((min + 1< config/private.key
cp config.yaml.[sqlite|postgres].example config/config.yaml
-
+
cp derp-example.yaml config/derp.yaml
```
@@ -81,16 +85,19 @@
-p 127.0.0.1:8080:8080 \
headscale/headscale:x.x.x headscale serve
```
+
## Nodes configuration
If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder
- ```shell
- systemctl stop tailscaled
- rm -fr /var/lib/tailscale
- systemctl start tailscaled
- ```
+```shell
+systemctl stop tailscaled
+rm -fr /var/lib/tailscale
+systemctl start tailscaled
+```
+
### Adding node based on MACHINEKEY
+
1. Add your first machine
```shell
diff --git a/grpcv1.go b/grpcv1.go
index 998f0c0e..40419c3c 100644
--- a/grpcv1.go
+++ b/grpcv1.go
@@ -155,7 +155,10 @@ func (api headscaleV1APIServer) RegisterMachine(
ctx context.Context,
request *v1.RegisterMachineRequest,
) (*v1.RegisterMachineResponse, error) {
- log.Trace().Str("namespace", request.GetNamespace()).Str("machine_key", request.GetKey()).Msg("Registering machine")
+ log.Trace().
+ Str("namespace", request.GetNamespace()).
+ Str("machine_key", request.GetKey()).
+ Msg("Registering machine")
machine, err := api.h.RegisterMachine(
request.GetKey(),
request.GetNamespace(),
@@ -208,7 +211,9 @@ func (api headscaleV1APIServer) ListMachines(
return nil, err
}
- sharedMachines, err := api.h.ListSharedMachinesInNamespace(request.GetNamespace())
+ sharedMachines, err := api.h.ListSharedMachinesInNamespace(
+ request.GetNamespace(),
+ )
if err != nil {
return nil, err
}
@@ -333,12 +338,16 @@ func (api headscaleV1APIServer) DebugCreateMachine(
return nil, err
}
- routes, err := stringToIpPrefix(request.GetRoutes())
+ routes, err := stringToIPPrefix(request.GetRoutes())
if err != nil {
return nil, err
}
- log.Trace().Caller().Interface("route-prefix", routes).Interface("route-str", request.GetRoutes()).Msg("")
+ log.Trace().
+ Caller().
+ Interface("route-prefix", routes).
+ Interface("route-str", request.GetRoutes()).
+ Msg("")
hostinfo := tailcfg.Hostinfo{
RoutableIPs: routes,
diff --git a/integration_cli_test.go b/integration_cli_test.go
index e1b16727..898e2cd7 100644
--- a/integration_cli_test.go
+++ b/integration_cli_test.go
@@ -88,6 +88,7 @@ func (s *IntegrationCLITestSuite) SetupTest() {
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK")
}
+
return nil
}); err != nil {
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
@@ -109,7 +110,10 @@ func (s *IntegrationCLITestSuite) TearDownTest() {
}
}
-func (s *IntegrationCLITestSuite) HandleStats(suiteName string, stats *suite.SuiteInformation) {
+func (s *IntegrationCLITestSuite) HandleStats(
+ suiteName string,
+ stats *suite.SuiteInformation,
+) {
s.stats = stats
}
@@ -144,7 +148,6 @@ func (s *IntegrationCLITestSuite) TestNamespaceCommand() {
namespaces := make([]*v1.Namespace, len(names))
for index, namespaceName := range names {
-
namespace, err := s.createNamespace(namespaceName)
assert.Nil(s.T(), err)
@@ -298,11 +301,26 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
assert.True(s.T(), listedPreAuthKeys[3].Expiration.AsTime().After(time.Now()))
assert.True(s.T(), listedPreAuthKeys[4].Expiration.AsTime().After(time.Now()))
- assert.True(s.T(), listedPreAuthKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)))
- assert.True(s.T(), listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)))
- assert.True(s.T(), listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)))
- assert.True(s.T(), listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)))
- assert.True(s.T(), listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)))
+ assert.True(
+ s.T(),
+ listedPreAuthKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ )
+ assert.True(
+ s.T(),
+ listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ )
+ assert.True(
+ s.T(),
+ listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ )
+ assert.True(
+ s.T(),
+ listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ )
+ assert.True(
+ s.T(),
+ listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)),
+ )
// Expire three keys
for i := 0; i < 3; i++ {
@@ -341,11 +359,26 @@ func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() {
err = json.Unmarshal([]byte(listAfterExpireResult), &listedAfterExpirePreAuthKeys)
assert.Nil(s.T(), err)
- assert.True(s.T(), listedAfterExpirePreAuthKeys[0].Expiration.AsTime().Before(time.Now()))
- assert.True(s.T(), listedAfterExpirePreAuthKeys[1].Expiration.AsTime().Before(time.Now()))
- assert.True(s.T(), listedAfterExpirePreAuthKeys[2].Expiration.AsTime().Before(time.Now()))
- assert.True(s.T(), listedAfterExpirePreAuthKeys[3].Expiration.AsTime().After(time.Now()))
- assert.True(s.T(), listedAfterExpirePreAuthKeys[4].Expiration.AsTime().After(time.Now()))
+ assert.True(
+ s.T(),
+ listedAfterExpirePreAuthKeys[0].Expiration.AsTime().Before(time.Now()),
+ )
+ assert.True(
+ s.T(),
+ listedAfterExpirePreAuthKeys[1].Expiration.AsTime().Before(time.Now()),
+ )
+ assert.True(
+ s.T(),
+ listedAfterExpirePreAuthKeys[2].Expiration.AsTime().Before(time.Now()),
+ )
+ assert.True(
+ s.T(),
+ listedAfterExpirePreAuthKeys[3].Expiration.AsTime().After(time.Now()),
+ )
+ assert.True(
+ s.T(),
+ listedAfterExpirePreAuthKeys[4].Expiration.AsTime().After(time.Now()),
+ )
}
func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() {
@@ -689,7 +722,10 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err)
var listOnlySharedMachineNamespace []v1.Machine
- err = json.Unmarshal([]byte(listOnlySharedMachineNamespaceResult), &listOnlySharedMachineNamespace)
+ err = json.Unmarshal(
+ []byte(listOnlySharedMachineNamespaceResult),
+ &listOnlySharedMachineNamespace,
+ )
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlySharedMachineNamespace, 2)
@@ -738,7 +774,10 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err)
var listOnlyMachineNamespaceAfterDelete []v1.Machine
- err = json.Unmarshal([]byte(listOnlyMachineNamespaceAfterDeleteResult), &listOnlyMachineNamespaceAfterDelete)
+ err = json.Unmarshal(
+ []byte(listOnlyMachineNamespaceAfterDeleteResult),
+ &listOnlyMachineNamespaceAfterDelete,
+ )
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterDelete, 4)
@@ -789,7 +828,10 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err)
var listOnlyMachineNamespaceAfterShare []v1.Machine
- err = json.Unmarshal([]byte(listOnlyMachineNamespaceAfterShareResult), &listOnlyMachineNamespaceAfterShare)
+ err = json.Unmarshal(
+ []byte(listOnlyMachineNamespaceAfterShareResult),
+ &listOnlyMachineNamespaceAfterShare,
+ )
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterShare, 5)
@@ -846,7 +888,10 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Nil(s.T(), err)
var listOnlyMachineNamespaceAfterUnshare []v1.Machine
- err = json.Unmarshal([]byte(listOnlyMachineNamespaceAfterUnshareResult), &listOnlyMachineNamespaceAfterUnshare)
+ err = json.Unmarshal(
+ []byte(listOnlyMachineNamespaceAfterUnshareResult),
+ &listOnlyMachineNamespaceAfterUnshare,
+ )
assert.Nil(s.T(), err)
assert.Len(s.T(), listOnlyMachineNamespaceAfterUnshare, 4)
@@ -1010,5 +1055,9 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
)
assert.Nil(s.T(), err)
- assert.Contains(s.T(), string(failEnableNonAdvertisedRoute), "route (route-machine) is not available on node")
+ assert.Contains(
+ s.T(),
+ string(failEnableNonAdvertisedRoute),
+ "route (route-machine) is not available on node",
+ )
}
diff --git a/integration_common_test.go b/integration_common_test.go
index 71d48669..31bae514 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -12,12 +12,18 @@ import (
"github.com/ory/dockertest/v3/docker"
)
-func ExecuteCommand(resource *dockertest.Resource, cmd []string, env []string) (string, error) {
+const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
+
+func ExecuteCommand(
+ resource *dockertest.Resource,
+ cmd []string,
+ env []string,
+) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
// TODO(kradalby): Make configurable
- timeout := 10 * time.Second
+ timeout := DOCKER_EXECUTE_TIMEOUT
type result struct {
exitCode int
@@ -51,11 +57,13 @@ func ExecuteCommand(resource *dockertest.Resource, cmd []string, env []string) (
fmt.Println("Command: ", cmd)
fmt.Println("stdout: ", stdout.String())
fmt.Println("stderr: ", stderr.String())
+
return "", fmt.Errorf("command failed with: %s", stderr.String())
}
return stdout.String(), nil
case <-time.After(timeout):
+
return "", fmt.Errorf("command timed out after %s", timeout)
}
}
diff --git a/integration_test.go b/integration_test.go
index c4f6bb41..a9125a0a 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -23,10 +23,9 @@ import (
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
+ "inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
-
- "inet.af/netaddr"
)
var tailscaleVersions = []string{"1.16.2", "1.14.3", "1.12.3"}
@@ -89,7 +88,10 @@ func TestIntegrationTestSuite(t *testing.T) {
}
}
-func (s *IntegrationTestSuite) saveLog(resource *dockertest.Resource, basePath string) error {
+func (s *IntegrationTestSuite) saveLog(
+ resource *dockertest.Resource,
+ basePath string,
+) error {
err := os.MkdirAll(basePath, os.ModePerm)
if err != nil {
return err
@@ -118,12 +120,20 @@ func (s *IntegrationTestSuite) saveLog(resource *dockertest.Resource, basePath s
fmt.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
- err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stdout.log"), []byte(stdout.String()), 0o644)
+ err = ioutil.WriteFile(
+ path.Join(basePath, resource.Container.Name+".stdout.log"),
+ []byte(stdout.String()),
+ 0o644,
+ )
if err != nil {
return err
}
- err = ioutil.WriteFile(path.Join(basePath, resource.Container.Name+".stderr.log"), []byte(stdout.String()), 0o644)
+ err = ioutil.WriteFile(
+ path.Join(basePath, resource.Container.Name+".stderr.log"),
+ []byte(stdout.String()),
+ 0o644,
+ )
if err != nil {
return err
}
@@ -144,24 +154,38 @@ func (s *IntegrationTestSuite) tailscaleContainer(
},
},
}
- hostname := fmt.Sprintf("%s-tailscale-%s-%s", namespace, strings.Replace(version, ".", "-", -1), identifier)
+ hostname := fmt.Sprintf(
+ "%s-tailscale-%s-%s",
+ namespace,
+ strings.Replace(version, ".", "-", -1),
+ identifier,
+ )
tailscaleOptions := &dockertest.RunOptions{
Name: hostname,
Networks: []*dockertest.Network{&s.network},
- Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"},
+ Cmd: []string{
+ "tailscaled",
+ "--tun=userspace-networking",
+ "--socks5-server=localhost:1055",
+ },
}
- pts, err := s.pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, DockerRestartPolicy)
+ pts, err := s.pool.BuildAndRunWithBuildOptions(
+ tailscaleBuildOptions,
+ tailscaleOptions,
+ DockerRestartPolicy,
+ )
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
fmt.Printf("Created %s container\n", hostname)
+
return hostname, pts
}
func (s *IntegrationTestSuite) SetupSuite() {
var err error
- h = Headscale{
+ app = Headscale{
dbType: "sqlite3",
dbString: "integration_test_db.sqlite3",
}
@@ -210,7 +234,11 @@ func (s *IntegrationTestSuite) SetupSuite() {
for i := 0; i < scales.count; i++ {
version := tailscaleVersions[i%len(tailscaleVersions)]
- hostname, container := s.tailscaleContainer(namespace, fmt.Sprint(i), version)
+ hostname, container := s.tailscaleContainer(
+ namespace,
+ fmt.Sprint(i),
+ version,
+ )
scales.tailscales[hostname] = *container
}
}
@@ -220,13 +248,16 @@ func (s *IntegrationTestSuite) SetupSuite() {
if err := s.pool.Retry(func() error {
url := fmt.Sprintf("http://%s/health", hostEndpoint)
+
resp, err := http.Get(url)
if err != nil {
return err
}
+
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK")
}
+
return nil
}); err != nil {
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
@@ -273,7 +304,10 @@ func (s *IntegrationTestSuite) SetupSuite() {
headscaleEndpoint := "http://headscale:8080"
- fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint)
+ fmt.Printf(
+ "Joining tailscale containers to headscale at %s\n",
+ headscaleEndpoint,
+ )
for hostname, tailscale := range scales.tailscales {
command := []string{
"tailscale",
@@ -307,7 +341,10 @@ func (s *IntegrationTestSuite) SetupSuite() {
func (s *IntegrationTestSuite) TearDownSuite() {
}
-func (s *IntegrationTestSuite) HandleStats(suiteName string, stats *suite.SuiteInformation) {
+func (s *IntegrationTestSuite) HandleStats(
+ suiteName string,
+ stats *suite.SuiteInformation,
+) {
s.stats = stats
}
@@ -427,7 +464,13 @@ func (s *IntegrationTestSuite) TestPingAllPeers() {
ip.String(),
}
- fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip)
+ fmt.Printf(
+ "Pinging from %s (%s) to %s (%s)\n",
+ hostname,
+ ips[hostname],
+ peername,
+ ip,
+ )
result, err := ExecuteCommand(
&tailscale,
command,
@@ -449,7 +492,15 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
result, err := ExecuteCommand(
&s.headscale,
- []string{"headscale", "nodes", "list", "--output", "json", "--namespace", "shared"},
+ []string{
+ "headscale",
+ "nodes",
+ "list",
+ "--output",
+ "json",
+ "--namespace",
+ "shared",
+ },
[]string{},
)
assert.Nil(s.T(), err)
@@ -459,7 +510,6 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
assert.Nil(s.T(), err)
for _, machine := range machineList {
-
result, err := ExecuteCommand(
&s.headscale,
[]string{
@@ -520,7 +570,13 @@ func (s *IntegrationTestSuite) TestSharedNodes() {
ip.String(),
}
- fmt.Printf("Pinging from %s (%s) to %s (%s)\n", hostname, mainIps[hostname], peername, ip)
+ fmt.Printf(
+ "Pinging from %s (%s) to %s (%s)\n",
+ hostname,
+ mainIps[hostname],
+ peername,
+ ip,
+ )
result, err := ExecuteCommand(
&tailscale,
command,
@@ -553,7 +609,6 @@ func (s *IntegrationTestSuite) TestTailDrop() {
for peername, ip := range ips {
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
if peername != hostname {
-
// Under normal circumstances, we should be able to send a file
// using `tailscale file cp` - but not in userspace networking mode
// So curl!
@@ -578,9 +633,19 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"PUT",
"--upload-file",
fmt.Sprintf("/tmp/file_from_%s", hostname),
- fmt.Sprintf("%s/v0/put/file_from_%s", peerAPI, hostname),
+ fmt.Sprintf(
+ "%s/v0/put/file_from_%s",
+ peerAPI,
+ hostname,
+ ),
}
- fmt.Printf("Sending file from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip)
+ fmt.Printf(
+ "Sending file from %s (%s) to %s (%s)\n",
+ hostname,
+ ips[hostname],
+ peername,
+ ip,
+ )
_, err = ExecuteCommand(
&tailscale,
command,
@@ -621,7 +686,13 @@ func (s *IntegrationTestSuite) TestTailDrop() {
"ls",
fmt.Sprintf("/tmp/file_from_%s", peername),
}
- fmt.Printf("Checking file in %s (%s) from %s (%s)\n", hostname, ips[hostname], peername, ip)
+ fmt.Printf(
+ "Checking file in %s (%s) from %s (%s)\n",
+ hostname,
+ ips[hostname],
+ peername,
+ ip,
+ )
result, err := ExecuteCommand(
&tailscale,
command,
@@ -629,7 +700,11 @@ func (s *IntegrationTestSuite) TestTailDrop() {
)
assert.Nil(t, err)
fmt.Printf("Result for %s: %s\n", peername, result)
- assert.Equal(t, result, fmt.Sprintf("/tmp/file_from_%s\n", peername))
+ assert.Equal(
+ t,
+ result,
+ fmt.Sprintf("/tmp/file_from_%s\n", peername),
+ )
}
})
}
@@ -696,10 +771,13 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e
ips[hostname] = ip
}
+
return ips, nil
}
-func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]string, error) {
+func getAPIURLs(
+ tailscales map[string]dockertest.Resource,
+) (map[netaddr.IP]string, error) {
fts := make(map[netaddr.IP]string)
for _, tailscale := range tailscales {
command := []string{
@@ -733,5 +811,6 @@ func getAPIURLs(tailscales map[string]dockertest.Resource) (map[netaddr.IP]strin
}
}
}
+
return fts, nil
}
diff --git a/k8s/README.md b/k8s/README.md
index 45574b48..78e9ef2c 100644
--- a/k8s/README.md
+++ b/k8s/README.md
@@ -24,6 +24,7 @@ Configure DERP servers by editing `base/site/derp.yaml` if needed.
You'll somehow need to get `headscale:latest` into your cluster image registry.
An easy way to do this with k3s:
+
- Reconfigure k3s to use docker instead of containerd (`k3s server --docker`)
- `docker build -t headscale:latest ..` from here
@@ -61,7 +62,7 @@ Use the wrapper script to remotely operate headscale to perform administrative
tasks like creating namespaces, authkeys, etc.
```
-[c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash
+[c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash
headscale is an open source implementation of the Tailscale control server
diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml
index a279bc1c..51da3427 100644
--- a/k8s/base/ingress.yaml
+++ b/k8s/base/ingress.yaml
@@ -6,13 +6,13 @@ metadata:
kubernetes.io/ingress.class: traefik
spec:
rules:
- - host: $(PUBLIC_HOSTNAME)
- http:
- paths:
- - backend:
- service:
- name: headscale
- port:
- number: 8080
- path: /
- pathType: Prefix
+ - host: $(PUBLIC_HOSTNAME)
+ http:
+ paths:
+ - backend:
+ service:
+ name: headscale
+ port:
+ number: 8080
+ path: /
+ pathType: Prefix
diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml
index 54d66e54..93278f7d 100644
--- a/k8s/base/kustomization.yaml
+++ b/k8s/base/kustomization.yaml
@@ -1,42 +1,42 @@
namespace: headscale
resources:
-- configmap.yaml
-- ingress.yaml
-- service.yaml
+ - configmap.yaml
+ - ingress.yaml
+ - service.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
-- name: headscale-site
- files:
- - derp.yaml=site/derp.yaml
- envs:
- - site/public.env
-- name: headscale-etc
- literals:
- - config.json={}
+ - name: headscale-site
+ files:
+ - derp.yaml=site/derp.yaml
+ envs:
+ - site/public.env
+ - name: headscale-etc
+ literals:
+ - config.json={}
secretGenerator:
-- name: headscale
- files:
- - secrets/private-key
+ - name: headscale
+ files:
+ - secrets/private-key
vars:
-- name: PUBLIC_PROTO
- objRef:
- kind: ConfigMap
- name: headscale-site
- apiVersion: v1
- fieldRef:
- fieldPath: data.public-proto
-- name: PUBLIC_HOSTNAME
- objRef:
- kind: ConfigMap
- name: headscale-site
- apiVersion: v1
- fieldRef:
- fieldPath: data.public-hostname
-- name: CONTACT_EMAIL
- objRef:
- kind: ConfigMap
- name: headscale-site
- apiVersion: v1
- fieldRef:
- fieldPath: data.contact-email
+ - name: PUBLIC_PROTO
+ objRef:
+ kind: ConfigMap
+ name: headscale-site
+ apiVersion: v1
+ fieldRef:
+ fieldPath: data.public-proto
+ - name: PUBLIC_HOSTNAME
+ objRef:
+ kind: ConfigMap
+ name: headscale-site
+ apiVersion: v1
+ fieldRef:
+ fieldPath: data.public-hostname
+ - name: CONTACT_EMAIL
+ objRef:
+ kind: ConfigMap
+ name: headscale-site
+ apiVersion: v1
+ fieldRef:
+ fieldPath: data.contact-email
diff --git a/k8s/base/service.yaml b/k8s/base/service.yaml
index 7fdf738f..39e67253 100644
--- a/k8s/base/service.yaml
+++ b/k8s/base/service.yaml
@@ -8,6 +8,6 @@ spec:
selector:
app: headscale
ports:
- - name: http
- targetPort: http
- port: 8080
+ - name: http
+ targetPort: http
+ port: 8080
diff --git a/k8s/postgres/deployment.yaml b/k8s/postgres/deployment.yaml
index dd45d05b..661d87ed 100644
--- a/k8s/postgres/deployment.yaml
+++ b/k8s/postgres/deployment.yaml
@@ -13,66 +13,66 @@ spec:
app: headscale
spec:
containers:
- - name: headscale
- image: "headscale:latest"
- imagePullPolicy: IfNotPresent
- command: ["/go/bin/headscale", "serve"]
- env:
- - name: SERVER_URL
- value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
- - name: LISTEN_ADDR
- valueFrom:
- configMapKeyRef:
- name: headscale-config
- key: listen_addr
- - name: PRIVATE_KEY_PATH
- value: /vol/secret/private-key
- - name: DERP_MAP_PATH
- value: /vol/config/derp.yaml
- - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
- valueFrom:
- configMapKeyRef:
- name: headscale-config
- key: ephemeral_node_inactivity_timeout
- - name: DB_TYPE
- value: postgres
- - name: DB_HOST
- value: postgres.headscale.svc.cluster.local
- - name: DB_PORT
- value: "5432"
- - name: DB_USER
- value: headscale
- - name: DB_PASS
- valueFrom:
- secretKeyRef:
- name: postgresql
- key: password
- - name: DB_NAME
- value: headscale
- ports:
- - name: http
- protocol: TCP
- containerPort: 8080
- livenessProbe:
- tcpSocket:
- port: http
- initialDelaySeconds: 30
- timeoutSeconds: 5
- periodSeconds: 15
- volumeMounts:
- - name: config
- mountPath: /vol/config
- - name: secret
- mountPath: /vol/secret
- - name: etc
- mountPath: /etc/headscale
+ - name: headscale
+ image: "headscale:latest"
+ imagePullPolicy: IfNotPresent
+ command: ["/go/bin/headscale", "serve"]
+ env:
+ - name: SERVER_URL
+ value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
+ - name: LISTEN_ADDR
+ valueFrom:
+ configMapKeyRef:
+ name: headscale-config
+ key: listen_addr
+ - name: PRIVATE_KEY_PATH
+ value: /vol/secret/private-key
+ - name: DERP_MAP_PATH
+ value: /vol/config/derp.yaml
+ - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
+ valueFrom:
+ configMapKeyRef:
+ name: headscale-config
+ key: ephemeral_node_inactivity_timeout
+ - name: DB_TYPE
+ value: postgres
+ - name: DB_HOST
+ value: postgres.headscale.svc.cluster.local
+ - name: DB_PORT
+ value: "5432"
+ - name: DB_USER
+ value: headscale
+ - name: DB_PASS
+ valueFrom:
+ secretKeyRef:
+ name: postgresql
+ key: password
+ - name: DB_NAME
+ value: headscale
+ ports:
+ - name: http
+ protocol: TCP
+ containerPort: 8080
+ livenessProbe:
+ tcpSocket:
+ port: http
+ initialDelaySeconds: 30
+ timeoutSeconds: 5
+ periodSeconds: 15
+ volumeMounts:
+ - name: config
+ mountPath: /vol/config
+ - name: secret
+ mountPath: /vol/secret
+ - name: etc
+ mountPath: /etc/headscale
volumes:
- - name: config
- configMap:
- name: headscale-site
- - name: etc
- configMap:
- name: headscale-etc
- - name: secret
- secret:
- secretName: headscale
+ - name: config
+ configMap:
+ name: headscale-site
+ - name: etc
+ configMap:
+ name: headscale-etc
+ - name: secret
+ secret:
+ secretName: headscale
diff --git a/k8s/postgres/kustomization.yaml b/k8s/postgres/kustomization.yaml
index 8bd6c40c..e732e3b9 100644
--- a/k8s/postgres/kustomization.yaml
+++ b/k8s/postgres/kustomization.yaml
@@ -1,13 +1,13 @@
namespace: headscale
bases:
-- ../base
+ - ../base
resources:
-- deployment.yaml
-- postgres-service.yaml
-- postgres-statefulset.yaml
+ - deployment.yaml
+ - postgres-service.yaml
+ - postgres-statefulset.yaml
generatorOptions:
disableNameSuffixHash: true
secretGenerator:
-- name: postgresql
- files:
- - secrets/password
+ - name: postgresql
+ files:
+ - secrets/password
diff --git a/k8s/postgres/postgres-service.yaml b/k8s/postgres/postgres-service.yaml
index e2f486cc..6252e7f9 100644
--- a/k8s/postgres/postgres-service.yaml
+++ b/k8s/postgres/postgres-service.yaml
@@ -8,6 +8,6 @@ spec:
selector:
app: postgres
ports:
- - name: postgres
- targetPort: postgres
- port: 5432
+ - name: postgres
+ targetPort: postgres
+ port: 5432
diff --git a/k8s/postgres/postgres-statefulset.yaml b/k8s/postgres/postgres-statefulset.yaml
index 25285c5e..b81c9bf0 100644
--- a/k8s/postgres/postgres-statefulset.yaml
+++ b/k8s/postgres/postgres-statefulset.yaml
@@ -14,36 +14,36 @@ spec:
app: postgres
spec:
containers:
- - name: postgres
- image: "postgres:13"
- imagePullPolicy: IfNotPresent
- env:
- - name: POSTGRES_PASSWORD
- valueFrom:
- secretKeyRef:
- name: postgresql
- key: password
- - name: POSTGRES_USER
- value: headscale
- ports:
- name: postgres
- protocol: TCP
- containerPort: 5432
- livenessProbe:
- tcpSocket:
- port: 5432
- initialDelaySeconds: 30
- timeoutSeconds: 5
- periodSeconds: 15
- volumeMounts:
- - name: pgdata
- mountPath: /var/lib/postgresql/data
+ image: "postgres:13"
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: postgresql
+ key: password
+ - name: POSTGRES_USER
+ value: headscale
+ ports:
+ - name: postgres
+ protocol: TCP
+ containerPort: 5432
+ livenessProbe:
+ tcpSocket:
+ port: 5432
+ initialDelaySeconds: 30
+ timeoutSeconds: 5
+ periodSeconds: 15
+ volumeMounts:
+ - name: pgdata
+ mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- - metadata:
- name: pgdata
- spec:
- storageClassName: local-path
- accessModes: ["ReadWriteOnce"]
- resources:
- requests:
- storage: 1Gi
+ - metadata:
+ name: pgdata
+ spec:
+ storageClassName: local-path
+ accessModes: ["ReadWriteOnce"]
+ resources:
+ requests:
+ storage: 1Gi
diff --git a/k8s/production-tls/ingress-patch.yaml b/k8s/production-tls/ingress-patch.yaml
index 387c7364..9e6177fb 100644
--- a/k8s/production-tls/ingress-patch.yaml
+++ b/k8s/production-tls/ingress-patch.yaml
@@ -6,6 +6,6 @@ metadata:
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
tls:
- - hosts:
- - $(PUBLIC_HOSTNAME)
- secretName: production-cert
+ - hosts:
+ - $(PUBLIC_HOSTNAME)
+ secretName: production-cert
diff --git a/k8s/production-tls/kustomization.yaml b/k8s/production-tls/kustomization.yaml
index f57cb540..d3147f5f 100644
--- a/k8s/production-tls/kustomization.yaml
+++ b/k8s/production-tls/kustomization.yaml
@@ -1,9 +1,9 @@
namespace: headscale
bases:
-- ../base
+ - ../base
resources:
-- production-issuer.yaml
+ - production-issuer.yaml
patches:
-- path: ingress-patch.yaml
- target:
- kind: Ingress
+ - path: ingress-patch.yaml
+ target:
+ kind: Ingress
diff --git a/k8s/production-tls/production-issuer.yaml b/k8s/production-tls/production-issuer.yaml
index 7ae9131a..f436090b 100644
--- a/k8s/production-tls/production-issuer.yaml
+++ b/k8s/production-tls/production-issuer.yaml
@@ -11,6 +11,6 @@ spec:
# Secret resource used to store the account's private key.
name: letsencrypt-production-acc-key
solvers:
- - http01:
- ingress:
- class: traefik
+ - http01:
+ ingress:
+ class: traefik
diff --git a/k8s/sqlite/kustomization.yaml b/k8s/sqlite/kustomization.yaml
index 5be451ca..ca799419 100644
--- a/k8s/sqlite/kustomization.yaml
+++ b/k8s/sqlite/kustomization.yaml
@@ -1,5 +1,5 @@
namespace: headscale
bases:
-- ../base
+ - ../base
resources:
-- statefulset.yaml
+ - statefulset.yaml
diff --git a/k8s/sqlite/statefulset.yaml b/k8s/sqlite/statefulset.yaml
index 9075e007..71077dad 100644
--- a/k8s/sqlite/statefulset.yaml
+++ b/k8s/sqlite/statefulset.yaml
@@ -14,66 +14,66 @@ spec:
app: headscale
spec:
containers:
- - name: headscale
- image: "headscale:latest"
- imagePullPolicy: IfNotPresent
- command: ["/go/bin/headscale", "serve"]
- env:
- - name: SERVER_URL
- value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
- - name: LISTEN_ADDR
- valueFrom:
- configMapKeyRef:
- name: headscale-config
- key: listen_addr
- - name: PRIVATE_KEY_PATH
- value: /vol/secret/private-key
- - name: DERP_MAP_PATH
- value: /vol/config/derp.yaml
- - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
- valueFrom:
- configMapKeyRef:
- name: headscale-config
- key: ephemeral_node_inactivity_timeout
- - name: DB_TYPE
- value: sqlite3
- - name: DB_PATH
- value: /vol/data/db.sqlite
- ports:
- - name: http
- protocol: TCP
- containerPort: 8080
- livenessProbe:
- tcpSocket:
- port: http
- initialDelaySeconds: 30
- timeoutSeconds: 5
- periodSeconds: 15
- volumeMounts:
- - name: config
- mountPath: /vol/config
- - name: data
- mountPath: /vol/data
- - name: secret
- mountPath: /vol/secret
- - name: etc
- mountPath: /etc/headscale
+ - name: headscale
+ image: "headscale:latest"
+ imagePullPolicy: IfNotPresent
+ command: ["/go/bin/headscale", "serve"]
+ env:
+ - name: SERVER_URL
+ value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
+ - name: LISTEN_ADDR
+ valueFrom:
+ configMapKeyRef:
+ name: headscale-config
+ key: listen_addr
+ - name: PRIVATE_KEY_PATH
+ value: /vol/secret/private-key
+ - name: DERP_MAP_PATH
+ value: /vol/config/derp.yaml
+ - name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
+ valueFrom:
+ configMapKeyRef:
+ name: headscale-config
+ key: ephemeral_node_inactivity_timeout
+ - name: DB_TYPE
+ value: sqlite3
+ - name: DB_PATH
+ value: /vol/data/db.sqlite
+ ports:
+ - name: http
+ protocol: TCP
+ containerPort: 8080
+ livenessProbe:
+ tcpSocket:
+ port: http
+ initialDelaySeconds: 30
+ timeoutSeconds: 5
+ periodSeconds: 15
+ volumeMounts:
+ - name: config
+ mountPath: /vol/config
+ - name: data
+ mountPath: /vol/data
+ - name: secret
+ mountPath: /vol/secret
+ - name: etc
+ mountPath: /etc/headscale
volumes:
- - name: config
- configMap:
- name: headscale-site
- - name: etc
- configMap:
- name: headscale-etc
- - name: secret
- secret:
- secretName: headscale
+ - name: config
+ configMap:
+ name: headscale-site
+ - name: etc
+ configMap:
+ name: headscale-etc
+ - name: secret
+ secret:
+ secretName: headscale
volumeClaimTemplates:
- - metadata:
- name: data
- spec:
- storageClassName: local-path
- accessModes: ["ReadWriteOnce"]
- resources:
- requests:
- storage: 1Gi
+ - metadata:
+ name: data
+ spec:
+ storageClassName: local-path
+ accessModes: ["ReadWriteOnce"]
+ resources:
+ requests:
+ storage: 1Gi
diff --git a/k8s/staging-tls/ingress-patch.yaml b/k8s/staging-tls/ingress-patch.yaml
index f97974bd..5a1daf0c 100644
--- a/k8s/staging-tls/ingress-patch.yaml
+++ b/k8s/staging-tls/ingress-patch.yaml
@@ -6,6 +6,6 @@ metadata:
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
tls:
- - hosts:
- - $(PUBLIC_HOSTNAME)
- secretName: staging-cert
+ - hosts:
+ - $(PUBLIC_HOSTNAME)
+ secretName: staging-cert
diff --git a/k8s/staging-tls/kustomization.yaml b/k8s/staging-tls/kustomization.yaml
index 931f27d5..0900c583 100644
--- a/k8s/staging-tls/kustomization.yaml
+++ b/k8s/staging-tls/kustomization.yaml
@@ -1,9 +1,9 @@
namespace: headscale
bases:
-- ../base
+ - ../base
resources:
-- staging-issuer.yaml
+ - staging-issuer.yaml
patches:
-- path: ingress-patch.yaml
- target:
- kind: Ingress
+ - path: ingress-patch.yaml
+ target:
+ kind: Ingress
diff --git a/k8s/staging-tls/staging-issuer.yaml b/k8s/staging-tls/staging-issuer.yaml
index 95325f6e..cf290415 100644
--- a/k8s/staging-tls/staging-issuer.yaml
+++ b/k8s/staging-tls/staging-issuer.yaml
@@ -11,6 +11,6 @@ spec:
# Secret resource used to store the account's private key.
name: letsencrypt-staging-acc-key
solvers:
- - http01:
- ingress:
- class: traefik
+ - http01:
+ ingress:
+ class: traefik
diff --git a/machine.go b/machine.go
index 557ab5b6..dcd79700 100644
--- a/machine.go
+++ b/machine.go
@@ -10,10 +10,9 @@ import (
"time"
"github.com/fatih/set"
+ v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/timestamppb"
-
- v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"gorm.io/datatypes"
"gorm.io/gorm"
"inet.af/netaddr"
@@ -21,7 +20,13 @@ import (
"tailscale.com/types/wgkey"
)
-// Machine is a Headscale client
+const (
+ errMachineNotFound = Error("machine not found")
+ errMachineAlreadyRegistered = Error("machine already registered")
+ errMachineRouteIsNotAvailable = Error("route is not available on machine")
+)
+
+// Machine is a Headscale client.
type Machine struct {
ID uint64 `gorm:"primary_key"`
MachineKey string `gorm:"type:varchar(64);unique_index"`
@@ -56,53 +61,58 @@ type (
MachinesP []*Machine
)
-// For the time being this method is rather naive
-func (m Machine) isAlreadyRegistered() bool {
- return m.Registered
+// For the time being this method is rather naive.
+func (machine Machine) isAlreadyRegistered() bool {
+ return machine.Registered
}
-// isExpired returns whether the machine registration has expired
-func (m Machine) isExpired() bool {
- return time.Now().UTC().After(*m.Expiry)
+// isExpired returns whether the machine registration has expired.
+func (machine Machine) isExpired() bool {
+ return time.Now().UTC().After(*machine.Expiry)
}
// If the Machine is expired, updateMachineExpiry updates the Machine Expiry time to the maximum allowed duration,
// or the default duration if no Expiry time was requested by the client. The expiry time here does not (yet) cause
// a client to be disconnected, however they will have to re-auth the machine if they attempt to reconnect after the
// expiry time.
-func (h *Headscale) updateMachineExpiry(m *Machine) {
- if m.isExpired() {
+func (h *Headscale) updateMachineExpiry(machine *Machine) {
+ if machine.isExpired() {
now := time.Now().UTC()
- maxExpiry := now.Add(h.cfg.MaxMachineRegistrationDuration) // calculate the maximum expiry
- defaultExpiry := now.Add(h.cfg.DefaultMachineRegistrationDuration) // calculate the default expiry
+ maxExpiry := now.Add(
+ h.cfg.MaxMachineRegistrationDuration,
+ ) // calculate the maximum expiry
+ defaultExpiry := now.Add(
+ h.cfg.DefaultMachineRegistrationDuration,
+ ) // calculate the default expiry
// clamp the expiry time of the machine registration to the maximum allowed, or use the default if none supplied
- if maxExpiry.Before(*m.RequestedExpiry) {
+ if maxExpiry.Before(*machine.RequestedExpiry) {
log.Debug().
Msgf("Clamping registration expiry time to maximum: %v (%v)", maxExpiry, h.cfg.MaxMachineRegistrationDuration)
- m.Expiry = &maxExpiry
- } else if m.RequestedExpiry.IsZero() {
+ machine.Expiry = &maxExpiry
+ } else if machine.RequestedExpiry.IsZero() {
log.Debug().Msgf("Using default machine registration expiry time: %v (%v)", defaultExpiry, h.cfg.DefaultMachineRegistrationDuration)
- m.Expiry = &defaultExpiry
+ machine.Expiry = &defaultExpiry
} else {
- log.Debug().Msgf("Using requested machine registration expiry time: %v", m.RequestedExpiry)
- m.Expiry = m.RequestedExpiry
+ log.Debug().Msgf("Using requested machine registration expiry time: %v", machine.RequestedExpiry)
+ machine.Expiry = machine.RequestedExpiry
}
- h.db.Save(&m)
+ h.db.Save(&machine)
}
}
-func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) {
+func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Finding direct peers")
machines := Machines{}
if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered",
- m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
+ machine.NamespaceID, machine.MachineKey).Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
+
return Machines{}, err
}
@@ -110,21 +120,22 @@ func (h *Headscale) getDirectPeers(m *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msgf("Found direct machines: %s", machines.String())
+
return machines, nil
}
-// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for
-func (h *Headscale) getShared(m *Machine) (Machines, error) {
+// getShared fetches machines that are shared to the `Namespace` of the machine we are getting peers for.
+func (h *Headscale) getShared(machine *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Finding shared peers")
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("namespace_id = ?",
- m.NamespaceID).Find(&sharedMachines).Error; err != nil {
+ machine.NamespaceID).Find(&sharedMachines).Error; err != nil {
return Machines{}, err
}
@@ -137,27 +148,30 @@ func (h *Headscale) getShared(m *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msgf("Found shared peers: %s", peers.String())
+
return peers, nil
}
-// getSharedTo fetches the machines of the namespaces this machine is shared in
-func (h *Headscale) getSharedTo(m *Machine) (Machines, error) {
+// getSharedTo fetches the machines of the namespaces this machine is shared in.
+func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Finding peers in namespaces this machine is shared with")
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Preload("Machine").Preload("Machine.Namespace").Where("machine_id = ?",
- m.ID).Find(&sharedMachines).Error; err != nil {
+ machine.ID).Find(&sharedMachines).Error; err != nil {
return Machines{}, err
}
peers := make(Machines, 0)
for _, sharedMachine := range sharedMachines {
- namespaceMachines, err := h.ListMachinesInNamespace(sharedMachine.Namespace.Name)
+ namespaceMachines, err := h.ListMachinesInNamespace(
+ sharedMachine.Namespace.Name,
+ )
if err != nil {
return Machines{}, err
}
@@ -168,36 +182,40 @@ func (h *Headscale) getSharedTo(m *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msgf("Found peers we are shared with: %s", peers.String())
+
return peers, nil
}
-func (h *Headscale) getPeers(m *Machine) (Machines, error) {
- direct, err := h.getDirectPeers(m)
+func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
+ direct, err := h.getDirectPeers(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
+
return Machines{}, err
}
- shared, err := h.getShared(m)
+ shared, err := h.getShared(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
+
return Machines{}, err
}
- sharedTo, err := h.getSharedTo(m)
+ sharedTo, err := h.getSharedTo(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
+
return Machines{}, err
}
@@ -208,7 +226,7 @@ func (h *Headscale) getPeers(m *Machine) (Machines, error) {
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msgf("Found total peers: %s", peers.String())
return peers, nil
@@ -219,10 +237,11 @@ func (h *Headscale) ListMachines() ([]Machine, error) {
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Find(&machines).Error; err != nil {
return nil, err
}
+
return machines, nil
}
-// GetMachine finds a Machine by name and namespace and returns the Machine struct
+// GetMachine finds a Machine by name and namespace and returns the Machine struct.
func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) {
machines, err := h.ListMachinesInNamespace(namespace)
if err != nil {
@@ -234,73 +253,77 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
return &m, nil
}
}
- return nil, fmt.Errorf("machine not found")
+
+ return nil, errMachineNotFound
}
-// GetMachineByID finds a Machine by ID and returns the Machine struct
+// GetMachineByID finds a Machine by ID and returns the Machine struct.
func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
m := Machine{}
if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil {
return nil, result.Error
}
+
return &m, nil
}
-// GetMachineByMachineKey finds a Machine by ID and returns the Machine struct
-func (h *Headscale) GetMachineByMachineKey(mKey string) (*Machine, error) {
+// GetMachineByMachineKey finds a Machine by ID and returns the Machine struct.
+func (h *Headscale) GetMachineByMachineKey(machineKey string) (*Machine, error) {
m := Machine{}
- if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey); result.Error != nil {
+ if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", machineKey); result.Error != nil {
return nil, result.Error
}
+
return &m, nil
}
// UpdateMachine takes a Machine struct pointer (typically already loaded from database
// and updates it with the latest data from the database.
-func (h *Headscale) UpdateMachine(m *Machine) error {
- if result := h.db.Find(m).First(&m); result.Error != nil {
+func (h *Headscale) UpdateMachine(machine *Machine) error {
+ if result := h.db.Find(machine).First(&machine); result.Error != nil {
return result.Error
}
+
return nil
}
-// DeleteMachine softs deletes a Machine from the database
-func (h *Headscale) DeleteMachine(m *Machine) error {
- err := h.RemoveSharedMachineFromAllNamespaces(m)
- if err != nil && err != errorMachineNotShared {
+// DeleteMachine softs deletes a Machine from the database.
+func (h *Headscale) DeleteMachine(machine *Machine) error {
+ err := h.RemoveSharedMachineFromAllNamespaces(machine)
+ if err != nil && errors.Is(err, errMachineNotShared) {
return err
}
- m.Registered = false
- namespaceID := m.NamespaceID
- h.db.Save(&m) // we mark it as unregistered, just in case
- if err := h.db.Delete(&m).Error; err != nil {
+ machine.Registered = false
+ namespaceID := machine.NamespaceID
+ h.db.Save(&machine) // we mark it as unregistered, just in case
+ if err := h.db.Delete(&machine).Error; err != nil {
return err
}
return h.RequestMapUpdates(namespaceID)
}
-// HardDeleteMachine hard deletes a Machine from the database
-func (h *Headscale) HardDeleteMachine(m *Machine) error {
- err := h.RemoveSharedMachineFromAllNamespaces(m)
- if err != nil && err != errorMachineNotShared {
+// HardDeleteMachine hard deletes a Machine from the database.
+func (h *Headscale) HardDeleteMachine(machine *Machine) error {
+ err := h.RemoveSharedMachineFromAllNamespaces(machine)
+ if err != nil && errors.Is(err, errMachineNotShared) {
return err
}
- namespaceID := m.NamespaceID
- if err := h.db.Unscoped().Delete(&m).Error; err != nil {
+ namespaceID := machine.NamespaceID
+ if err := h.db.Unscoped().Delete(&machine).Error; err != nil {
return err
}
return h.RequestMapUpdates(namespaceID)
}
-// GetHostInfo returns a Hostinfo struct for the machine
-func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
+// GetHostInfo returns a Hostinfo struct for the machine.
+func (machine *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
hostinfo := tailcfg.Hostinfo{}
- if len(m.HostInfo) != 0 {
- hi, err := m.HostInfo.MarshalJSON()
+ if len(machine.HostInfo) != 0 {
+ hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
@@ -309,21 +332,21 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
return nil, err
}
}
+
return &hostinfo, nil
}
-func (h *Headscale) isOutdated(m *Machine) bool {
- err := h.UpdateMachine(m)
- if err != nil {
+func (h *Headscale) isOutdated(machine *Machine) bool {
+ if err := h.UpdateMachine(machine); err != nil {
// It does not seem meaningful to propagate this error as the end result
// will have to be that the machine has to be considered outdated.
return true
}
- sharedMachines, _ := h.getShared(m)
+ sharedMachines, _ := h.getShared(machine)
namespaceSet := set.New(set.ThreadSafe)
- namespaceSet.Add(m.Namespace.Name)
+ namespaceSet.Add(machine.Namespace.Name)
// Check if any of our shared namespaces has updates that we have
// not propagated.
@@ -333,27 +356,30 @@ func (h *Headscale) isOutdated(m *Machine) bool {
namespaces := make([]string, namespaceSet.Size())
for index, namespace := range namespaceSet.List() {
- namespaces[index] = namespace.(string)
+ if name, ok := namespace.(string); ok {
+ namespaces[index] = name
+ }
}
lastChange := h.getLastStateChange(namespaces...)
log.Trace().
Caller().
- Str("machine", m.Name).
- Time("last_successful_update", *m.LastSuccessfulUpdate).
+ Str("machine", machine.Name).
+ Time("last_successful_update", *machine.LastSuccessfulUpdate).
Time("last_state_change", lastChange).
- Msgf("Checking if %s is missing updates", m.Name)
- return m.LastSuccessfulUpdate.Before(lastChange)
+ Msgf("Checking if %s is missing updates", machine.Name)
+
+ return machine.LastSuccessfulUpdate.Before(lastChange)
}
-func (m Machine) String() string {
- return m.Name
+func (machine Machine) String() string {
+ return machine.Name
}
-func (ms Machines) String() string {
- temp := make([]string, len(ms))
+func (machines Machines) String() string {
+ temp := make([]string, len(machines))
- for index, machine := range ms {
+ for index, machine := range machines {
temp[index] = machine.Name
}
@@ -361,24 +387,24 @@ func (ms Machines) String() string {
}
// TODO(kradalby): Remove when we have generics...
-func (ms MachinesP) String() string {
- temp := make([]string, len(ms))
+func (machines MachinesP) String() string {
+ temp := make([]string, len(machines))
- for index, machine := range ms {
+ for index, machine := range machines {
temp[index] = machine.Name
}
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
}
-func (ms Machines) toNodes(
+func (machines Machines) toNodes(
baseDomain string,
dnsConfig *tailcfg.DNSConfig,
includeRoutes bool,
) ([]*tailcfg.Node, error) {
- nodes := make([]*tailcfg.Node, len(ms))
+ nodes := make([]*tailcfg.Node, len(machines))
- for index, machine := range ms {
+ for index, machine := range machines {
node, err := machine.toNode(baseDomain, dnsConfig, includeRoutes)
if err != nil {
return nil, err
@@ -391,20 +417,25 @@ func (ms Machines) toNodes(
}
// toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes
-// as per the expected behaviour in the official SaaS
-func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, includeRoutes bool) (*tailcfg.Node, error) {
- nKey, err := wgkey.ParseHex(m.NodeKey)
+// as per the expected behaviour in the official SaaS.
+func (machine Machine) toNode(
+ baseDomain string,
+ dnsConfig *tailcfg.DNSConfig,
+ includeRoutes bool,
+) (*tailcfg.Node, error) {
+ nodeKey, err := wgkey.ParseHex(machine.NodeKey)
if err != nil {
return nil, err
}
- mKey, err := wgkey.ParseHex(m.MachineKey)
+
+ machineKey, err := wgkey.ParseHex(machine.MachineKey)
if err != nil {
return nil, err
}
var discoKey tailcfg.DiscoKey
- if m.DiscoKey != "" {
- dKey, err := wgkey.ParseHex(m.DiscoKey)
+ if machine.DiscoKey != "" {
+ dKey, err := wgkey.ParseHex(machine.DiscoKey)
if err != nil {
return nil, err
}
@@ -414,23 +445,27 @@ func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, include
}
addrs := []netaddr.IPPrefix{}
- ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", m.IPAddress))
+ ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", machine.IPAddress))
if err != nil {
log.Trace().
Caller().
- Str("ip", m.IPAddress).
- Msgf("Failed to parse IP Prefix from IP: %s", m.IPAddress)
+ Str("ip", machine.IPAddress).
+ Msgf("Failed to parse IP Prefix from IP: %s", machine.IPAddress)
+
return nil, err
}
addrs = append(addrs, ip) // missing the ipv6 ?
allowedIPs := []netaddr.IPPrefix{}
- allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients
+ allowedIPs = append(
+ allowedIPs,
+ ip,
+ ) // we append the node own IP, as it is required by the clients
if includeRoutes {
routesStr := []string{}
- if len(m.EnabledRoutes) != 0 {
- allwIps, err := m.EnabledRoutes.MarshalJSON()
+ if len(machine.EnabledRoutes) != 0 {
+ allwIps, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
@@ -450,8 +485,8 @@ func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, include
}
endpoints := []string{}
- if len(m.Endpoints) != 0 {
- be, err := m.Endpoints.MarshalJSON()
+ if len(machine.Endpoints) != 0 {
+ be, err := machine.Endpoints.MarshalJSON()
if err != nil {
return nil, err
}
@@ -462,8 +497,8 @@ func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, include
}
hostinfo := tailcfg.Hostinfo{}
- if len(m.HostInfo) != 0 {
- hi, err := m.HostInfo.MarshalJSON()
+ if len(machine.HostInfo) != 0 {
+ hi, err := machine.HostInfo.MarshalJSON()
if err != nil {
return nil, err
}
@@ -481,29 +516,34 @@ func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, include
}
var keyExpiry time.Time
- if m.Expiry != nil {
- keyExpiry = *m.Expiry
+ if machine.Expiry != nil {
+ keyExpiry = *machine.Expiry
} else {
keyExpiry = time.Time{}
}
var hostname string
if dnsConfig != nil && dnsConfig.Proxied { // MagicDNS
- hostname = fmt.Sprintf("%s.%s.%s", m.Name, m.Namespace.Name, baseDomain)
+ hostname = fmt.Sprintf(
+ "%s.%s.%s",
+ machine.Name,
+ machine.Namespace.Name,
+ baseDomain,
+ )
} else {
- hostname = m.Name
+ hostname = machine.Name
}
- n := tailcfg.Node{
- ID: tailcfg.NodeID(m.ID), // this is the actual ID
+ node := tailcfg.Node{
+ ID: tailcfg.NodeID(machine.ID), // this is the actual ID
StableID: tailcfg.StableNodeID(
- strconv.FormatUint(m.ID, 10),
+ strconv.FormatUint(machine.ID, Base10),
), // in headscale, unlike tailcontrol server, IDs are permanent
Name: hostname,
- User: tailcfg.UserID(m.NamespaceID),
- Key: tailcfg.NodeKey(nKey),
+ User: tailcfg.UserID(machine.NamespaceID),
+ Key: tailcfg.NodeKey(nodeKey),
KeyExpiry: keyExpiry,
- Machine: tailcfg.MachineKey(mKey),
+ Machine: tailcfg.MachineKey(machineKey),
DiscoKey: discoKey,
Addresses: addrs,
AllowedIPs: allowedIPs,
@@ -511,81 +551,90 @@ func (m Machine) toNode(baseDomain string, dnsConfig *tailcfg.DNSConfig, include
DERP: derp,
Hostinfo: hostinfo,
- Created: m.CreatedAt,
- LastSeen: m.LastSeen,
+ Created: machine.CreatedAt,
+ LastSeen: machine.LastSeen,
KeepAlive: true,
- MachineAuthorized: m.Registered,
+ MachineAuthorized: machine.Registered,
Capabilities: []string{tailcfg.CapabilityFileSharing},
}
- return &n, nil
+
+ return &node, nil
}
-func (m *Machine) toProto() *v1.Machine {
- machine := &v1.Machine{
- Id: m.ID,
- MachineKey: m.MachineKey,
+func (machine *Machine) toProto() *v1.Machine {
+ machineProto := &v1.Machine{
+ Id: machine.ID,
+ MachineKey: machine.MachineKey,
- NodeKey: m.NodeKey,
- DiscoKey: m.DiscoKey,
- IpAddress: m.IPAddress,
- Name: m.Name,
- Namespace: m.Namespace.toProto(),
+ NodeKey: machine.NodeKey,
+ DiscoKey: machine.DiscoKey,
+ IpAddress: machine.IPAddress,
+ Name: machine.Name,
+ Namespace: machine.Namespace.toProto(),
- Registered: m.Registered,
+ Registered: machine.Registered,
// TODO(kradalby): Implement register method enum converter
// RegisterMethod: ,
- CreatedAt: timestamppb.New(m.CreatedAt),
+ CreatedAt: timestamppb.New(machine.CreatedAt),
}
- if m.AuthKey != nil {
- machine.PreAuthKey = m.AuthKey.toProto()
+ if machine.AuthKey != nil {
+ machineProto.PreAuthKey = machine.AuthKey.toProto()
}
- if m.LastSeen != nil {
- machine.LastSeen = timestamppb.New(*m.LastSeen)
+ if machine.LastSeen != nil {
+ machineProto.LastSeen = timestamppb.New(*machine.LastSeen)
}
- if m.LastSuccessfulUpdate != nil {
- machine.LastSuccessfulUpdate = timestamppb.New(*m.LastSuccessfulUpdate)
+ if machine.LastSuccessfulUpdate != nil {
+ machineProto.LastSuccessfulUpdate = timestamppb.New(
+ *machine.LastSuccessfulUpdate,
+ )
}
- if m.Expiry != nil {
- machine.Expiry = timestamppb.New(*m.Expiry)
+ if machine.Expiry != nil {
+ machineProto.Expiry = timestamppb.New(*machine.Expiry)
}
- return machine
+ return machineProto
}
-// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey
-func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, error) {
- ns, err := h.GetNamespace(namespace)
+// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
+func (h *Headscale) RegisterMachine(
+ key string,
+ namespaceName string,
+) (*Machine, error) {
+ namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
- mKey, err := wgkey.ParseHex(key)
+ machineKey, err := wgkey.ParseHex(key)
if err != nil {
return nil, err
}
- m := Machine{}
- if result := h.db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
- return nil, errors.New("Machine not found")
+ machine := Machine{}
+ if result := h.db.First(&machine, "machine_key = ?", machineKey.HexString()); errors.Is(
+ result.Error,
+ gorm.ErrRecordNotFound,
+ ) {
+ return nil, errMachineNotFound
}
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Attempting to register machine")
- if m.isAlreadyRegistered() {
- err := errors.New("Machine already registered")
+ if machine.isAlreadyRegistered() {
+ err := errMachineAlreadyRegistered
log.Error().
Caller().
Err(err).
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Attempting to register machine")
return nil, err
@@ -596,42 +645,44 @@ func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, err
log.Error().
Caller().
Err(err).
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Could not find IP for the new machine")
+
return nil, err
}
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("ip", ip.String()).
Msg("Found IP for host")
- m.IPAddress = ip.String()
- m.NamespaceID = ns.ID
- m.Registered = true
- m.RegisterMethod = "cli"
- h.db.Save(&m)
+ machine.IPAddress = ip.String()
+ machine.NamespaceID = namespace.ID
+ machine.Registered = true
+ machine.RegisterMethod = "cli"
+ h.db.Save(&machine)
log.Trace().
Caller().
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("ip", ip.String()).
Msg("Machine registered with the database")
- return &m, nil
+ return &machine, nil
}
-func (m *Machine) GetAdvertisedRoutes() ([]netaddr.IPPrefix, error) {
- hostInfo, err := m.GetHostInfo()
+func (machine *Machine) GetAdvertisedRoutes() ([]netaddr.IPPrefix, error) {
+ hostInfo, err := machine.GetHostInfo()
if err != nil {
return nil, err
}
+
return hostInfo.RoutableIPs, nil
}
-func (m *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
- data, err := m.EnabledRoutes.MarshalJSON()
+func (machine *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
+ data, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
@@ -654,13 +705,13 @@ func (m *Machine) GetEnabledRoutes() ([]netaddr.IPPrefix, error) {
return routes, nil
}
-func (m *Machine) IsRoutesEnabled(routeStr string) bool {
+func (machine *Machine) IsRoutesEnabled(routeStr string) bool {
route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return false
}
- enabledRoutes, err := m.GetEnabledRoutes()
+ enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return false
}
@@ -670,12 +721,13 @@ func (m *Machine) IsRoutesEnabled(routeStr string) bool {
return true
}
}
+
return false
}
// EnableNodeRoute enables new routes based on a list of new routes. It will _replace_ the
// previous list of routes.
-func (h *Headscale) EnableRoutes(m *Machine, routeStrs ...string) error {
+func (h *Headscale) EnableRoutes(machine *Machine, routeStrs ...string) error {
newRoutes := make([]netaddr.IPPrefix, len(routeStrs))
for index, routeStr := range routeStrs {
route, err := netaddr.ParseIPPrefix(routeStr)
@@ -686,14 +738,18 @@ func (h *Headscale) EnableRoutes(m *Machine, routeStrs ...string) error {
newRoutes[index] = route
}
- availableRoutes, err := m.GetAdvertisedRoutes()
+ availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return err
}
for _, newRoute := range newRoutes {
- if !containsIpPrefix(availableRoutes, newRoute) {
- return fmt.Errorf("route (%s) is not available on node %s", m.Name, newRoute)
+ if !containsIPPrefix(availableRoutes, newRoute) {
+ return fmt.Errorf(
+ "route (%s) is not available on node %s: %w",
+ machine.Name,
+ newRoute, errMachineRouteIsNotAvailable,
+ )
}
}
@@ -702,10 +758,10 @@ func (h *Headscale) EnableRoutes(m *Machine, routeStrs ...string) error {
return err
}
- m.EnabledRoutes = datatypes.JSON(routes)
- h.db.Save(&m)
+ machine.EnabledRoutes = datatypes.JSON(routes)
+ h.db.Save(&machine)
- err = h.RequestMapUpdates(m.NamespaceID)
+ err = h.RequestMapUpdates(machine.NamespaceID)
if err != nil {
return err
}
@@ -713,13 +769,13 @@ func (h *Headscale) EnableRoutes(m *Machine, routeStrs ...string) error {
return nil
}
-func (m *Machine) RoutesToProto() (*v1.Routes, error) {
- availableRoutes, err := m.GetAdvertisedRoutes()
+func (machine *Machine) RoutesToProto() (*v1.Routes, error) {
+ availableRoutes, err := machine.GetAdvertisedRoutes()
if err != nil {
return nil, err
}
- enabledRoutes, err := m.GetEnabledRoutes()
+ enabledRoutes, err := machine.GetEnabledRoutes()
if err != nil {
return nil, err
}
diff --git a/machine_test.go b/machine_test.go
index dfe84d33..cf367403 100644
--- a/machine_test.go
+++ b/machine_test.go
@@ -8,152 +8,159 @@ import (
)
func (s *Suite) TestGetMachine(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "testmachine")
+ _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil)
- m := &Machine{
+ machine := &Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(m)
+ app.db.Save(machine)
- m1, err := h.GetMachine("test", "testmachine")
+ machineFromDB, err := app.GetMachine("test", "testmachine")
c.Assert(err, check.IsNil)
- _, err = m1.GetHostInfo()
+ _, err = machineFromDB.GetHostInfo()
c.Assert(err, check.IsNil)
}
func (s *Suite) TestGetMachineByID(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachineByID(0)
+ _, err = app.GetMachineByID(0)
c.Assert(err, check.NotNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- m1, err := h.GetMachineByID(0)
+ machineByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
- _, err = m1.GetHostInfo()
+ _, err = machineByID.GetHostInfo()
c.Assert(err, check.IsNil)
}
func (s *Suite) TestDeleteMachine(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(1),
}
- h.db.Save(&m)
- err = h.DeleteMachine(&m)
+ app.db.Save(&machine)
+
+ err = app.DeleteMachine(&machine)
c.Assert(err, check.IsNil)
- v, err := h.getValue("namespaces_pending_updates")
+
+ namespacesPendingUpdates, err := app.getValue("namespaces_pending_updates")
c.Assert(err, check.IsNil)
+
names := []string{}
- err = json.Unmarshal([]byte(v), &names)
+ err = json.Unmarshal([]byte(namespacesPendingUpdates), &names)
c.Assert(err, check.IsNil)
- c.Assert(names, check.DeepEquals, []string{n.Name})
- h.checkForNamespacesPendingUpdates()
- v, _ = h.getValue("namespaces_pending_updates")
- c.Assert(v, check.Equals, "")
- _, err = h.GetMachine(n.Name, "testmachine")
+ c.Assert(names, check.DeepEquals, []string{namespace.Name})
+
+ app.checkForNamespacesPendingUpdates()
+
+ namespacesPendingUpdates, _ = app.getValue("namespaces_pending_updates")
+ c.Assert(namespacesPendingUpdates, check.Equals, "")
+ _, err = app.GetMachine(namespace.Name, "testmachine")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestHardDeleteMachine(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine3",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(1),
}
- h.db.Save(&m)
- err = h.HardDeleteMachine(&m)
+ app.db.Save(&machine)
+
+ err = app.HardDeleteMachine(&machine)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine(n.Name, "testmachine3")
+
+ _, err = app.GetMachine(namespace.Name, "testmachine3")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestGetDirectPeers(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachineByID(0)
+ _, err = app.GetMachineByID(0)
c.Assert(err, check.NotNil)
- for i := 0; i <= 10; i++ {
- m := Machine{
- ID: uint64(i),
- MachineKey: "foo" + strconv.Itoa(i),
- NodeKey: "bar" + strconv.Itoa(i),
- DiscoKey: "faa" + strconv.Itoa(i),
- Name: "testmachine" + strconv.Itoa(i),
- NamespaceID: n.ID,
+ for index := 0; index <= 10; index++ {
+ machine := Machine{
+ ID: uint64(index),
+ MachineKey: "foo" + strconv.Itoa(index),
+ NodeKey: "bar" + strconv.Itoa(index),
+ DiscoKey: "faa" + strconv.Itoa(index),
+ Name: "testmachine" + strconv.Itoa(index),
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
}
- m1, err := h.GetMachineByID(0)
+ machine0ByID, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
- _, err = m1.GetHostInfo()
+ _, err = machine0ByID.GetHostInfo()
c.Assert(err, check.IsNil)
- peers, err := h.getDirectPeers(m1)
+ peersOfMachine0, err := app.getDirectPeers(machine0ByID)
c.Assert(err, check.IsNil)
- c.Assert(len(peers), check.Equals, 9)
- c.Assert(peers[0].Name, check.Equals, "testmachine2")
- c.Assert(peers[5].Name, check.Equals, "testmachine7")
- c.Assert(peers[8].Name, check.Equals, "testmachine10")
+ c.Assert(len(peersOfMachine0), check.Equals, 9)
+ c.Assert(peersOfMachine0[0].Name, check.Equals, "testmachine2")
+ c.Assert(peersOfMachine0[5].Name, check.Equals, "testmachine7")
+ c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
}
diff --git a/metrics.go b/metrics.go
index 0d3dca34..f0ce16ec 100644
--- a/metrics.go
+++ b/metrics.go
@@ -32,7 +32,7 @@ var (
Name: "update_request_sent_to_node_total",
Help: "The number of calls/messages issued on a specific nodes update channel",
}, []string{"namespace", "machine", "status"})
- //TODO(kradalby): This is very debugging, we might want to remove it.
+ // TODO(kradalby): This is very debugging, we might want to remove it.
updateRequestsReceivedOnChannel = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: prometheusNamespace,
Name: "update_request_received_on_channel_total",
diff --git a/namespaces.go b/namespaces.go
index e5d17839..e512068d 100644
--- a/namespaces.go
+++ b/namespaces.go
@@ -15,9 +15,9 @@ import (
)
const (
- errorNamespaceExists = Error("Namespace already exists")
- errorNamespaceNotFound = Error("Namespace not found")
- errorNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
+ errNamespaceExists = Error("Namespace already exists")
+ errNamespaceNotFound = Error("Namespace not found")
+ errNamespaceNotEmptyOfNodes = Error("Namespace not empty: node(s) found")
)
// Namespace is the way Headscale implements the concept of users in Tailscale
@@ -30,51 +30,53 @@ type Namespace struct {
}
// 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) {
- n := Namespace{}
- if err := h.db.Where("name = ?", name).First(&n).Error; err == nil {
- return nil, errorNamespaceExists
+ namespace := Namespace{}
+ if err := h.db.Where("name = ?", name).First(&namespace).Error; err == nil {
+ return nil, errNamespaceExists
}
- n.Name = name
- if err := h.db.Create(&n).Error; err != nil {
+ namespace.Name = name
+ if err := h.db.Create(&namespace).Error; err != nil {
log.Error().
Str("func", "CreateNamespace").
Err(err).
Msg("Could not create row")
+
return nil, err
}
- return &n, nil
+
+ return &namespace, nil
}
// DestroyNamespace destroys a Namespace. Returns error if the Namespace does
// not exist or if there are machines associated with it.
func (h *Headscale) DestroyNamespace(name string) error {
- n, err := h.GetNamespace(name)
+ namespace, err := h.GetNamespace(name)
if err != nil {
- return errorNamespaceNotFound
+ return errNamespaceNotFound
}
- m, err := h.ListMachinesInNamespace(name)
+ machines, err := h.ListMachinesInNamespace(name)
if err != nil {
return err
}
- if len(m) > 0 {
- return errorNamespaceNotEmptyOfNodes
+ if len(machines) > 0 {
+ return errNamespaceNotEmptyOfNodes
}
keys, err := h.ListPreAuthKeys(name)
if err != nil {
return err
}
- for _, p := range keys {
- err = h.DestroyPreAuthKey(&p)
+ for _, key := range keys {
+ err = h.DestroyPreAuthKey(key)
if err != nil {
return err
}
}
- if result := h.db.Unscoped().Delete(&n); result.Error != nil {
+ if result := h.db.Unscoped().Delete(&namespace); result.Error != nil {
return result.Error
}
@@ -84,25 +86,25 @@ func (h *Headscale) DestroyNamespace(name string) error {
// RenameNamespace renames a Namespace. Returns error if the Namespace does
// not exist or if another Namespace exists with the new name.
func (h *Headscale) RenameNamespace(oldName, newName string) error {
- n, err := h.GetNamespace(oldName)
+ oldNamespace, err := h.GetNamespace(oldName)
if err != nil {
return err
}
_, err = h.GetNamespace(newName)
if err == nil {
- return errorNamespaceExists
+ return errNamespaceExists
}
- if !errors.Is(err, errorNamespaceNotFound) {
+ if !errors.Is(err, errNamespaceNotFound) {
return err
}
- n.Name = newName
+ oldNamespace.Name = newName
- if result := h.db.Save(&n); result.Error != nil {
+ if result := h.db.Save(&oldNamespace); result.Error != nil {
return result.Error
}
- err = h.RequestMapUpdates(n.ID)
+ err = h.RequestMapUpdates(oldNamespace.ID)
if err != nil {
return err
}
@@ -110,39 +112,45 @@ func (h *Headscale) RenameNamespace(oldName, newName string) error {
return nil
}
-// GetNamespace fetches a namespace by name
+// GetNamespace fetches a namespace by name.
func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
- n := Namespace{}
- if result := h.db.First(&n, "name = ?", name); errors.Is(result.Error, gorm.ErrRecordNotFound) {
- return nil, errorNamespaceNotFound
+ namespace := Namespace{}
+ if result := h.db.First(&namespace, "name = ?", name); errors.Is(
+ result.Error,
+ gorm.ErrRecordNotFound,
+ ) {
+ return nil, errNamespaceNotFound
}
- return &n, nil
+
+ return &namespace, nil
}
-// ListNamespaces gets all the existing namespaces
+// ListNamespaces gets all the existing namespaces.
func (h *Headscale) ListNamespaces() ([]Namespace, error) {
namespaces := []Namespace{}
if err := h.db.Find(&namespaces).Error; err != nil {
return nil, err
}
+
return namespaces, 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) {
- n, err := h.GetNamespace(name)
+ namespace, err := h.GetNamespace(name)
if err != nil {
return nil, err
}
machines := []Machine{}
- if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
+ if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where(&Machine{NamespaceID: namespace.ID}).Find(&machines).Error; err != nil {
return nil, err
}
+
return machines, nil
}
-// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace
+// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace.
func (h *Headscale) ListSharedMachinesInNamespace(name string) ([]Machine, error) {
namespace, err := h.GetNamespace(name)
if err != nil {
@@ -155,48 +163,61 @@ func (h *Headscale) ListSharedMachinesInNamespace(name string) ([]Machine, error
machines := []Machine{}
for _, sharedMachine := range sharedMachines {
- machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled
+ machine, err := h.GetMachineByID(
+ sharedMachine.MachineID,
+ ) // otherwise not everything comes filled
if err != nil {
return nil, err
}
machines = append(machines, *machine)
}
+
return machines, nil
}
-// SetMachineNamespace assigns a Machine to a namespace
-func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error {
- n, err := h.GetNamespace(namespaceName)
+// SetMachineNamespace assigns a Machine to a namespace.
+func (h *Headscale) SetMachineNamespace(machine *Machine, namespaceName string) error {
+ namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return err
}
- m.NamespaceID = n.ID
- h.db.Save(&m)
+ machine.NamespaceID = namespace.ID
+ h.db.Save(&machine)
+
return nil
}
-// RequestMapUpdates signals the KV worker to update the maps for this namespace
+// TODO(kradalby): Remove the need for this.
+// RequestMapUpdates signals the KV worker to update the maps for this namespace.
func (h *Headscale) RequestMapUpdates(namespaceID uint) error {
namespace := Namespace{}
if err := h.db.First(&namespace, namespaceID).Error; err != nil {
return err
}
- v, err := h.getValue("namespaces_pending_updates")
- if err != nil || v == "" {
- err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name))
+ namespacesPendingUpdates, err := h.getValue("namespaces_pending_updates")
+ if err != nil || namespacesPendingUpdates == "" {
+ err = h.setValue(
+ "namespaces_pending_updates",
+ fmt.Sprintf(`["%s"]`, namespace.Name),
+ )
if err != nil {
return err
}
+
return nil
}
names := []string{}
- err = json.Unmarshal([]byte(v), &names)
+ err = json.Unmarshal([]byte(namespacesPendingUpdates), &names)
if err != nil {
- err = h.setValue("namespaces_pending_updates", fmt.Sprintf(`["%s"]`, namespace.Name))
+ err = h.setValue(
+ "namespaces_pending_updates",
+ fmt.Sprintf(`["%s"]`, namespace.Name),
+ )
if err != nil {
return err
}
+
return nil
}
@@ -207,22 +228,24 @@ func (h *Headscale) RequestMapUpdates(namespaceID uint) error {
Str("func", "RequestMapUpdates").
Err(err).
Msg("Could not marshal namespaces_pending_updates")
+
return err
}
+
return h.setValue("namespaces_pending_updates", string(data))
}
func (h *Headscale) checkForNamespacesPendingUpdates() {
- v, err := h.getValue("namespaces_pending_updates")
+ namespacesPendingUpdates, err := h.getValue("namespaces_pending_updates")
if err != nil {
return
}
- if v == "" {
+ if namespacesPendingUpdates == "" {
return
}
namespaces := []string{}
- err = json.Unmarshal([]byte(v), &namespaces)
+ err = json.Unmarshal([]byte(namespacesPendingUpdates), &namespaces)
if err != nil {
return
}
@@ -233,24 +256,25 @@ func (h *Headscale) checkForNamespacesPendingUpdates() {
Msg("Sending updates to nodes in namespacespace")
h.setLastStateChangeToNow(namespace)
}
- newV, err := h.getValue("namespaces_pending_updates")
+ newPendingUpdateValue, err := h.getValue("namespaces_pending_updates")
if err != nil {
return
}
- if v == newV { // only clear when no changes, so we notified everybody
+ if namespacesPendingUpdates == newPendingUpdateValue { // only clear when no changes, so we notified everybody
err = h.setValue("namespaces_pending_updates", "")
if err != nil {
log.Error().
Str("func", "checkForNamespacesPendingUpdates").
Err(err).
Msg("Could not save to KV")
+
return
}
}
}
func (n *Namespace) toUser() *tailcfg.User {
- u := tailcfg.User{
+ user := tailcfg.User{
ID: tailcfg.UserID(n.ID),
LoginName: n.Name,
DisplayName: n.Name,
@@ -259,25 +283,27 @@ func (n *Namespace) toUser() *tailcfg.User {
Logins: []tailcfg.LoginID{},
Created: time.Time{},
}
- return &u
+
+ return &user
}
func (n *Namespace) toLogin() *tailcfg.Login {
- l := tailcfg.Login{
+ login := tailcfg.Login{
ID: tailcfg.LoginID(n.ID),
LoginName: n.Name,
DisplayName: n.Name,
ProfilePicURL: "",
Domain: "headscale.net",
}
- return &l
+
+ return &login
}
-func getMapResponseUserProfiles(m Machine, peers Machines) []tailcfg.UserProfile {
+func getMapResponseUserProfiles(machine Machine, peers Machines) []tailcfg.UserProfile {
namespaceMap := make(map[string]Namespace)
- namespaceMap[m.Namespace.Name] = m.Namespace
- for _, p := range peers {
- namespaceMap[p.Namespace.Name] = p.Namespace // not worth checking if already is there
+ namespaceMap[machine.Namespace.Name] = machine.Namespace
+ for _, peer := range peers {
+ namespaceMap[peer.Namespace.Name] = peer.Namespace // not worth checking if already is there
}
profiles := []tailcfg.UserProfile{}
@@ -289,12 +315,13 @@ func getMapResponseUserProfiles(m Machine, peers Machines) []tailcfg.UserProfile
DisplayName: namespace.Name,
})
}
+
return profiles
}
func (n *Namespace) toProto() *v1.Namespace {
return &v1.Namespace{
- Id: strconv.FormatUint(uint64(n.ID), 10),
+ Id: strconv.FormatUint(uint64(n.ID), Base10),
Name: n.Name,
CreatedAt: timestamppb.New(n.CreatedAt),
}
diff --git a/namespaces_test.go b/namespaces_test.go
index 72193ee2..bbae98f9 100644
--- a/namespaces_test.go
+++ b/namespaces_test.go
@@ -7,207 +7,232 @@ import (
)
func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- c.Assert(n.Name, check.Equals, "test")
+ c.Assert(namespace.Name, check.Equals, "test")
- ns, err := h.ListNamespaces()
+ namespaces, err := app.ListNamespaces()
c.Assert(err, check.IsNil)
- c.Assert(len(ns), check.Equals, 1)
+ c.Assert(len(namespaces), check.Equals, 1)
- err = h.DestroyNamespace("test")
+ err = app.DestroyNamespace("test")
c.Assert(err, check.IsNil)
- _, err = h.GetNamespace("test")
+ _, err = app.GetNamespace("test")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
- err := h.DestroyNamespace("test")
- c.Assert(err, check.Equals, errorNamespaceNotFound)
+ err := app.DestroyNamespace("test")
+ c.Assert(err, check.Equals, errNamespaceNotFound)
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- err = h.DestroyNamespace("test")
+ err = app.DestroyNamespace("test")
c.Assert(err, check.IsNil)
- result := h.db.Preload("Namespace").First(&pak, "key = ?", pak.Key)
+ result := app.db.Preload("Namespace").First(&pak, "key = ?", pak.Key)
// destroying a namespace also deletes all associated preauthkeys
c.Assert(result.Error, check.Equals, gorm.ErrRecordNotFound)
- n, err = h.CreateNamespace("test")
+ namespace, err = app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err = h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err = app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- err = h.DestroyNamespace("test")
- c.Assert(err, check.Equals, errorNamespaceNotEmptyOfNodes)
+ err = app.DestroyNamespace("test")
+ c.Assert(err, check.Equals, errNamespaceNotEmptyOfNodes)
}
func (s *Suite) TestRenameNamespace(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespaceTest, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- c.Assert(n.Name, check.Equals, "test")
+ c.Assert(namespaceTest.Name, check.Equals, "test")
- ns, err := h.ListNamespaces()
+ namespaces, err := app.ListNamespaces()
c.Assert(err, check.IsNil)
- c.Assert(len(ns), check.Equals, 1)
+ c.Assert(len(namespaces), check.Equals, 1)
- err = h.RenameNamespace("test", "test_renamed")
+ err = app.RenameNamespace("test", "test_renamed")
c.Assert(err, check.IsNil)
- _, err = h.GetNamespace("test")
- c.Assert(err, check.Equals, errorNamespaceNotFound)
+ _, err = app.GetNamespace("test")
+ c.Assert(err, check.Equals, errNamespaceNotFound)
- _, err = h.GetNamespace("test_renamed")
+ _, err = app.GetNamespace("test_renamed")
c.Assert(err, check.IsNil)
- err = h.RenameNamespace("test_does_not_exit", "test")
- c.Assert(err, check.Equals, errorNamespaceNotFound)
+ err = app.RenameNamespace("test_does_not_exit", "test")
+ c.Assert(err, check.Equals, errNamespaceNotFound)
- n2, err := h.CreateNamespace("test2")
+ namespaceTest2, err := app.CreateNamespace("test2")
c.Assert(err, check.IsNil)
- c.Assert(n2.Name, check.Equals, "test2")
+ c.Assert(namespaceTest2.Name, check.Equals, "test2")
- err = h.RenameNamespace("test2", "test_renamed")
- c.Assert(err, check.Equals, errorNamespaceExists)
+ err = app.RenameNamespace("test2", "test_renamed")
+ c.Assert(err, check.Equals, errNamespaceExists)
}
func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
- n1, err := h.CreateNamespace("shared1")
+ namespaceShared1, err := app.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
- n2, err := h.CreateNamespace("shared2")
+ namespaceShared2, err := app.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
- n3, err := h.CreateNamespace("shared3")
+ namespaceShared3, err := app.CreateNamespace("shared3")
c.Assert(err, check.IsNil)
- pak1n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
+ preAuthKeyShared1, err := app.CreatePreAuthKey(
+ namespaceShared1.Name,
+ false,
+ false,
+ nil,
+ )
c.Assert(err, check.IsNil)
- pak2n2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
+ preAuthKeyShared2, err := app.CreatePreAuthKey(
+ namespaceShared2.Name,
+ false,
+ false,
+ nil,
+ )
c.Assert(err, check.IsNil)
- pak3n3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
+ preAuthKeyShared3, err := app.CreatePreAuthKey(
+ namespaceShared3.Name,
+ false,
+ false,
+ nil,
+ )
c.Assert(err, check.IsNil)
- pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
+ preAuthKey2Shared1, err := app.CreatePreAuthKey(
+ namespaceShared1.Name,
+ false,
+ false,
+ nil,
+ )
c.Assert(err, check.IsNil)
- _, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
+ _, err = app.GetMachine(namespaceShared1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
- m1 := &Machine{
+ machineInShared1 := &Machine{
ID: 1,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
- NamespaceID: n1.ID,
- Namespace: *n1,
+ NamespaceID: namespaceShared1.ID,
+ Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
- AuthKeyID: uint(pak1n1.ID),
+ AuthKeyID: uint(preAuthKeyShared1.ID),
}
- h.db.Save(m1)
+ app.db.Save(machineInShared1)
- _, err = h.GetMachine(n1.Name, m1.Name)
+ _, err = app.GetMachine(namespaceShared1.Name, machineInShared1.Name)
c.Assert(err, check.IsNil)
- m2 := &Machine{
+ machineInShared2 := &Machine{
ID: 2,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
- NamespaceID: n2.ID,
- Namespace: *n2,
+ NamespaceID: namespaceShared2.ID,
+ Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
- AuthKeyID: uint(pak2n2.ID),
+ AuthKeyID: uint(preAuthKeyShared2.ID),
}
- h.db.Save(m2)
+ app.db.Save(machineInShared2)
- _, err = h.GetMachine(n2.Name, m2.Name)
+ _, err = app.GetMachine(namespaceShared2.Name, machineInShared2.Name)
c.Assert(err, check.IsNil)
- m3 := &Machine{
+ machineInShared3 := &Machine{
ID: 3,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3",
- NamespaceID: n3.ID,
- Namespace: *n3,
+ NamespaceID: namespaceShared3.ID,
+ Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.3",
- AuthKeyID: uint(pak3n3.ID),
+ AuthKeyID: uint(preAuthKeyShared3.ID),
}
- h.db.Save(m3)
+ app.db.Save(machineInShared3)
- _, err = h.GetMachine(n3.Name, m3.Name)
+ _, err = app.GetMachine(namespaceShared3.Name, machineInShared3.Name)
c.Assert(err, check.IsNil)
- m4 := &Machine{
+ machine2InShared1 := &Machine{
ID: 4,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4",
- NamespaceID: n1.ID,
- Namespace: *n1,
+ NamespaceID: namespaceShared1.ID,
+ Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.4",
- AuthKeyID: uint(pak4n1.ID),
+ AuthKeyID: uint(preAuthKey2Shared1.ID),
}
- h.db.Save(m4)
+ app.db.Save(machine2InShared1)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machineInShared2, namespaceShared1)
c.Assert(err, check.IsNil)
- m1peers, err := h.getPeers(m1)
+ peersOfMachine1InShared1, err := app.getPeers(machineInShared1)
c.Assert(err, check.IsNil)
- userProfiles := getMapResponseUserProfiles(*m1, m1peers)
+ userProfiles := getMapResponseUserProfiles(
+ *machineInShared1,
+ peersOfMachine1InShared1,
+ )
log.Trace().Msgf("userProfiles %#v", userProfiles)
c.Assert(len(userProfiles), check.Equals, 2)
found := false
- for _, up := range userProfiles {
- if up.DisplayName == n1.Name {
+ for _, userProfiles := range userProfiles {
+ if userProfiles.DisplayName == namespaceShared1.Name {
found = true
+
break
}
}
c.Assert(found, check.Equals, true)
found = false
- for _, up := range userProfiles {
- if up.DisplayName == n2.Name {
+ for _, userProfile := range userProfiles {
+ if userProfile.DisplayName == namespaceShared2.Name {
found = true
+
break
}
}
diff --git a/oidc.go b/oidc.go
index 51c443db..07561e8a 100644
--- a/oidc.go
+++ b/oidc.go
@@ -17,6 +17,12 @@ import (
"golang.org/x/oauth2"
)
+const (
+ oidcStateCacheExpiration = time.Minute * 5
+ oidcStateCacheCleanupInterval = time.Minute * 10
+ randomByteSize = 16
+)
+
type IDTokenClaims struct {
Name string `json:"name,omitempty"`
Groups []string `json:"groups,omitempty"`
@@ -32,6 +38,7 @@ func (h *Headscale) initOIDC() error {
if err != nil {
log.Error().Msgf("Could not retrieve OIDC Config: %s", err.Error())
+
return err
}
@@ -39,14 +46,20 @@ func (h *Headscale) initOIDC() error {
ClientID: h.cfg.OIDC.ClientID,
ClientSecret: h.cfg.OIDC.ClientSecret,
Endpoint: h.oidcProvider.Endpoint(),
- RedirectURL: fmt.Sprintf("%s/oidc/callback", strings.TrimSuffix(h.cfg.ServerURL, "/")),
- Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ RedirectURL: fmt.Sprintf(
+ "%s/oidc/callback",
+ strings.TrimSuffix(h.cfg.ServerURL, "/"),
+ ),
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
}
// init the state cache if it hasn't been already
if h.oidcStateCache == nil {
- h.oidcStateCache = cache.New(time.Minute*5, time.Minute*10)
+ h.oidcStateCache = cache.New(
+ oidcStateCacheExpiration,
+ oidcStateCacheCleanupInterval,
+ )
}
return nil
@@ -54,50 +67,53 @@ func (h *Headscale) initOIDC() error {
// RegisterOIDC redirects to the OIDC provider for authentication
// Puts machine key in cache so the callback can retrieve it using the oidc state param
-// Listens in /oidc/register/:mKey
-func (h *Headscale) RegisterOIDC(c *gin.Context) {
- mKeyStr := c.Param("mkey")
+// Listens in /oidc/register/:mKey.
+func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
+ mKeyStr := ctx.Param("mkey")
if mKeyStr == "" {
- c.String(http.StatusBadRequest, "Wrong params")
+ ctx.String(http.StatusBadRequest, "Wrong params")
+
return
}
- b := make([]byte, 16)
- _, err := rand.Read(b)
- if err != nil {
+ randomBlob := make([]byte, randomByteSize)
+ if _, err := rand.Read(randomBlob); err != nil {
log.Error().Msg("could not read 16 bytes from rand")
- c.String(http.StatusInternalServerError, "could not read 16 bytes from rand")
+ ctx.String(http.StatusInternalServerError, "could not read 16 bytes from rand")
+
return
}
- stateStr := hex.EncodeToString(b)[:32]
+ stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later
- h.oidcStateCache.Set(stateStr, mKeyStr, time.Minute*5)
+ h.oidcStateCache.Set(stateStr, mKeyStr, oidcStateCacheExpiration)
- authUrl := h.oauth2Config.AuthCodeURL(stateStr)
- log.Debug().Msgf("Redirecting to %s for authentication", authUrl)
+ authURL := h.oauth2Config.AuthCodeURL(stateStr)
+ log.Debug().Msgf("Redirecting to %s for authentication", authURL)
- c.Redirect(http.StatusFound, authUrl)
+ ctx.Redirect(http.StatusFound, authURL)
}
// OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace
// TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
// TODO: Add groups information from OIDC tokens into machine HostInfo
-// Listens in /oidc/callback
-func (h *Headscale) OIDCCallback(c *gin.Context) {
- code := c.Query("code")
- state := c.Query("state")
+// Listens in /oidc/callback.
+func (h *Headscale) OIDCCallback(ctx *gin.Context) {
+ code := ctx.Query("code")
+ state := ctx.Query("state")
if code == "" || state == "" {
- c.String(http.StatusBadRequest, "Wrong params")
+ ctx.String(http.StatusBadRequest, "Wrong params")
+
return
}
oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
if err != nil {
- c.String(http.StatusBadRequest, "Could not exchange code for token")
+ ctx.String(http.StatusBadRequest, "Could not exchange code for token")
+
return
}
@@ -105,7 +121,8 @@ func (h *Headscale) OIDCCallback(c *gin.Context) {
rawIDToken, rawIDTokenOK := oauth2Token.Extra("id_token").(string)
if !rawIDTokenOK {
- c.String(http.StatusBadRequest, "Could not extract ID Token")
+ ctx.String(http.StatusBadRequest, "Could not extract ID Token")
+
return
}
@@ -113,21 +130,26 @@ func (h *Headscale) OIDCCallback(c *gin.Context) {
idToken, err := verifier.Verify(context.Background(), rawIDToken)
if err != nil {
- c.String(http.StatusBadRequest, "Failed to verify id token: %s", err.Error())
+ ctx.String(http.StatusBadRequest, "Failed to verify id token: %s", err.Error())
+
return
}
// TODO: we can use userinfo at some point to grab additional information about the user (groups membership, etc)
- //userInfo, err := oidcProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
- //if err != nil {
- // c.String(http.StatusBadRequest, fmt.Sprintf("Failed to retrieve userinfo: %s", err))
- // return
- //}
+ // userInfo, err := oidcProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
+ // if err != nil {
+ // c.String(http.StatusBadRequest, fmt.Sprintf("Failed to retrieve userinfo: %s", err))
+ // return
+ // }
// Extract custom claims
var claims IDTokenClaims
if err = idToken.Claims(&claims); err != nil {
- c.String(http.StatusBadRequest, fmt.Sprintf("Failed to decode id token claims: %s", err))
+ ctx.String(
+ http.StatusBadRequest,
+ fmt.Sprintf("Failed to decode id token claims: %s", err),
+ )
+
return
}
@@ -135,62 +157,80 @@ func (h *Headscale) OIDCCallback(c *gin.Context) {
mKeyIf, mKeyFound := h.oidcStateCache.Get(state)
if !mKeyFound {
- log.Error().Msg("requested machine state key expired before authorisation completed")
- c.String(http.StatusBadRequest, "state has expired")
+ log.Error().
+ Msg("requested machine state key expired before authorisation completed")
+ ctx.String(http.StatusBadRequest, "state has expired")
+
return
}
mKeyStr, mKeyOK := mKeyIf.(string)
if !mKeyOK {
log.Error().Msg("could not get machine key from cache")
- c.String(http.StatusInternalServerError, "could not get machine key from cache")
+ ctx.String(
+ http.StatusInternalServerError,
+ "could not get machine key from cache",
+ )
+
return
}
// retrieve machine information
- m, err := h.GetMachineByMachineKey(mKeyStr)
+ machine, err := h.GetMachineByMachineKey(mKeyStr)
if err != nil {
log.Error().Msg("machine key not found in database")
- c.String(http.StatusInternalServerError, "could not get machine info from database")
+ ctx.String(
+ http.StatusInternalServerError,
+ "could not get machine info from database",
+ )
+
return
}
now := time.Now().UTC()
- if nsName, ok := h.getNamespaceFromEmail(claims.Email); ok {
+ if namespaceName, ok := h.getNamespaceFromEmail(claims.Email); ok {
// register the machine if it's new
- if !m.Registered {
-
+ if !machine.Registered {
log.Debug().Msg("Registering new machine after successful callback")
- ns, err := h.GetNamespace(nsName)
+ namespace, err := h.GetNamespace(namespaceName)
if err != nil {
- ns, err = h.CreateNamespace(nsName)
+ namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
- log.Error().Msgf("could not create new namespace '%s'", claims.Email)
- c.String(http.StatusInternalServerError, "could not create new namespace")
+ log.Error().
+ Msgf("could not create new namespace '%s'", claims.Email)
+ ctx.String(
+ http.StatusInternalServerError,
+ "could not create new namespace",
+ )
+
return
}
}
ip, err := h.getAvailableIP()
if err != nil {
- c.String(http.StatusInternalServerError, "could not get an IP from the pool")
+ ctx.String(
+ http.StatusInternalServerError,
+ "could not get an IP from the pool",
+ )
+
return
}
- m.IPAddress = ip.String()
- m.NamespaceID = ns.ID
- m.Registered = true
- m.RegisterMethod = "oidc"
- m.LastSuccessfulUpdate = &now
- h.db.Save(&m)
+ machine.IPAddress = ip.String()
+ machine.NamespaceID = namespace.ID
+ machine.Registered = true
+ machine.RegisterMethod = "oidc"
+ machine.LastSuccessfulUpdate = &now
+ h.db.Save(&machine)
}
- h.updateMachineExpiry(m)
+ h.updateMachineExpiry(machine)
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
+ ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
headscale
@@ -201,15 +241,17 @@ func (h *Headscale) OIDCCallback(c *gin.Context) {
`, claims.Email)))
-
}
log.Error().
Str("email", claims.Email).
Str("username", claims.Username).
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Email could not be mapped to a namespace")
- c.String(http.StatusBadRequest, "email from claim could not be mapped to a namespace")
+ ctx.String(
+ http.StatusBadRequest,
+ "email from claim could not be mapped to a namespace",
+ )
}
// getNamespaceFromEmail passes the users email through a list of "matchers"
diff --git a/oidc_test.go b/oidc_test.go
index c7a29ce9..21a4357d 100644
--- a/oidc_test.go
+++ b/oidc_test.go
@@ -145,29 +145,37 @@ func TestHeadscale_getNamespaceFromEmail(t *testing.T) {
},
}
//nolint
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- h := &Headscale{
- cfg: tt.fields.cfg,
- db: tt.fields.db,
- dbString: tt.fields.dbString,
- dbType: tt.fields.dbType,
- dbDebug: tt.fields.dbDebug,
- publicKey: tt.fields.publicKey,
- privateKey: tt.fields.privateKey,
- aclPolicy: tt.fields.aclPolicy,
- aclRules: tt.fields.aclRules,
- lastStateChange: tt.fields.lastStateChange,
- oidcProvider: tt.fields.oidcProvider,
- oauth2Config: tt.fields.oauth2Config,
- oidcStateCache: tt.fields.oidcStateCache,
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ app := &Headscale{
+ cfg: test.fields.cfg,
+ db: test.fields.db,
+ dbString: test.fields.dbString,
+ dbType: test.fields.dbType,
+ dbDebug: test.fields.dbDebug,
+ publicKey: test.fields.publicKey,
+ privateKey: test.fields.privateKey,
+ aclPolicy: test.fields.aclPolicy,
+ aclRules: test.fields.aclRules,
+ lastStateChange: test.fields.lastStateChange,
+ oidcProvider: test.fields.oidcProvider,
+ oauth2Config: test.fields.oauth2Config,
+ oidcStateCache: test.fields.oidcStateCache,
}
- got, got1 := h.getNamespaceFromEmail(tt.args.email)
- if got != tt.want {
- t.Errorf("Headscale.getNamespaceFromEmail() got = %v, want %v", got, tt.want)
+ got, got1 := app.getNamespaceFromEmail(test.args.email)
+ if got != test.want {
+ t.Errorf(
+ "Headscale.getNamespaceFromEmail() got = %v, want %v",
+ got,
+ test.want,
+ )
}
- if got1 != tt.want1 {
- t.Errorf("Headscale.getNamespaceFromEmail() got1 = %v, want %v", got1, tt.want1)
+ if got1 != test.want1 {
+ t.Errorf(
+ "Headscale.getNamespaceFromEmail() got1 = %v, want %v",
+ got1,
+ test.want1,
+ )
}
})
}
diff --git a/poll.go b/poll.go
index 6a652805..9cf14e7a 100644
--- a/poll.go
+++ b/poll.go
@@ -15,6 +15,11 @@ import (
"tailscale.com/types/wgkey"
)
+const (
+ keepAliveInterval = 60 * time.Second
+ updateCheckInterval = 10 * time.Second
+)
+
// PollNetMapHandler takes care of /machine/:id/map
//
// This is the busiest endpoint, as it keeps the HTTP long poll that updates
@@ -24,20 +29,21 @@ import (
// only after their first request (marked with the ReadOnly field).
//
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
-func (h *Headscale) PollNetMapHandler(c *gin.Context) {
+func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
log.Trace().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
+ Str("id", ctx.Param("id")).
Msg("PollNetMapHandler called")
- body, _ := io.ReadAll(c.Request.Body)
- mKeyStr := c.Param("id")
+ body, _ := io.ReadAll(ctx.Request.Body)
+ mKeyStr := ctx.Param("id")
mKey, err := wgkey.ParseHex(mKeyStr)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot parse client key")
- c.String(http.StatusBadRequest, "")
+ ctx.String(http.StatusBadRequest, "")
+
return
}
req := tailcfg.MapRequest{}
@@ -47,34 +53,36 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
Str("handler", "PollNetMap").
Err(err).
Msg("Cannot decode message")
- c.String(http.StatusBadRequest, "")
+ ctx.String(http.StatusBadRequest, "")
+
return
}
- m, err := h.GetMachineByMachineKey(mKey.HexString())
+ machine, err := h.GetMachineByMachineKey(mKey.HexString())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn().
Str("handler", "PollNetMap").
Msgf("Ignoring request, cannot find machine with key %s", mKey.HexString())
- c.String(http.StatusUnauthorized, "")
+ ctx.String(http.StatusUnauthorized, "")
+
return
}
log.Error().
Str("handler", "PollNetMap").
Msgf("Failed to fetch machine from the database with Machine key: %s", mKey.HexString())
- c.String(http.StatusInternalServerError, "")
+ ctx.String(http.StatusInternalServerError, "")
}
log.Trace().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
- Str("machine", m.Name).
+ Str("id", ctx.Param("id")).
+ Str("machine", machine.Name).
Msg("Found machine in database")
hostinfo, _ := json.Marshal(req.Hostinfo)
- m.Name = req.Hostinfo.Hostname
- m.HostInfo = datatypes.JSON(hostinfo)
- m.DiscoKey = wgkey.Key(req.DiscoKey).HexString()
+ machine.Name = req.Hostinfo.Hostname
+ machine.HostInfo = datatypes.JSON(hostinfo)
+ machine.DiscoKey = wgkey.Key(req.DiscoKey).HexString()
now := time.Now().UTC()
// From Tailscale client:
@@ -87,20 +95,21 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
// before their first real endpoint update.
if !req.ReadOnly {
endpoints, _ := json.Marshal(req.Endpoints)
- m.Endpoints = datatypes.JSON(endpoints)
- m.LastSeen = &now
+ machine.Endpoints = datatypes.JSON(endpoints)
+ machine.LastSeen = &now
}
- h.db.Save(&m)
+ h.db.Save(&machine)
- data, err := h.getMapResponse(mKey, req, m)
+ data, err := h.getMapResponse(mKey, req, machine)
if err != nil {
log.Error().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
- Str("machine", m.Name).
+ Str("id", ctx.Param("id")).
+ Str("machine", machine.Name).
Err(err).
Msg("Failed to get Map response")
- c.String(http.StatusInternalServerError, ":(")
+ ctx.String(http.StatusInternalServerError, ":(")
+
return
}
@@ -111,8 +120,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
log.Debug().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
- Str("machine", m.Name).
+ Str("id", ctx.Param("id")).
+ Str("machine", machine.Name).
Bool("readOnly", req.ReadOnly).
Bool("omitPeers", req.OmitPeers).
Bool("stream", req.Stream).
@@ -121,15 +130,16 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
if req.ReadOnly {
log.Info().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Client is starting up. Probably interested in a DERP map")
- c.Data(200, "application/json; charset=utf-8", data)
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
+
return
}
// There has been an update to _any_ of the nodes that the other nodes would
// need to know about
- h.setLastStateChangeToNow(m.Namespace.Name)
+ h.setLastStateChangeToNow(machine.Namespace.Name)
// The request is not ReadOnly, so we need to set up channels for updating
// peers via longpoll
@@ -137,8 +147,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
// Only create update channel if it has not been created
log.Trace().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
- Str("machine", m.Name).
+ Str("id", ctx.Param("id")).
+ Str("machine", machine.Name).
Msg("Loading or creating update channel")
updateChan := make(chan struct{})
@@ -152,46 +162,59 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
if req.OmitPeers && !req.Stream {
log.Info().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Client sent endpoint update and is ok with a response without peer list")
- c.Data(200, "application/json; charset=utf-8", data)
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", data)
// It sounds like we should update the nodes when we have received a endpoint update
// even tho the comments in the tailscale code dont explicitly say so.
- updateRequestsFromNode.WithLabelValues(m.Name, m.Namespace.Name, "endpoint-update").Inc()
+ updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "endpoint-update").
+ Inc()
go func() { updateChan <- struct{}{} }()
+
return
} else if req.OmitPeers && req.Stream {
log.Warn().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Ignoring request, don't know how to handle it")
- c.String(http.StatusBadRequest, "")
+ ctx.String(http.StatusBadRequest, "")
+
return
}
log.Info().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Client is ready to access the tailnet")
log.Info().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Sending initial map")
go func() { pollDataChan <- data }()
log.Info().
Str("handler", "PollNetMap").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Notifying peers")
- updateRequestsFromNode.WithLabelValues(m.Name, m.Namespace.Name, "full-update").Inc()
+ updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "full-update").
+ Inc()
go func() { updateChan <- struct{}{} }()
- h.PollNetMapStream(c, m, req, mKey, pollDataChan, keepAliveChan, updateChan, cancelKeepAlive)
+ h.PollNetMapStream(
+ ctx,
+ machine,
+ req,
+ mKey,
+ pollDataChan,
+ keepAliveChan,
+ updateChan,
+ cancelKeepAlive,
+ )
log.Trace().
Str("handler", "PollNetMap").
- Str("id", c.Param("id")).
- Str("machine", m.Name).
+ Str("id", ctx.Param("id")).
+ Str("machine", machine.Name).
Msg("Finished stream, closing PollNetMap session")
}
@@ -199,165 +222,181 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
// stream logic, ensuring we communicate updates and data
// to the connected clients.
func (h *Headscale) PollNetMapStream(
- c *gin.Context,
- m *Machine,
- req tailcfg.MapRequest,
- mKey wgkey.Key,
+ ctx *gin.Context,
+ machine *Machine,
+ mapRequest tailcfg.MapRequest,
+ machineKey wgkey.Key,
pollDataChan chan []byte,
keepAliveChan chan []byte,
updateChan chan struct{},
cancelKeepAlive chan struct{},
) {
- go h.scheduledPollWorker(cancelKeepAlive, updateChan, keepAliveChan, mKey, req, m)
+ go h.scheduledPollWorker(
+ cancelKeepAlive,
+ updateChan,
+ keepAliveChan,
+ machineKey,
+ mapRequest,
+ machine,
+ )
- c.Stream(func(w io.Writer) bool {
+ ctx.Stream(func(writer io.Writer) bool {
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Waiting for data to stream...")
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
select {
case data := <-pollDataChan:
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Sending data received via pollData channel")
- _, err := w.Write(data)
+ _, err := writer.Write(data)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "pollData").
Err(err).
Msg("Cannot write data")
+
return false
}
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Data from pollData channel written successfully")
// TODO(kradalby): Abstract away all the database calls, this can cause race conditions
// when an outdated machine object is kept alive, e.g. db is update from
// command line, but then overwritten.
- err = h.UpdateMachine(m)
+ err = h.UpdateMachine(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "pollData").
Err(err).
Msg("Cannot update machine from database")
}
now := time.Now().UTC()
- m.LastSeen = &now
+ machine.LastSeen = &now
- lastStateUpdate.WithLabelValues(m.Namespace.Name, m.Name).Set(float64(now.Unix()))
- m.LastSuccessfulUpdate = &now
+ lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Name).
+ Set(float64(now.Unix()))
+ machine.LastSuccessfulUpdate = &now
- h.db.Save(&m)
+ h.db.Save(&machine)
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "pollData").
Int("bytes", len(data)).
Msg("Machine entry in database updated successfully after sending pollData")
+
return true
case data := <-keepAliveChan:
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Sending keep alive message")
- _, err := w.Write(data)
+ _, err := writer.Write(data)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "keepAlive").
Err(err).
Msg("Cannot write keep alive message")
+
return false
}
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Keep alive sent successfully")
// TODO(kradalby): Abstract away all the database calls, this can cause race conditions
// when an outdated machine object is kept alive, e.g. db is update from
// command line, but then overwritten.
- err = h.UpdateMachine(m)
+ err = h.UpdateMachine(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "keepAlive").
Err(err).
Msg("Cannot update machine from database")
}
now := time.Now().UTC()
- m.LastSeen = &now
- h.db.Save(&m)
+ machine.LastSeen = &now
+ h.db.Save(&machine)
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "keepAlive").
Int("bytes", len(data)).
Msg("Machine updated successfully after sending keep alive")
+
return true
case <-updateChan:
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "update").
Msg("Received a request for update")
- updateRequestsReceivedOnChannel.WithLabelValues(m.Name, m.Namespace.Name).Inc()
- if h.isOutdated(m) {
+ updateRequestsReceivedOnChannel.WithLabelValues(machine.Name, machine.Namespace.Name).
+ Inc()
+ if h.isOutdated(machine) {
log.Debug().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
- Time("last_successful_update", *m.LastSuccessfulUpdate).
- Time("last_state_change", h.getLastStateChange(m.Namespace.Name)).
- Msgf("There has been updates since the last successful update to %s", m.Name)
- data, err := h.getMapResponse(mKey, req, m)
+ Str("machine", machine.Name).
+ Time("last_successful_update", *machine.LastSuccessfulUpdate).
+ Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
+ Msgf("There has been updates since the last successful update to %s", machine.Name)
+ data, err := h.getMapResponse(machineKey, mapRequest, machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "update").
Err(err).
Msg("Could not get the map update")
}
- _, err = w.Write(data)
+ _, err = writer.Write(data)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "update").
Err(err).
Msg("Could not write the map response")
- updateRequestsSentToNode.WithLabelValues(m.Name, m.Namespace.Name, "failed").Inc()
+ updateRequestsSentToNode.WithLabelValues(machine.Name, machine.Namespace.Name, "failed").
+ Inc()
+
return false
}
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "update").
Msg("Updated Map has been sent")
- updateRequestsSentToNode.WithLabelValues(m.Name, m.Namespace.Name, "success").Inc()
+ updateRequestsSentToNode.WithLabelValues(machine.Name, machine.Namespace.Name, "success").
+ Inc()
// Keep track of the last successful update,
// we sometimes end in a state were the update
@@ -366,77 +405,79 @@ func (h *Headscale) PollNetMapStream(
// TODO(kradalby): Abstract away all the database calls, this can cause race conditions
// when an outdated machine object is kept alive, e.g. db is update from
// command line, but then overwritten.
- err = h.UpdateMachine(m)
+ err = h.UpdateMachine(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "update").
Err(err).
Msg("Cannot update machine from database")
}
now := time.Now().UTC()
- lastStateUpdate.WithLabelValues(m.Namespace.Name, m.Name).Set(float64(now.Unix()))
- m.LastSuccessfulUpdate = &now
+ lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Name).
+ Set(float64(now.Unix()))
+ machine.LastSuccessfulUpdate = &now
- h.db.Save(&m)
+ h.db.Save(&machine)
} else {
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
- Time("last_successful_update", *m.LastSuccessfulUpdate).
- Time("last_state_change", h.getLastStateChange(m.Namespace.Name)).
- Msgf("%s is up to date", m.Name)
+ Str("machine", machine.Name).
+ Time("last_successful_update", *machine.LastSuccessfulUpdate).
+ Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
+ Msgf("%s is up to date", machine.Name)
}
+
return true
- case <-c.Request.Context().Done():
+ case <-ctx.Request.Context().Done():
log.Info().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("The client has closed the connection")
// TODO: Abstract away all the database calls, this can cause race conditions
// when an outdated machine object is kept alive, e.g. db is update from
// command line, but then overwritten.
- err := h.UpdateMachine(m)
+ err := h.UpdateMachine(machine)
if err != nil {
log.Error().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "Done").
Err(err).
Msg("Cannot update machine from database")
}
now := time.Now().UTC()
- m.LastSeen = &now
- h.db.Save(&m)
+ machine.LastSeen = &now
+ h.db.Save(&machine)
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "Done").
Msg("Cancelling keepAlive channel")
cancelKeepAlive <- struct{}{}
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing update channel")
- //h.closeUpdateChannel(m)
+ // h.closeUpdateChannel(m)
close(updateChan)
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing pollData channel")
close(pollDataChan)
log.Trace().
Str("handler", "PollNetMapStream").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Str("channel", "Done").
Msg("Closing keepAliveChan channel")
close(keepAliveChan)
@@ -450,12 +491,12 @@ func (h *Headscale) scheduledPollWorker(
cancelChan <-chan struct{},
updateChan chan<- struct{},
keepAliveChan chan<- []byte,
- mKey wgkey.Key,
- req tailcfg.MapRequest,
- m *Machine,
+ machineKey wgkey.Key,
+ mapRequest tailcfg.MapRequest,
+ machine *Machine,
) {
- keepAliveTicker := time.NewTicker(60 * time.Second)
- updateCheckerTicker := time.NewTicker(10 * time.Second)
+ keepAliveTicker := time.NewTicker(keepAliveInterval)
+ updateCheckerTicker := time.NewTicker(updateCheckInterval)
for {
select {
@@ -463,27 +504,29 @@ func (h *Headscale) scheduledPollWorker(
return
case <-keepAliveTicker.C:
- data, err := h.getMapKeepAliveResponse(mKey, req, m)
+ data, err := h.getMapKeepAliveResponse(machineKey, mapRequest)
if err != nil {
log.Error().
Str("func", "keepAlive").
Err(err).
Msg("Error generating the keep alive msg")
+
return
}
log.Debug().
Str("func", "keepAlive").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Sending keepalive")
keepAliveChan <- data
case <-updateCheckerTicker.C:
log.Debug().
Str("func", "scheduledPollWorker").
- Str("machine", m.Name).
+ Str("machine", machine.Name).
Msg("Sending update request")
- updateRequestsFromNode.WithLabelValues(m.Name, m.Namespace.Name, "scheduled-update").Inc()
+ updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "scheduled-update").
+ Inc()
updateChan <- struct{}{}
}
}
diff --git a/preauth_keys.go b/preauth_keys.go
index 41b10e39..50bc4746 100644
--- a/preauth_keys.go
+++ b/preauth_keys.go
@@ -7,19 +7,19 @@ import (
"strconv"
"time"
+ v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
-
- v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
)
const (
- errorAuthKeyNotFound = Error("AuthKey not found")
- errorAuthKeyExpired = Error("AuthKey expired")
+ errPreAuthKeyNotFound = Error("AuthKey not found")
+ errPreAuthKeyExpired = Error("AuthKey expired")
errSingleUseAuthKeyHasBeenUsed = Error("AuthKey has already been used")
+ errNamespaceMismatch = Error("namespace mismatch")
)
-// PreAuthKey describes a pre-authorization key usable in a particular namespace
+// PreAuthKey describes a pre-authorization key usable in a particular namespace.
type PreAuthKey struct {
ID uint64 `gorm:"primary_key"`
Key string
@@ -33,14 +33,14 @@ type PreAuthKey struct {
Expiration *time.Time
}
-// CreatePreAuthKey creates a new PreAuthKey in a namespace, and returns it
+// CreatePreAuthKey creates a new PreAuthKey in a namespace, and returns it.
func (h *Headscale) CreatePreAuthKey(
namespaceName string,
reusable bool,
ephemeral bool,
expiration *time.Time,
) (*PreAuthKey, error) {
- n, err := h.GetNamespace(namespaceName)
+ namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
@@ -51,35 +51,36 @@ func (h *Headscale) CreatePreAuthKey(
return nil, err
}
- k := PreAuthKey{
+ key := PreAuthKey{
Key: kstr,
- NamespaceID: n.ID,
- Namespace: *n,
+ NamespaceID: namespace.ID,
+ Namespace: *namespace,
Reusable: reusable,
Ephemeral: ephemeral,
CreatedAt: &now,
Expiration: expiration,
}
- h.db.Save(&k)
+ h.db.Save(&key)
- return &k, nil
+ return &key, nil
}
-// ListPreAuthKeys returns the list of PreAuthKeys for a namespace
+// ListPreAuthKeys returns the list of PreAuthKeys for a namespace.
func (h *Headscale) ListPreAuthKeys(namespaceName string) ([]PreAuthKey, error) {
- n, err := h.GetNamespace(namespaceName)
+ namespace, err := h.GetNamespace(namespaceName)
if err != nil {
return nil, err
}
keys := []PreAuthKey{}
- if err := h.db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: n.ID}).Find(&keys).Error; err != nil {
+ if err := h.db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: namespace.ID}).Find(&keys).Error; err != nil {
return nil, err
}
+
return keys, nil
}
-// GetPreAuthKey returns a PreAuthKey for a given key
+// GetPreAuthKey returns a PreAuthKey for a given key.
func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, error) {
pak, err := h.checkKeyValidity(key)
if err != nil {
@@ -87,7 +88,7 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er
}
if pak.Namespace.Name != namespace {
- return nil, errors.New("Namespace mismatch")
+ return nil, errNamespaceMismatch
}
return pak, nil
@@ -95,32 +96,36 @@ func (h *Headscale) GetPreAuthKey(namespace string, key string) (*PreAuthKey, er
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
// does not exist.
-func (h *Headscale) DestroyPreAuthKey(pak *PreAuthKey) error {
- if result := h.db.Unscoped().Delete(&pak); result.Error != nil {
+func (h *Headscale) DestroyPreAuthKey(pak PreAuthKey) error {
+ if result := h.db.Unscoped().Delete(pak); result.Error != nil {
return result.Error
}
return nil
}
-// MarkExpirePreAuthKey marks a PreAuthKey as expired
+// MarkExpirePreAuthKey marks a PreAuthKey as expired.
func (h *Headscale) ExpirePreAuthKey(k *PreAuthKey) error {
if err := h.db.Model(&k).Update("Expiration", time.Now()).Error; err != nil {
return err
}
+
return nil
}
// 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) {
pak := PreAuthKey{}
- if result := h.db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is(result.Error, gorm.ErrRecordNotFound) {
- return nil, errorAuthKeyNotFound
+ if result := h.db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is(
+ result.Error,
+ gorm.ErrRecordNotFound,
+ ) {
+ return nil, errPreAuthKeyNotFound
}
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
- return nil, errorAuthKeyExpired
+ return nil, errPreAuthKeyExpired
}
if pak.Reusable || pak.Ephemeral { // we don't need to check if has been used before
@@ -145,13 +150,14 @@ func (h *Headscale) generateKey() (string, error) {
if _, err := rand.Read(bytes); err != nil {
return "", err
}
+
return hex.EncodeToString(bytes), nil
}
func (key *PreAuthKey) toProto() *v1.PreAuthKey {
protoKey := v1.PreAuthKey{
Namespace: key.Namespace.Name,
- Id: strconv.FormatUint(key.ID, 10),
+ Id: strconv.FormatUint(key.ID, Base10),
Key: key.Key,
Ephemeral: key.Ephemeral,
Reusable: key.Reusable,
diff --git a/preauth_keys_test.go b/preauth_keys_test.go
index dceec00b..fd0feb03 100644
--- a/preauth_keys_test.go
+++ b/preauth_keys_test.go
@@ -7,189 +7,189 @@ import (
)
func (*Suite) TestCreatePreAuthKey(c *check.C) {
- _, err := h.CreatePreAuthKey("bogus", true, false, nil)
+ _, err := app.CreatePreAuthKey("bogus", true, false, nil)
c.Assert(err, check.NotNil)
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- k, err := h.CreatePreAuthKey(n.Name, true, false, nil)
+ key, err := app.CreatePreAuthKey(namespace.Name, true, false, nil)
c.Assert(err, check.IsNil)
// Did we get a valid key?
- c.Assert(k.Key, check.NotNil)
- c.Assert(len(k.Key), check.Equals, 48)
+ c.Assert(key.Key, check.NotNil)
+ c.Assert(len(key.Key), check.Equals, 48)
// Make sure the Namespace association is populated
- c.Assert(k.Namespace.Name, check.Equals, n.Name)
+ c.Assert(key.Namespace.Name, check.Equals, namespace.Name)
- _, err = h.ListPreAuthKeys("bogus")
+ _, err = app.ListPreAuthKeys("bogus")
c.Assert(err, check.NotNil)
- keys, err := h.ListPreAuthKeys(n.Name)
+ keys, err := app.ListPreAuthKeys(namespace.Name)
c.Assert(err, check.IsNil)
c.Assert(len(keys), check.Equals, 1)
// Make sure the Namespace association is populated
- c.Assert((keys)[0].Namespace.Name, check.Equals, n.Name)
+ c.Assert((keys)[0].Namespace.Name, check.Equals, namespace.Name)
}
func (*Suite) TestExpiredPreAuthKey(c *check.C) {
- n, err := h.CreateNamespace("test2")
+ namespace, err := app.CreateNamespace("test2")
c.Assert(err, check.IsNil)
now := time.Now()
- pak, err := h.CreatePreAuthKey(n.Name, true, false, &now)
+ pak, err := app.CreatePreAuthKey(namespace.Name, true, false, &now)
c.Assert(err, check.IsNil)
- p, err := h.checkKeyValidity(pak.Key)
- c.Assert(err, check.Equals, errorAuthKeyExpired)
- c.Assert(p, check.IsNil)
+ key, err := app.checkKeyValidity(pak.Key)
+ c.Assert(err, check.Equals, errPreAuthKeyExpired)
+ c.Assert(key, check.IsNil)
}
func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) {
- p, err := h.checkKeyValidity("potatoKey")
- c.Assert(err, check.Equals, errorAuthKeyNotFound)
- c.Assert(p, check.IsNil)
+ key, err := app.checkKeyValidity("potatoKey")
+ c.Assert(err, check.Equals, errPreAuthKeyNotFound)
+ c.Assert(key, check.IsNil)
}
func (*Suite) TestValidateKeyOk(c *check.C) {
- n, err := h.CreateNamespace("test3")
+ namespace, err := app.CreateNamespace("test3")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, true, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil)
c.Assert(err, check.IsNil)
- p, err := h.checkKeyValidity(pak.Key)
+ key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.IsNil)
- c.Assert(p.ID, check.Equals, pak.ID)
+ c.Assert(key.ID, check.Equals, pak.ID)
}
func (*Suite) TestAlreadyUsedKey(c *check.C) {
- n, err := h.CreateNamespace("test4")
+ namespace, err := app.CreateNamespace("test4")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testest",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- p, err := h.checkKeyValidity(pak.Key)
+ key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
- c.Assert(p, check.IsNil)
+ c.Assert(key, check.IsNil)
}
func (*Suite) TestReusableBeingUsedKey(c *check.C) {
- n, err := h.CreateNamespace("test5")
+ namespace, err := app.CreateNamespace("test5")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, true, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil)
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 1,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testest",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- p, err := h.checkKeyValidity(pak.Key)
+ key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.IsNil)
- c.Assert(p.ID, check.Equals, pak.ID)
+ c.Assert(key.ID, check.Equals, pak.ID)
}
func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) {
- n, err := h.CreateNamespace("test6")
+ namespace, err := app.CreateNamespace("test6")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- p, err := h.checkKeyValidity(pak.Key)
+ key, err := app.checkKeyValidity(pak.Key)
c.Assert(err, check.IsNil)
- c.Assert(p.ID, check.Equals, pak.ID)
+ c.Assert(key.ID, check.Equals, pak.ID)
}
func (*Suite) TestEphemeralKey(c *check.C) {
- n, err := h.CreateNamespace("test7")
+ namespace, err := app.CreateNamespace("test7")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, true, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, true, nil)
c.Assert(err, check.IsNil)
now := time.Now()
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testest",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
LastSeen: &now,
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- _, err = h.checkKeyValidity(pak.Key)
+ _, err = app.checkKeyValidity(pak.Key)
// Ephemeral keys are by definition reusable
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test7", "testest")
+ _, err = app.GetMachine("test7", "testest")
c.Assert(err, check.IsNil)
- h.expireEphemeralNodesWorker()
+ app.expireEphemeralNodesWorker()
// The machine record should have been deleted
- _, err = h.GetMachine("test7", "testest")
+ _, err = app.GetMachine("test7", "testest")
c.Assert(err, check.NotNil)
}
func (*Suite) TestExpirePreauthKey(c *check.C) {
- n, err := h.CreateNamespace("test3")
+ namespace, err := app.CreateNamespace("test3")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, true, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, true, false, nil)
c.Assert(err, check.IsNil)
c.Assert(pak.Expiration, check.IsNil)
- err = h.ExpirePreAuthKey(pak)
+ err = app.ExpirePreAuthKey(pak)
c.Assert(err, check.IsNil)
c.Assert(pak.Expiration, check.NotNil)
- p, err := h.checkKeyValidity(pak.Key)
- c.Assert(err, check.Equals, errorAuthKeyExpired)
- c.Assert(p, check.IsNil)
+ key, err := app.checkKeyValidity(pak.Key)
+ c.Assert(err, check.Equals, errPreAuthKeyExpired)
+ c.Assert(key, check.IsNil)
}
func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) {
- n, err := h.CreateNamespace("test6")
+ namespace, err := app.CreateNamespace("test6")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak.Used = true
- h.db.Save(&pak)
+ app.db.Save(&pak)
- _, err = h.checkKeyValidity(pak.Key)
+ _, err = app.checkKeyValidity(pak.Key)
c.Assert(err, check.Equals, errSingleUseAuthKeyHasBeenUsed)
}
diff --git a/proto/headscale/v1/headscale.proto b/proto/headscale/v1/headscale.proto
index 26fe2f96..e7a63fc5 100644
--- a/proto/headscale/v1/headscale.proto
+++ b/proto/headscale/v1/headscale.proto
@@ -12,115 +12,115 @@ import "headscale/v1/routes.proto";
service HeadscaleService {
// --- Namespace start ---
- rpc GetNamespace(GetNamespaceRequest) returns(GetNamespaceResponse) {
- option(google.api.http) = {
- get : "/api/v1/namespace/{name}"
+ rpc GetNamespace(GetNamespaceRequest) returns (GetNamespaceResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/namespace/{name}"
};
}
- rpc CreateNamespace(CreateNamespaceRequest) returns(CreateNamespaceResponse) {
- option(google.api.http) = {
- post : "/api/v1/namespace"
- body : "*"
+ rpc CreateNamespace(CreateNamespaceRequest) returns (CreateNamespaceResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/namespace"
+ body: "*"
};
}
- rpc RenameNamespace(RenameNamespaceRequest) returns(RenameNamespaceResponse) {
- option(google.api.http) = {
- post : "/api/v1/namespace/{old_name}/rename/{new_name}"
+ rpc RenameNamespace(RenameNamespaceRequest) returns (RenameNamespaceResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/namespace/{old_name}/rename/{new_name}"
};
}
- rpc DeleteNamespace(DeleteNamespaceRequest) returns(DeleteNamespaceResponse) {
- option(google.api.http) = {
- delete : "/api/v1/namespace/{name}"
+ rpc DeleteNamespace(DeleteNamespaceRequest) returns (DeleteNamespaceResponse) {
+ option (google.api.http) = {
+ delete: "/api/v1/namespace/{name}"
};
}
- rpc ListNamespaces(ListNamespacesRequest) returns(ListNamespacesResponse) {
- option(google.api.http) = {
- get : "/api/v1/namespace"
+ rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/namespace"
};
}
// --- Namespace end ---
// --- PreAuthKeys start ---
- rpc CreatePreAuthKey(CreatePreAuthKeyRequest) returns(CreatePreAuthKeyResponse) {
- option(google.api.http) = {
- post : "/api/v1/preauthkey"
- body : "*"
+ rpc CreatePreAuthKey(CreatePreAuthKeyRequest) returns (CreatePreAuthKeyResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/preauthkey"
+ body: "*"
};
}
- rpc ExpirePreAuthKey(ExpirePreAuthKeyRequest) returns(ExpirePreAuthKeyResponse) {
- option(google.api.http) = {
- post : "/api/v1/preauthkey/expire"
- body : "*"
+ rpc ExpirePreAuthKey(ExpirePreAuthKeyRequest) returns (ExpirePreAuthKeyResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/preauthkey/expire"
+ body: "*"
};
}
- rpc ListPreAuthKeys(ListPreAuthKeysRequest) returns(ListPreAuthKeysResponse) {
- option(google.api.http) = {
- get : "/api/v1/preauthkey"
+ rpc ListPreAuthKeys(ListPreAuthKeysRequest) returns (ListPreAuthKeysResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/preauthkey"
};
}
// --- PreAuthKeys end ---
// --- Machine start ---
- rpc DebugCreateMachine(DebugCreateMachineRequest) returns(DebugCreateMachineResponse) {
- option(google.api.http) = {
- post : "/api/v1/debug/machine"
- body : "*"
+ rpc DebugCreateMachine(DebugCreateMachineRequest) returns (DebugCreateMachineResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/debug/machine"
+ body: "*"
};
}
- rpc GetMachine(GetMachineRequest) returns(GetMachineResponse) {
- option(google.api.http) = {
- get : "/api/v1/machine/{machine_id}"
+ rpc GetMachine(GetMachineRequest) returns (GetMachineResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/machine/{machine_id}"
};
}
- rpc RegisterMachine(RegisterMachineRequest) returns(RegisterMachineResponse) {
- option(google.api.http) = {
- post : "/api/v1/machine/register"
+ rpc RegisterMachine(RegisterMachineRequest) returns (RegisterMachineResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/machine/register"
};
}
- rpc DeleteMachine(DeleteMachineRequest) returns(DeleteMachineResponse) {
- option(google.api.http) = {
- delete : "/api/v1/machine/{machine_id}"
+ rpc DeleteMachine(DeleteMachineRequest) returns (DeleteMachineResponse) {
+ option (google.api.http) = {
+ delete: "/api/v1/machine/{machine_id}"
};
}
- rpc ListMachines(ListMachinesRequest) returns(ListMachinesResponse) {
- option(google.api.http) = {
- get : "/api/v1/machine"
+ rpc ListMachines(ListMachinesRequest) returns (ListMachinesResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/machine"
};
}
- rpc ShareMachine(ShareMachineRequest) returns(ShareMachineResponse) {
- option(google.api.http) = {
- post : "/api/v1/machine/{machine_id}/share/{namespace}"
+ rpc ShareMachine(ShareMachineRequest) returns (ShareMachineResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/machine/{machine_id}/share/{namespace}"
};
}
- rpc UnshareMachine(UnshareMachineRequest) returns(UnshareMachineResponse) {
- option(google.api.http) = {
- post : "/api/v1/machine/{machine_id}/unshare/{namespace}"
+ rpc UnshareMachine(UnshareMachineRequest) returns (UnshareMachineResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/machine/{machine_id}/unshare/{namespace}"
};
}
// --- Machine end ---
// --- Route start ---
- rpc GetMachineRoute(GetMachineRouteRequest) returns(GetMachineRouteResponse) {
- option(google.api.http) = {
- get : "/api/v1/machine/{machine_id}/routes"
+ rpc GetMachineRoute(GetMachineRouteRequest) returns (GetMachineRouteResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/machine/{machine_id}/routes"
};
}
- rpc EnableMachineRoutes(EnableMachineRoutesRequest) returns(EnableMachineRoutesResponse) {
- option(google.api.http) = {
- post : "/api/v1/machine/{machine_id}/routes"
+ rpc EnableMachineRoutes(EnableMachineRoutesRequest) returns (EnableMachineRoutesResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/machine/{machine_id}/routes"
};
}
// --- Route end ---
diff --git a/routes.go b/routes.go
index f07b709a..448095a5 100644
--- a/routes.go
+++ b/routes.go
@@ -2,38 +2,48 @@ package headscale
import (
"encoding/json"
- "fmt"
"gorm.io/datatypes"
"inet.af/netaddr"
)
+const (
+ errRouteIsNotAvailable = Error("route is not available")
+)
+
// Deprecated: use machine function instead
// GetAdvertisedNodeRoutes returns the subnet routes advertised by a node (identified by
-// namespace and node name)
-func (h *Headscale) GetAdvertisedNodeRoutes(namespace string, nodeName string) (*[]netaddr.IPPrefix, error) {
- m, err := h.GetMachine(namespace, nodeName)
+// namespace and node name).
+func (h *Headscale) GetAdvertisedNodeRoutes(
+ namespace string,
+ nodeName string,
+) (*[]netaddr.IPPrefix, error) {
+ machine, err := h.GetMachine(namespace, nodeName)
if err != nil {
return nil, err
}
- hostInfo, err := m.GetHostInfo()
+ hostInfo, err := machine.GetHostInfo()
if err != nil {
return nil, err
}
+
return &hostInfo.RoutableIPs, nil
}
// Deprecated: use machine function instead
// GetEnabledNodeRoutes returns the subnet routes enabled by a node (identified by
-// namespace and node name)
-func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]netaddr.IPPrefix, error) {
- m, err := h.GetMachine(namespace, nodeName)
+// namespace and node name).
+func (h *Headscale) GetEnabledNodeRoutes(
+ namespace string,
+ nodeName string,
+) ([]netaddr.IPPrefix, error) {
+ machine, err := h.GetMachine(namespace, nodeName)
if err != nil {
return nil, err
}
- data, err := m.EnabledRoutes.MarshalJSON()
+ data, err := machine.EnabledRoutes.MarshalJSON()
if err != nil {
return nil, err
}
@@ -57,8 +67,12 @@ func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]n
}
// Deprecated: use machine function instead
-// IsNodeRouteEnabled checks if a certain route has been enabled
-func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool {
+// IsNodeRouteEnabled checks if a certain route has been enabled.
+func (h *Headscale) IsNodeRouteEnabled(
+ namespace string,
+ nodeName string,
+ routeStr string,
+) bool {
route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil {
return false
@@ -74,14 +88,19 @@ func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeS
return true
}
}
+
return false
}
// Deprecated: use EnableRoute in machine.go
// EnableNodeRoute enables a subnet route advertised by a node (identified by
-// namespace and node name)
-func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) error {
- m, err := h.GetMachine(namespace, nodeName)
+// namespace and node name).
+func (h *Headscale) EnableNodeRoute(
+ namespace string,
+ nodeName string,
+ routeStr string,
+) error {
+ machine, err := h.GetMachine(namespace, nodeName)
if err != nil {
return err
}
@@ -113,7 +132,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
}
if !available {
- return fmt.Errorf("route (%s) is not available on node %s", nodeName, routeStr)
+ return errRouteIsNotAvailable
}
routes, err := json.Marshal(enabledRoutes)
@@ -121,10 +140,10 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
return err
}
- m.EnabledRoutes = datatypes.JSON(routes)
- h.db.Save(&m)
+ machine.EnabledRoutes = datatypes.JSON(routes)
+ h.db.Save(&machine)
- err = h.RequestMapUpdates(m.NamespaceID)
+ err = h.RequestMapUpdates(machine.NamespaceID)
if err != nil {
return err
}
diff --git a/routes_test.go b/routes_test.go
index ad16d21c..18cb0ceb 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -10,57 +10,60 @@ import (
)
func (s *Suite) TestGetRoutes(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "test_get_route_machine")
+ _, err = app.GetMachine("test", "test_get_route_machine")
c.Assert(err, check.NotNil)
route, err := netaddr.ParseIPPrefix("10.0.0.0/24")
c.Assert(err, check.IsNil)
- hi := tailcfg.Hostinfo{
+ hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route},
}
- hostinfo, err := json.Marshal(hi)
+ hostinfo, err := json.Marshal(hostInfo)
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "test_get_route_machine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostinfo),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- r, err := h.GetAdvertisedNodeRoutes("test", "test_get_route_machine")
+ advertisedRoutes, err := app.GetAdvertisedNodeRoutes(
+ "test",
+ "test_get_route_machine",
+ )
c.Assert(err, check.IsNil)
- c.Assert(len(*r), check.Equals, 1)
+ c.Assert(len(*advertisedRoutes), check.Equals, 1)
- err = h.EnableNodeRoute("test", "test_get_route_machine", "192.168.0.0/24")
+ err = app.EnableNodeRoute("test", "test_get_route_machine", "192.168.0.0/24")
c.Assert(err, check.NotNil)
- err = h.EnableNodeRoute("test", "test_get_route_machine", "10.0.0.0/24")
+ err = app.EnableNodeRoute("test", "test_get_route_machine", "10.0.0.0/24")
c.Assert(err, check.IsNil)
}
func (s *Suite) TestGetEnableRoutes(c *check.C) {
- n, err := h.CreateNamespace("test")
+ namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "test_enable_route_machine")
+ _, err = app.GetMachine("test", "test_enable_route_machine")
c.Assert(err, check.NotNil)
route, err := netaddr.ParseIPPrefix(
@@ -73,56 +76,68 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
)
c.Assert(err, check.IsNil)
- hi := tailcfg.Hostinfo{
+ hostInfo := tailcfg.Hostinfo{
RoutableIPs: []netaddr.IPPrefix{route, route2},
}
- hostinfo, err := json.Marshal(hi)
+ hostinfo, err := json.Marshal(hostInfo)
c.Assert(err, check.IsNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "test_enable_route_machine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
HostInfo: datatypes.JSON(hostinfo),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- availableRoutes, err := h.GetAdvertisedNodeRoutes("test", "test_enable_route_machine")
+ availableRoutes, err := app.GetAdvertisedNodeRoutes(
+ "test",
+ "test_enable_route_machine",
+ )
c.Assert(err, check.IsNil)
c.Assert(len(*availableRoutes), check.Equals, 2)
- enabledRoutes, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
+ noEnabledRoutes, err := app.GetEnabledNodeRoutes(
+ "test",
+ "test_enable_route_machine",
+ )
c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes), check.Equals, 0)
+ c.Assert(len(noEnabledRoutes), check.Equals, 0)
- err = h.EnableNodeRoute("test", "test_enable_route_machine", "192.168.0.0/24")
+ err = app.EnableNodeRoute("test", "test_enable_route_machine", "192.168.0.0/24")
c.Assert(err, check.NotNil)
- err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
+ err = app.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
c.Assert(err, check.IsNil)
- enabledRoutes1, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
+ enabledRoutes, err := app.GetEnabledNodeRoutes("test", "test_enable_route_machine")
c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes1), check.Equals, 1)
+ c.Assert(len(enabledRoutes), check.Equals, 1)
// Adding it twice will just let it pass through
- err = h.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
+ err = app.EnableNodeRoute("test", "test_enable_route_machine", "10.0.0.0/24")
c.Assert(err, check.IsNil)
- enabledRoutes2, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
+ enableRoutesAfterDoubleApply, err := app.GetEnabledNodeRoutes(
+ "test",
+ "test_enable_route_machine",
+ )
c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes2), check.Equals, 1)
+ c.Assert(len(enableRoutesAfterDoubleApply), check.Equals, 1)
- err = h.EnableNodeRoute("test", "test_enable_route_machine", "150.0.10.0/25")
+ err = app.EnableNodeRoute("test", "test_enable_route_machine", "150.0.10.0/25")
c.Assert(err, check.IsNil)
- enabledRoutes3, err := h.GetEnabledNodeRoutes("test", "test_enable_route_machine")
+ enabledRoutesWithAdditionalRoute, err := app.GetEnabledNodeRoutes(
+ "test",
+ "test_enable_route_machine",
+ )
c.Assert(err, check.IsNil)
- c.Assert(len(enabledRoutes3), check.Equals, 2)
+ c.Assert(len(enabledRoutesWithAdditionalRoute), check.Equals, 2)
}
diff --git a/sharing.go b/sharing.go
index 5f6a8f45..be1689d5 100644
--- a/sharing.go
+++ b/sharing.go
@@ -2,11 +2,13 @@ package headscale
import "gorm.io/gorm"
-const errorSameNamespace = Error("Destination namespace same as origin")
-const errorMachineAlreadyShared = Error("Node already shared to this namespace")
-const errorMachineNotShared = Error("Machine not shared to this namespace")
+const (
+ errSameNamespace = Error("Destination namespace same as origin")
+ errMachineAlreadyShared = Error("Node already shared to this namespace")
+ errMachineNotShared = Error("Machine not shared to this namespace")
+)
-// SharedMachine is a join table to support sharing nodes between namespaces
+// SharedMachine is a join table to support sharing nodes between namespaces.
type SharedMachine struct {
gorm.Model
MachineID uint64
@@ -15,49 +17,57 @@ type SharedMachine struct {
Namespace Namespace
}
-// AddSharedMachineToNamespace adds a machine as a shared node to a namespace
-func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error {
- if m.NamespaceID == ns.ID {
- return errorSameNamespace
+// AddSharedMachineToNamespace adds a machine as a shared node to a namespace.
+func (h *Headscale) AddSharedMachineToNamespace(
+ machine *Machine,
+ namespace *Namespace,
+) error {
+ if machine.NamespaceID == namespace.ID {
+ return errSameNamespace
}
sharedMachines := []SharedMachine{}
- if err := h.db.Where("machine_id = ? AND namespace_id = ?", m.ID, ns.ID).Find(&sharedMachines).Error; err != nil {
+ if err := h.db.Where("machine_id = ? AND namespace_id = ?", machine.ID, namespace.ID).Find(&sharedMachines).Error; err != nil {
return err
}
if len(sharedMachines) > 0 {
- return errorMachineAlreadyShared
+ return errMachineAlreadyShared
}
sharedMachine := SharedMachine{
- MachineID: m.ID,
- Machine: *m,
- NamespaceID: ns.ID,
- Namespace: *ns,
+ MachineID: machine.ID,
+ Machine: *machine,
+ NamespaceID: namespace.ID,
+ Namespace: *namespace,
}
h.db.Save(&sharedMachine)
return nil
}
-// RemoveSharedMachineFromNamespace removes a shared machine from a namespace
-func (h *Headscale) RemoveSharedMachineFromNamespace(m *Machine, ns *Namespace) error {
- if m.NamespaceID == ns.ID {
+// RemoveSharedMachineFromNamespace removes a shared machine from a namespace.
+func (h *Headscale) RemoveSharedMachineFromNamespace(
+ machine *Machine,
+ namespace *Namespace,
+) error {
+ if machine.NamespaceID == namespace.ID {
// Can't unshare from primary namespace
- return errorMachineNotShared
+ return errMachineNotShared
}
sharedMachine := SharedMachine{}
- result := h.db.Where("machine_id = ? AND namespace_id = ?", m.ID, ns.ID).Unscoped().Delete(&sharedMachine)
+ result := h.db.Where("machine_id = ? AND namespace_id = ?", machine.ID, namespace.ID).
+ Unscoped().
+ Delete(&sharedMachine)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
- return errorMachineNotShared
+ return errMachineNotShared
}
- err := h.RequestMapUpdates(ns.ID)
+ err := h.RequestMapUpdates(namespace.ID)
if err != nil {
return err
}
@@ -65,10 +75,10 @@ func (h *Headscale) RemoveSharedMachineFromNamespace(m *Machine, ns *Namespace)
return nil
}
-// RemoveSharedMachineFromAllNamespaces removes a machine as a shared node from all namespaces
-func (h *Headscale) RemoveSharedMachineFromAllNamespaces(m *Machine) error {
+// RemoveSharedMachineFromAllNamespaces removes a machine as a shared node from all namespaces.
+func (h *Headscale) RemoveSharedMachineFromAllNamespaces(machine *Machine) error {
sharedMachine := SharedMachine{}
- if result := h.db.Where("machine_id = ?", m.ID).Unscoped().Delete(&sharedMachine); result.Error != nil {
+ if result := h.db.Where("machine_id = ?", machine.ID).Unscoped().Delete(&sharedMachine); result.Error != nil {
return result.Error
}
diff --git a/sharing_test.go b/sharing_test.go
index 4d9e4092..7ec1b0ee 100644
--- a/sharing_test.go
+++ b/sharing_test.go
@@ -4,45 +4,48 @@ import (
"gopkg.in/check.v1"
)
-func CreateNodeNamespace(c *check.C, namespace, node, key, IP string) (*Namespace, *Machine) {
- n1, err := h.CreateNamespace(namespace)
+func CreateNodeNamespace(
+ c *check.C,
+ namespaceName, node, key, ip string,
+) (*Namespace, *Machine) {
+ namespace, err := app.CreateNamespace(namespaceName)
c.Assert(err, check.IsNil)
- pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
+ pak1, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine(n1.Name, node)
+ _, err = app.GetMachine(namespace.Name, node)
c.Assert(err, check.NotNil)
- m1 := &Machine{
+ machine := &Machine{
ID: 0,
MachineKey: key,
NodeKey: key,
DiscoKey: key,
Name: node,
- NamespaceID: n1.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
- IPAddress: IP,
+ IPAddress: ip,
AuthKeyID: uint(pak1.ID),
}
- h.db.Save(m1)
+ app.db.Save(machine)
- _, err = h.GetMachine(n1.Name, m1.Name)
+ _, err = app.GetMachine(namespace.Name, machine.Name)
c.Assert(err, check.IsNil)
- return n1, m1
+ return namespace, machine
}
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_get_shared_nodes_2",
@@ -50,21 +53,21 @@ func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
"100.64.0.2",
)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShared, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShared), check.Equals, 0)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1sAfter, err := h.getPeers(m1)
+ peersOfMachine1AfterShared, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1sAfter), check.Equals, 1)
- c.Assert(p1sAfter[0].ID, check.Equals, m2.ID)
+ c.Assert(len(peersOfMachine1AfterShared), check.Equals, 1)
+ c.Assert(peersOfMachine1AfterShared[0].ID, check.Equals, machine2.ID)
}
func (s *Suite) TestSameNamespace(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
@@ -72,23 +75,23 @@ func (s *Suite) TestSameNamespace(c *check.C) {
"100.64.0.1",
)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
- err = h.AddSharedMachineToNamespace(m1, n1)
- c.Assert(err, check.Equals, errorSameNamespace)
+ err = app.AddSharedMachineToNamespace(machine1, namespace1)
+ c.Assert(err, check.Equals, errSameNamespace)
}
func (s *Suite) TestUnshare(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_unshare_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_unshare_2",
@@ -96,40 +99,40 @@ func (s *Suite) TestUnshare(c *check.C) {
"100.64.0.2",
)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1s, err = h.getShared(m1)
+ peersOfMachine1BeforeShare, err = app.getShared(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 1)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1)
- err = h.RemoveSharedMachineFromNamespace(m2, n1)
+ err = app.RemoveSharedMachineFromNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1s, err = h.getShared(m1)
+ peersOfMachine1BeforeShare, err = app.getShared(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
- err = h.RemoveSharedMachineFromNamespace(m2, n1)
- c.Assert(err, check.Equals, errorMachineNotShared)
+ err = app.RemoveSharedMachineFromNamespace(machine2, namespace1)
+ c.Assert(err, check.Equals, errMachineNotShared)
- err = h.RemoveSharedMachineFromNamespace(m1, n1)
- c.Assert(err, check.Equals, errorMachineNotShared)
+ err = app.RemoveSharedMachineFromNamespace(machine1, namespace1)
+ c.Assert(err, check.Equals, errMachineNotShared)
}
func (s *Suite) TestAlreadyShared(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_get_shared_nodes_2",
@@ -137,25 +140,25 @@ func (s *Suite) TestAlreadyShared(c *check.C) {
"100.64.0.2",
)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- err = h.AddSharedMachineToNamespace(m2, n1)
- c.Assert(err, check.Equals, errorMachineAlreadyShared)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
+ c.Assert(err, check.Equals, errMachineAlreadyShared)
}
func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_get_shared_nodes_2",
@@ -163,35 +166,35 @@ func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
"100.64.0.2",
)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 0)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 0)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1sAfter, err := h.getPeers(m1)
+ peersOfMachine1AfterShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1sAfter), check.Equals, 1)
- c.Assert(p1sAfter[0].Name, check.Equals, "test_get_shared_nodes_2")
+ c.Assert(len(peersOfMachine1AfterShare), check.Equals, 1)
+ c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, "test_get_shared_nodes_2")
}
func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_get_shared_nodes_2",
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
"100.64.0.2",
)
- _, m3 := CreateNodeNamespace(
+ _, machine3 := CreateNodeNamespace(
c,
"shared3",
"test_get_shared_nodes_3",
@@ -199,76 +202,80 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
"100.64.0.3",
)
- pak4, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
+ pak4, err := app.CreatePreAuthKey(namespace1.Name, false, false, nil)
c.Assert(err, check.IsNil)
- m4 := &Machine{
+ machine4 := &Machine{
ID: 4,
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
Name: "test_get_shared_nodes_4",
- NamespaceID: n1.ID,
+ NamespaceID: namespace1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.4",
AuthKeyID: uint(pak4.ID),
}
- h.db.Save(m4)
+ app.db.Save(machine4)
- _, err = h.GetMachine(n1.Name, m4.Name)
+ _, err = app.GetMachine(namespace1.Name, machine4.Name)
c.Assert(err, check.IsNil)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 1) // node1 can see node4
- c.Assert(p1s[0].Name, check.Equals, m4.Name)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1) // node1 can see node4
+ c.Assert(peersOfMachine1BeforeShare[0].Name, check.Equals, machine4.Name)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1sAfter, err := h.getPeers(m1)
- c.Assert(err, check.IsNil)
- c.Assert(len(p1sAfter), check.Equals, 2) // node1 can see node2 (shared) and node4 (same namespace)
- c.Assert(p1sAfter[0].Name, check.Equals, m2.Name)
- c.Assert(p1sAfter[1].Name, check.Equals, m4.Name)
-
- node1shared, err := h.getShared(m1)
- c.Assert(err, check.IsNil)
- c.Assert(len(node1shared), check.Equals, 1) // node1 can see node2 as shared
- c.Assert(node1shared[0].Name, check.Equals, m2.Name)
-
- pAlone, err := h.getPeers(m3)
- c.Assert(err, check.IsNil)
- c.Assert(len(pAlone), check.Equals, 0) // node3 is alone
-
- pSharedTo, err := h.getPeers(m2)
+ peersOfMachine1AfterShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
c.Assert(
- len(pSharedTo),
+ len(peersOfMachine1AfterShare),
+ check.Equals,
+ 2,
+ ) // node1 can see node2 (shared) and node4 (same namespace)
+ c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, machine2.Name)
+ c.Assert(peersOfMachine1AfterShare[1].Name, check.Equals, machine4.Name)
+
+ sharedOfMachine1, err := app.getShared(machine1)
+ c.Assert(err, check.IsNil)
+ c.Assert(len(sharedOfMachine1), check.Equals, 1) // node1 can see node2 as shared
+ c.Assert(sharedOfMachine1[0].Name, check.Equals, machine2.Name)
+
+ peersOfMachine3, err := app.getPeers(machine3)
+ c.Assert(err, check.IsNil)
+ c.Assert(len(peersOfMachine3), check.Equals, 0) // node3 is alone
+
+ peersOfMachine2, err := app.getPeers(machine2)
+ c.Assert(err, check.IsNil)
+ c.Assert(
+ len(peersOfMachine2),
check.Equals,
2,
) // node2 should see node1 (sharedTo) and node4 (sharedTo), as is shared in namespace1
- c.Assert(pSharedTo[0].Name, check.Equals, m1.Name)
- c.Assert(pSharedTo[1].Name, check.Equals, m4.Name)
+ c.Assert(peersOfMachine2[0].Name, check.Equals, machine1.Name)
+ c.Assert(peersOfMachine2[1].Name, check.Equals, machine4.Name)
}
func (s *Suite) TestDeleteSharedMachine(c *check.C) {
- n1, m1 := CreateNodeNamespace(
+ namespace1, machine1 := CreateNodeNamespace(
c,
"shared1",
"test_get_shared_nodes_1",
"686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
"100.64.0.1",
)
- _, m2 := CreateNodeNamespace(
+ _, machine2 := CreateNodeNamespace(
c,
"shared2",
"test_get_shared_nodes_2",
"dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
"100.64.0.2",
)
- _, m3 := CreateNodeNamespace(
+ _, machine3 := CreateNodeNamespace(
c,
"shared3",
"test_get_shared_nodes_3",
@@ -276,56 +283,58 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) {
"100.64.0.3",
)
- pak4n1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
+ pak4n1, err := app.CreatePreAuthKey(namespace1.Name, false, false, nil)
c.Assert(err, check.IsNil)
- m4 := &Machine{
+ machine4 := &Machine{
ID: 4,
MachineKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
NodeKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
DiscoKey: "4c3e07c3ecd40e9c945bb6797557c451850691c0409740578325e17009dd298f",
Name: "test_get_shared_nodes_4",
- NamespaceID: n1.ID,
+ NamespaceID: namespace1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.4",
AuthKeyID: uint(pak4n1.ID),
}
- h.db.Save(m4)
+ app.db.Save(machine4)
- _, err = h.GetMachine(n1.Name, m4.Name)
+ _, err = app.GetMachine(namespace1.Name, machine4.Name)
c.Assert(err, check.IsNil)
- p1s, err := h.getPeers(m1)
+ peersOfMachine1BeforeShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1s), check.Equals, 1) // nodes 1 and 4
- c.Assert(p1s[0].Name, check.Equals, m4.Name)
+ c.Assert(len(peersOfMachine1BeforeShare), check.Equals, 1) // nodes 1 and 4
+ c.Assert(peersOfMachine1BeforeShare[0].Name, check.Equals, machine4.Name)
- err = h.AddSharedMachineToNamespace(m2, n1)
+ err = app.AddSharedMachineToNamespace(machine2, namespace1)
c.Assert(err, check.IsNil)
- p1sAfter, err := h.getPeers(m1)
+ peersOfMachine1AfterShare, err := app.getPeers(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(p1sAfter), check.Equals, 2) // nodes 1, 2, 4
- c.Assert(p1sAfter[0].Name, check.Equals, m2.Name)
- c.Assert(p1sAfter[1].Name, check.Equals, m4.Name)
+ c.Assert(len(peersOfMachine1AfterShare), check.Equals, 2) // nodes 1, 2, 4
+ c.Assert(peersOfMachine1AfterShare[0].Name, check.Equals, machine2.Name)
+ c.Assert(peersOfMachine1AfterShare[1].Name, check.Equals, machine4.Name)
- node1shared, err := h.getShared(m1)
+ sharedOfMachine1, err := app.getShared(machine1)
c.Assert(err, check.IsNil)
- c.Assert(len(node1shared), check.Equals, 1) // nodes 1, 2, 4
- c.Assert(node1shared[0].Name, check.Equals, m2.Name)
+ c.Assert(len(sharedOfMachine1), check.Equals, 1) // nodes 1, 2, 4
+ c.Assert(sharedOfMachine1[0].Name, check.Equals, machine2.Name)
- pAlone, err := h.getPeers(m3)
+ peersOfMachine3, err := app.getPeers(machine3)
c.Assert(err, check.IsNil)
- c.Assert(len(pAlone), check.Equals, 0) // node 3 is alone
+ c.Assert(len(peersOfMachine3), check.Equals, 0) // node 3 is alone
- sharedMachines, err := h.ListSharedMachinesInNamespace(n1.Name)
+ sharedMachinesInNamespace1, err := app.ListSharedMachinesInNamespace(
+ namespace1.Name,
+ )
c.Assert(err, check.IsNil)
- c.Assert(len(sharedMachines), check.Equals, 1)
+ c.Assert(len(sharedMachinesInNamespace1), check.Equals, 1)
- err = h.DeleteMachine(m2)
+ err = app.DeleteMachine(machine2)
c.Assert(err, check.IsNil)
- sharedMachines, err = h.ListSharedMachinesInNamespace(n1.Name)
+ sharedMachinesInNamespace1, err = app.ListSharedMachinesInNamespace(namespace1.Name)
c.Assert(err, check.IsNil)
- c.Assert(len(sharedMachines), check.Equals, 0)
+ c.Assert(len(sharedMachinesInNamespace1), check.Equals, 0)
}
diff --git a/swagger.go b/swagger.go
index 17f57697..9e62d393 100644
--- a/swagger.go
+++ b/swagger.go
@@ -6,16 +6,15 @@ import (
"net/http"
"text/template"
- "github.com/rs/zerolog/log"
-
"github.com/gin-gonic/gin"
+ "github.com/rs/zerolog/log"
)
//go:embed gen/openapiv2/headscale/v1/headscale.swagger.json
var apiV1JSON []byte
-func SwaggerUI(c *gin.Context) {
- t := template.Must(template.New("swagger").Parse(`
+func SwaggerUI(ctx *gin.Context) {
+ swaggerTemplate := template.Must(template.New("swagger").Parse(`
@@ -48,18 +47,23 @@ func SwaggerUI(c *gin.Context) {
`))
var payload bytes.Buffer
- if err := t.Execute(&payload, struct{}{}); err != nil {
+ if err := swaggerTemplate.Execute(&payload, struct{}{}); err != nil {
log.Error().
Caller().
Err(err).
Msg("Could not render Swagger")
- c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte("Could not render Swagger"))
+ ctx.Data(
+ http.StatusInternalServerError,
+ "text/html; charset=utf-8",
+ []byte("Could not render Swagger"),
+ )
+
return
}
- c.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
+ ctx.Data(http.StatusOK, "text/html; charset=utf-8", payload.Bytes())
}
-func SwaggerAPIv1(c *gin.Context) {
- c.Data(http.StatusOK, "application/json; charset=utf-8", apiV1JSON)
+func SwaggerAPIv1(ctx *gin.Context) {
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", apiV1JSON)
}
diff --git a/utils.go b/utils.go
index ad0a72d7..9f7849e2 100644
--- a/utils.go
+++ b/utils.go
@@ -20,31 +20,48 @@ import (
"tailscale.com/types/wgkey"
)
+const (
+ errCannotDecryptReponse = Error("cannot decrypt response")
+ errResponseMissingNonce = Error("response missing nonce")
+ errCouldNotAllocateIP = Error("could not find any suitable IP")
+)
+
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
type Error string
func (e Error) Error() string { return string(e) }
-func decode(msg []byte, v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) error {
+func decode(
+ msg []byte,
+ v interface{},
+ pubKey *wgkey.Key,
+ privKey *wgkey.Private,
+) error {
return decodeMsg(msg, v, pubKey, privKey)
}
-func decodeMsg(msg []byte, v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) error {
+func decodeMsg(
+ msg []byte,
+ output interface{},
+ pubKey *wgkey.Key,
+ privKey *wgkey.Private,
+) error {
decrypted, err := decryptMsg(msg, pubKey, privKey)
if err != nil {
return err
}
// fmt.Println(string(decrypted))
- if err := json.Unmarshal(decrypted, v); err != nil {
- return fmt.Errorf("response: %v", err)
+ if err := json.Unmarshal(decrypted, output); err != nil {
+ return err
}
+
return nil
}
func decryptMsg(msg []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, error) {
var nonce [24]byte
if len(msg) < len(nonce)+1 {
- return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
+ return nil, errResponseMissingNonce
}
copy(nonce[:], msg)
msg = msg[len(nonce):]
@@ -52,8 +69,9 @@ func decryptMsg(msg []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte,
pub, pri := (*[32]byte)(pubKey), (*[32]byte)(privKey)
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
if !ok {
- return nil, fmt.Errorf("cannot decrypt response")
+ return nil, errCannotDecryptReponse
}
+
return decrypted, nil
}
@@ -66,13 +84,18 @@ func encode(v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, e
return encodeMsg(b, pubKey, privKey)
}
-func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, error) {
+func encodeMsg(
+ payload []byte,
+ pubKey *wgkey.Key,
+ privKey *wgkey.Private,
+) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
pub, pri := (*[32]byte)(pubKey), (*[32]byte)(privKey)
- msg := box.Seal(nonce[:], b, &nonce, pub, pri)
+ msg := box.Seal(nonce[:], payload, &nonce, pub, pri)
+
return msg, nil
}
@@ -89,7 +112,7 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) {
for {
if !ipPrefix.Contains(ip) {
- return nil, fmt.Errorf("could not find any suitable IP in %s", ipPrefix)
+ return nil, errCouldNotAllocateIP
}
// Some OS (including Linux) does not like when IPs ends with 0 or 255, which
@@ -98,13 +121,14 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) {
ipRaw := ip.As4()
if ipRaw[3] == 0 || ipRaw[3] == 255 {
ip = ip.Next()
+
continue
}
if ip.IsZero() &&
ip.IsLoopback() {
-
ip = ip.Next()
+
continue
}
@@ -125,7 +149,7 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
if addr != "" {
ip, err := netaddr.ParseIP(addr)
if err != nil {
- return nil, fmt.Errorf("failed to parse ip from database, %w", err)
+ return nil, fmt.Errorf("failed to parse ip from database: %w", err)
}
ips[index] = ip
@@ -156,11 +180,16 @@ func tailNodesToString(nodes []*tailcfg.Node) string {
}
func tailMapResponseToString(resp tailcfg.MapResponse) string {
- return fmt.Sprintf("{ Node: %s, Peers: %s }", resp.Node.Name, tailNodesToString(resp.Peers))
+ return fmt.Sprintf(
+ "{ Node: %s, Peers: %s }",
+ resp.Node.Name,
+ tailNodesToString(resp.Peers),
+ )
}
func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer
+
return d.DialContext(ctx, "unix", addr)
}
@@ -174,7 +203,7 @@ func ipPrefixToString(prefixes []netaddr.IPPrefix) []string {
return result
}
-func stringToIpPrefix(prefixes []string) ([]netaddr.IPPrefix, error) {
+func stringToIPPrefix(prefixes []string) ([]netaddr.IPPrefix, error) {
result := make([]netaddr.IPPrefix, len(prefixes))
for index, prefixStr := range prefixes {
@@ -189,7 +218,7 @@ func stringToIpPrefix(prefixes []string) ([]netaddr.IPPrefix, error) {
return result, nil
}
-func containsIpPrefix(prefixes []netaddr.IPPrefix, prefix netaddr.IPPrefix) bool {
+func containsIPPrefix(prefixes []netaddr.IPPrefix, prefix netaddr.IPPrefix) bool {
for _, p := range prefixes {
if prefix == p {
return true
diff --git a/utils_test.go b/utils_test.go
index f50cd117..dcda6130 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -6,7 +6,7 @@ import (
)
func (s *Suite) TestGetAvailableIp(c *check.C) {
- ip, err := h.getAvailableIP()
+ ip, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
@@ -16,33 +16,33 @@ func (s *Suite) TestGetAvailableIp(c *check.C) {
}
func (s *Suite) TestGetUsedIps(c *check.C) {
- ip, err := h.getAvailableIP()
+ ip, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
- n, err := h.CreateNamespace("test_ip")
+ namespace, err := app.CreateNamespace("test_ip")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "testmachine")
+ _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
IPAddress: ip.String(),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- ips, err := h.getUsedIPs()
+ ips, err := app.getUsedIPs()
c.Assert(err, check.IsNil)
@@ -50,42 +50,42 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
c.Assert(ips[0], check.Equals, expected)
- m1, err := h.GetMachineByID(0)
+ machine1, err := app.GetMachineByID(0)
c.Assert(err, check.IsNil)
- c.Assert(m1.IPAddress, check.Equals, expected.String())
+ c.Assert(machine1.IPAddress, check.Equals, expected.String())
}
func (s *Suite) TestGetMultiIp(c *check.C) {
- n, err := h.CreateNamespace("test-ip-multi")
+ namespace, err := app.CreateNamespace("test-ip-multi")
c.Assert(err, check.IsNil)
- for i := 1; i <= 350; i++ {
- ip, err := h.getAvailableIP()
+ for index := 1; index <= 350; index++ {
+ ip, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "testmachine")
+ _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil)
- m := Machine{
- ID: uint64(i),
+ machine := Machine{
+ ID: uint64(index),
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
IPAddress: ip.String(),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
}
- ips, err := h.getUsedIPs()
+ ips, err := app.getUsedIPs()
c.Assert(err, check.IsNil)
@@ -96,59 +96,67 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47"))
// Check that we can read back the IPs
- m1, err := h.GetMachineByID(1)
+ machine1, err := app.GetMachineByID(1)
c.Assert(err, check.IsNil)
- c.Assert(m1.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.1").String())
+ c.Assert(
+ machine1.IPAddress,
+ check.Equals,
+ netaddr.MustParseIP("10.27.0.1").String(),
+ )
- m50, err := h.GetMachineByID(50)
+ machine50, err := app.GetMachineByID(50)
c.Assert(err, check.IsNil)
- c.Assert(m50.IPAddress, check.Equals, netaddr.MustParseIP("10.27.0.50").String())
+ c.Assert(
+ machine50.IPAddress,
+ check.Equals,
+ netaddr.MustParseIP("10.27.0.50").String(),
+ )
expectedNextIP := netaddr.MustParseIP("10.27.1.97")
- nextIP, err := h.getAvailableIP()
+ nextIP, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
c.Assert(nextIP.String(), check.Equals, expectedNextIP.String())
// If we call get Available again, we should receive
// the same IP, as it has not been reserved.
- nextIP2, err := h.getAvailableIP()
+ nextIP2, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String())
}
func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
- ip, err := h.getAvailableIP()
+ ip, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
expected := netaddr.MustParseIP("10.27.0.1")
c.Assert(ip.String(), check.Equals, expected.String())
- n, err := h.CreateNamespace("test_ip")
+ namespace, err := app.CreateNamespace("test_ip")
c.Assert(err, check.IsNil)
- pak, err := h.CreatePreAuthKey(n.Name, false, false, nil)
+ pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
- _, err = h.GetMachine("test", "testmachine")
+ _, err = app.GetMachine("test", "testmachine")
c.Assert(err, check.NotNil)
- m := Machine{
+ machine := Machine{
ID: 0,
MachineKey: "foo",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
- NamespaceID: n.ID,
+ NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: "authKey",
AuthKeyID: uint(pak.ID),
}
- h.db.Save(&m)
+ app.db.Save(&machine)
- ip2, err := h.getAvailableIP()
+ ip2, err := app.getAvailableIP()
c.Assert(err, check.IsNil)
c.Assert(ip2.String(), check.Equals, expected.String())