From cf3fc85196e13e52db055dbc594d437dcc9765ac Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 12 Jul 2022 12:27:28 +0200
Subject: [PATCH 01/18] Make tailnet updates check configurable

---
 config-example.yaml |  6 ++++++
 config.go           | 16 ++++++++++++++++
 poll.go             |  5 ++---
 3 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/config-example.yaml b/config-example.yaml
index 9740f3a..c32f941 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -103,6 +103,12 @@ disable_check_updates: false
 # Time before an inactive ephemeral node is deleted?
 ephemeral_node_inactivity_timeout: 30m
 
+# Period to check for changes in the tailnet. A value too low will severily affect
+# CPU consumption of Headscale. A value too high (over 60s) will cause problems
+# to the nodes, as they won't get updates or keep alive messages on time.
+# In case of doubts, do not touch the default 10s.
+changes_check_interval: 10s
+
 # SQLite config
 db_type: sqlite3
 db_path: /var/lib/headscale/db.sqlite
diff --git a/config.go b/config.go
index 9e71a75..0ef0911 100644
--- a/config.go
+++ b/config.go
@@ -26,6 +26,7 @@ type Config struct {
 	GRPCAddr                       string
 	GRPCAllowInsecure              bool
 	EphemeralNodeInactivityTimeout time.Duration
+	ChangesCheckInterval           time.Duration
 	IPPrefixes                     []netaddr.IPPrefix
 	PrivateKeyPath                 string
 	BaseDomain                     string
@@ -162,6 +163,8 @@ func LoadConfig(path string, isFile bool) error {
 
 	viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
 
+	viper.SetDefault("changes_check_interval", "10s")
+
 	if err := viper.ReadInConfig(); err != nil {
 		log.Warn().Err(err).Msg("Failed to read configuration from disk")
 
@@ -217,6 +220,15 @@ func LoadConfig(path string, isFile bool) error {
 		)
 	}
 
+	maxChangesCheckInterval, _ := time.ParseDuration("60s")
+	if viper.GetDuration("changes_check_interval") > maxChangesCheckInterval {
+		errorText += fmt.Sprintf(
+			"Fatal config error: changes_check_interval (%s) is set too high, must be less than %s",
+			viper.GetString("changes_check_interval"),
+			maxChangesCheckInterval,
+		)
+	}
+
 	if errorText != "" {
 		//nolint
 		return errors.New(strings.TrimSuffix(errorText, "\n"))
@@ -478,6 +490,10 @@ func GetHeadscaleConfig() (*Config, error) {
 			"ephemeral_node_inactivity_timeout",
 		),
 
+		ChangesCheckInterval: viper.GetDuration(
+			"changes_check_interval",
+		),
+
 		DBtype: viper.GetString("db_type"),
 		DBpath: AbsolutePathFromConfigPath(viper.GetString("db_path")),
 		DBhost: viper.GetString("db_host"),
diff --git a/poll.go b/poll.go
index 9218495..95fb542 100644
--- a/poll.go
+++ b/poll.go
@@ -16,8 +16,7 @@ import (
 )
 
 const (
-	keepAliveInterval   = 60 * time.Second
-	updateCheckInterval = 10 * time.Second
+	keepAliveInterval = 60 * time.Second
 )
 
 type contextKey string
@@ -640,7 +639,7 @@ func (h *Headscale) scheduledPollWorker(
 	machine *Machine,
 ) {
 	keepAliveTicker := time.NewTicker(keepAliveInterval)
-	updateCheckerTicker := time.NewTicker(updateCheckInterval)
+	updateCheckerTicker := time.NewTicker(h.cfg.ChangesCheckInterval)
 
 	defer closeChanWithLog(
 		updateChan,

From 8e0939f403147f7cb5c3f294e0df6ea6f76d1688 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 12 Jul 2022 12:33:42 +0200
Subject: [PATCH 02/18] Updated changelog

---
 CHANGELOG.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e91605..23064ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,11 +30,10 @@
 - Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285) [#612](https://github.com/juanfont/headscale/pull/601)
 - Add configuration option to allow Tailscale clients to use a random WireGuard port. [kb/1181/firewalls](https://tailscale.com/kb/1181/firewalls) [#624](https://github.com/juanfont/headscale/pull/624)
 - Improve obtuse UX regarding missing configuration (`ephemeral_node_inactivity_timeout` not set) [#639](https://github.com/juanfont/headscale/pull/639)
-- Fix nodes being shown as 'offline' in `tailscale status` [648](https://github.com/juanfont/headscale/pull/648)
 - Fix nodes being shown as 'offline' in `tailscale status` [#648](https://github.com/juanfont/headscale/pull/648)
 - Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
 - Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648)
-
+- Make tailnet updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
 
 ## 0.15.0 (2022-03-20)
 

From 5b5298b0255098a0653da985f3b412fc133cbb40 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 12 Jul 2022 12:52:03 +0200
Subject: [PATCH 03/18] Renamed config param for node update check internal

---
 CHANGELOG.md        |  2 +-
 config-example.yaml |  6 +++---
 config.go           | 18 +++++++++---------
 poll.go             |  2 +-
 4 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23064ac..1c63ae4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,7 +33,7 @@
 - Fix nodes being shown as 'offline' in `tailscale status` [#648](https://github.com/juanfont/headscale/pull/648)
 - Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
 - Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648)
-- Make tailnet updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
+- Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
 
 ## 0.15.0 (2022-03-20)
 
diff --git a/config-example.yaml b/config-example.yaml
index c32f941..d3d155e 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -103,11 +103,11 @@ disable_check_updates: false
 # Time before an inactive ephemeral node is deleted?
 ephemeral_node_inactivity_timeout: 30m
 
-# Period to check for changes in the tailnet. A value too low will severily affect
+# Period to check for node updates in the tailnet. A value too low will severily affect
 # CPU consumption of Headscale. A value too high (over 60s) will cause problems
-# to the nodes, as they won't get updates or keep alive messages on time.
+# to the nodes, as they won't get updates or keep alive messages in time.
 # In case of doubts, do not touch the default 10s.
-changes_check_interval: 10s
+node_update_check_interval: 10s
 
 # SQLite config
 db_type: sqlite3
diff --git a/config.go b/config.go
index 0ef0911..6789f6f 100644
--- a/config.go
+++ b/config.go
@@ -26,7 +26,7 @@ type Config struct {
 	GRPCAddr                       string
 	GRPCAllowInsecure              bool
 	EphemeralNodeInactivityTimeout time.Duration
-	ChangesCheckInterval           time.Duration
+	NodeUpdateCheckInterval        time.Duration
 	IPPrefixes                     []netaddr.IPPrefix
 	PrivateKeyPath                 string
 	BaseDomain                     string
@@ -163,7 +163,7 @@ func LoadConfig(path string, isFile bool) error {
 
 	viper.SetDefault("ephemeral_node_inactivity_timeout", "120s")
 
-	viper.SetDefault("changes_check_interval", "10s")
+	viper.SetDefault("node_update_check_interval", "10s")
 
 	if err := viper.ReadInConfig(); err != nil {
 		log.Warn().Err(err).Msg("Failed to read configuration from disk")
@@ -220,12 +220,12 @@ func LoadConfig(path string, isFile bool) error {
 		)
 	}
 
-	maxChangesCheckInterval, _ := time.ParseDuration("60s")
-	if viper.GetDuration("changes_check_interval") > maxChangesCheckInterval {
+	maxNodeUpdateCheckInterval, _ := time.ParseDuration("60s")
+	if viper.GetDuration("node_update_check_interval") > maxNodeUpdateCheckInterval {
 		errorText += fmt.Sprintf(
-			"Fatal config error: changes_check_interval (%s) is set too high, must be less than %s",
-			viper.GetString("changes_check_interval"),
-			maxChangesCheckInterval,
+			"Fatal config error: node_update_check_interval (%s) is set too high, must be less than %s",
+			viper.GetString("node_update_check_interval"),
+			maxNodeUpdateCheckInterval,
 		)
 	}
 
@@ -490,8 +490,8 @@ func GetHeadscaleConfig() (*Config, error) {
 			"ephemeral_node_inactivity_timeout",
 		),
 
-		ChangesCheckInterval: viper.GetDuration(
-			"changes_check_interval",
+		NodeUpdateCheckInterval: viper.GetDuration(
+			"node_update_check_interval",
 		),
 
 		DBtype: viper.GetString("db_type"),
diff --git a/poll.go b/poll.go
index 95fb542..6628a17 100644
--- a/poll.go
+++ b/poll.go
@@ -639,7 +639,7 @@ func (h *Headscale) scheduledPollWorker(
 	machine *Machine,
 ) {
 	keepAliveTicker := time.NewTicker(keepAliveInterval)
-	updateCheckerTicker := time.NewTicker(h.cfg.ChangesCheckInterval)
+	updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
 
 	defer closeChanWithLog(
 		updateChan,

From 4ccff8bf2891d36225983ad42d0b36f28b9b368a Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 12 Jul 2022 13:13:04 +0200
Subject: [PATCH 04/18] Added the new parameter to the integration test params

---
 integration_test/etc/alt-config.dump.gold.yaml | 1 +
 integration_test/etc/alt-config.yaml           | 1 +
 integration_test/etc/config.dump.gold.yaml     | 1 +
 integration_test/etc/config.yaml               | 1 +
 integration_test/etc_embedded_derp/config.yaml | 1 +
 5 files changed, 5 insertions(+)

diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml
index a3d7adb..e893423 100644
--- a/integration_test/etc/alt-config.dump.gold.yaml
+++ b/integration_test/etc/alt-config.dump.gold.yaml
@@ -20,6 +20,7 @@ dns_config:
   nameservers:
     - 1.1.1.1
 ephemeral_node_inactivity_timeout: 30m
+node_update_check_interval: 10s
 grpc_allow_insecure: false
 grpc_listen_addr: :50443
 ip_prefixes:
diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml
index 8de9a82..fa1bfcb 100644
--- a/integration_test/etc/alt-config.yaml
+++ b/integration_test/etc/alt-config.yaml
@@ -2,6 +2,7 @@ log_level: trace
 acl_policy_path: ""
 db_type: sqlite3
 ephemeral_node_inactivity_timeout: 30m
+node_update_check_interval: 10s
 ip_prefixes:
   - fd7a:115c:a1e0::/48
   - 100.64.0.0/10
diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml
index 4d03d74..17bb0ca 100644
--- a/integration_test/etc/config.dump.gold.yaml
+++ b/integration_test/etc/config.dump.gold.yaml
@@ -20,6 +20,7 @@ dns_config:
   nameservers:
     - 1.1.1.1
 ephemeral_node_inactivity_timeout: 30m
+node_update_check_interval: 10s
 grpc_allow_insecure: false
 grpc_listen_addr: :50443
 ip_prefixes:
diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml
index f055b4c..e6b34af 100644
--- a/integration_test/etc/config.yaml
+++ b/integration_test/etc/config.yaml
@@ -2,6 +2,7 @@ log_level: trace
 acl_policy_path: ""
 db_type: sqlite3
 ephemeral_node_inactivity_timeout: 30m
+node_update_check_interval: 10s
 ip_prefixes:
   - fd7a:115c:a1e0::/48
   - 100.64.0.0/10
diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index a8b57af..e6ad3b0 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -2,6 +2,7 @@ log_level: trace
 acl_policy_path: ""
 db_type: sqlite3
 ephemeral_node_inactivity_timeout: 30m
+node_update_check_interval: 10s
 ip_prefixes:
   - fd7a:115c:a1e0::/48
   - 100.64.0.0/10

From b8c3387892896e1bbc50b5db9deed5e30cb2216b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 12 Jul 2022 11:35:28 +0000
Subject: [PATCH 05/18] docs(README): update contributors

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 262d553..f00772e 100644
--- a/README.md
+++ b/README.md
@@ -572,9 +572,9 @@ make build
     </td>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/Bpazy>
-            <img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
+            <img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ziyuan Han/>
             <br />
-            <sub style="font-size:14px"><b>ZiYuan</b></sub>
+            <sub style="font-size:14px"><b>Ziyuan Han</b></sub>
         </a>
     </td>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">

From aca5646032da4bd3bfe163766926ce170c48815e Mon Sep 17 00:00:00 2001
From: Jiang Zhu <jiang.moe@outlook.com>
Date: Sat, 16 Jul 2022 02:03:46 +0800
Subject: [PATCH 06/18] remove gin completely, ~2MB reduction on final binary

---
 app.go | 17 ++++-------------
 go.mod | 11 -----------
 go.sum | 23 -----------------------
 3 files changed, 4 insertions(+), 47 deletions(-)

diff --git a/app.go b/app.go
index e4e6910..11c8d68 100644
--- a/app.go
+++ b/app.go
@@ -17,17 +17,16 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"github.com/gin-gonic/gin"
 	"github.com/gorilla/mux"
 	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/patrickmn/go-cache"
 	zerolog "github.com/philip-bui/grpc-zerolog"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/puzpuzpuz/xsync"
 	zl "github.com/rs/zerolog"
 	"github.com/rs/zerolog/log"
-	ginprometheus "github.com/zsais/go-gin-prometheus"
 	"golang.org/x/crypto/acme"
 	"golang.org/x/crypto/acme/autocert"
 	"golang.org/x/oauth2"
@@ -411,15 +410,6 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
 	return os.Remove(h.cfg.UnixSocket)
 }
 
-func (h *Headscale) createPrometheusRouter() *gin.Engine {
-	promRouter := gin.Default()
-
-	prometheus := ginprometheus.NewPrometheus("gin")
-	prometheus.Use(promRouter)
-
-	return promRouter
-}
-
 func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
 	router := mux.NewRouter()
 
@@ -647,11 +637,12 @@ func (h *Headscale) Serve() error {
 	log.Info().
 		Msgf("listening and serving HTTP on: %s", h.cfg.Addr)
 
-	promRouter := h.createPrometheusRouter()
+	promMux := http.NewServeMux()
+	promMux.Handle("/metrics", promhttp.Handler())
 
 	promHTTPServer := &http.Server{
 		Addr:         h.cfg.MetricsAddr,
-		Handler:      promRouter,
+		Handler:      promMux,
 		ReadTimeout:  HTTPReadTimeout,
 		WriteTimeout: 0,
 	}
diff --git a/go.mod b/go.mod
index e10ae35..80a3e48 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,6 @@ require (
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/deckarep/golang-set/v2 v2.1.0
 	github.com/efekarakus/termcolor v1.0.1
-	github.com/gin-gonic/gin v1.7.7
 	github.com/glebarez/sqlite v1.4.3
 	github.com/gofrs/uuid v4.2.0+incompatible
 	github.com/gorilla/mux v1.8.0
@@ -28,7 +27,6 @@ require (
 	github.com/stretchr/testify v1.7.1
 	github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
 	github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
-	github.com/zsais/go-gin-prometheus v0.1.0
 	golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
 	golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
@@ -61,11 +59,7 @@ require (
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.1 // indirect
-	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/glebarez/go-sqlite v1.16.0 // indirect
-	github.com/go-playground/locales v0.13.0 // indirect
-	github.com/go-playground/universal-translator v0.17.0 // indirect
-	github.com/go-playground/validator/v10 v10.4.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
@@ -90,11 +84,9 @@ require (
 	github.com/jinzhu/now v1.1.4 // indirect
 	github.com/josharian/native v1.0.0 // indirect
 	github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
-	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
-	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
@@ -106,8 +98,6 @@ require (
 	github.com/mitchellh/go-ps v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.4.3 // indirect
 	github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
 	github.com/opencontainers/runc v1.0.2 // indirect
@@ -126,7 +116,6 @@ require (
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
-	github.com/ugorji/go/codec v1.1.7 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index b4d03b9..3290d9c 100644
--- a/go.sum
+++ b/go.sum
@@ -236,10 +236,6 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5
 github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
 github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
-github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
 github.com/glebarez/go-sqlite v1.16.0 h1:h28rHued+hGof3fNLksBcLwz/a71fiGZ/eIJHK0SsLI=
 github.com/glebarez/go-sqlite v1.16.0/go.mod h1:i8/JtqoqzBAFkrUTxbQFkQ05odCOds3j7NlDaXjqiPY=
 github.com/glebarez/sqlite v1.4.3 h1:ZABNo+2YIau8F8sZ7Qh/1h/ZnlSUMHFGD4zJKPval7A=
@@ -256,14 +252,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
-github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
-github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
-github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -546,10 +534,8 @@ github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6k
 github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -595,8 +581,6 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
 github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
 github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0=
 github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88=
-github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
-github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
 github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -686,11 +670,9 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J
 github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
 github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
@@ -925,10 +907,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1
 github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY=
 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
 github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
 github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@@ -962,8 +941,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
-github.com/zsais/go-gin-prometheus v0.1.0 h1:bkLv1XCdzqVgQ36ScgRi09MA2UC1t3tAB6nsfErsGO4=
-github.com/zsais/go-gin-prometheus v0.1.0/go.mod h1:Slirjzuz8uM8Cw0jmPNqbneoqcUtY2GGjn2bEd4NRLY=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k=

From b755d4765258027584b505e03a606f65380ada82 Mon Sep 17 00:00:00 2001
From: Jiang Zhu <jiang.moe@outlook.com>
Date: Tue, 19 Jul 2022 20:45:23 +0800
Subject: [PATCH 07/18] update CHANGELOG

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c63ae4..e1ec1b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,7 +32,7 @@
 - Improve obtuse UX regarding missing configuration (`ephemeral_node_inactivity_timeout` not set) [#639](https://github.com/juanfont/headscale/pull/639)
 - Fix nodes being shown as 'offline' in `tailscale status` [#648](https://github.com/juanfont/headscale/pull/648)
 - Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
-- Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648)
+- Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648) [677](https://github.com/juanfont/headscale/pull/677)
 - Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
 
 ## 0.15.0 (2022-03-20)

From 5724f4607c0bc24ccb6ceca2e84870eed63f7d0e Mon Sep 17 00:00:00 2001
From: Jiang Zhu <jiang.moe@outlook.com>
Date: Tue, 19 Jul 2022 20:45:32 +0800
Subject: [PATCH 08/18] fix nix build

---
 flake.nix | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/flake.nix b/flake.nix
index afa8c8b..f9f3c83 100644
--- a/flake.nix
+++ b/flake.nix
@@ -24,7 +24,7 @@
 
               # When updating go.mod or go.sum, a new sha will need to be calculated,
               # update this if you have a mismatch after doing a change to thos files.
-              vendorSha256 = "sha256-T6rH+aqofFmCPxDfoA5xd3kNUJeZkT4GRyuFEnenps8=";
+              vendorSha256 = "sha256-b9C6F+7N0ecW0HiTx+rztZnxb+n6U6YTSOJvp3GqnWQ=";
 
               ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
             };

From 02c7a46b977462d47240de2a3a8a3476306c08e6 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 20 Jul 2022 07:21:19 +0000
Subject: [PATCH 09/18] docs(README): update contributors

---
 README.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index f00772e..38578c3 100644
--- a/README.md
+++ b/README.md
@@ -188,13 +188,6 @@ make build
             <sub style="font-size:14px"><b>Ward Vandewege</b></sub>
         </a>
     </td>
-    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
-        <a href=https://github.com/reynico>
-            <img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
-            <br />
-            <sub style="font-size:14px"><b>Nico</b></sub>
-        </a>
-    </td>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/huskyii>
             <img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
@@ -202,6 +195,13 @@ make build
             <sub style="font-size:14px"><b>Jiang Zhu</b></sub>
         </a>
     </td>
+    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
+        <a href=https://github.com/reynico>
+            <img src=https://avatars.githubusercontent.com/u/715768?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nico/>
+            <br />
+            <sub style="font-size:14px"><b>Nico</b></sub>
+        </a>
+    </td>
 </tr>
 <tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">

From 889eff265fdcaff348c406270666fa3067194559 Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Thu, 30 Jun 2022 23:35:22 +0200
Subject: [PATCH 10/18] graceful shutdown fix

---
 app.go  | 23 +++++++++++++++++------
 poll.go | 21 ++++++++++++---------
 2 files changed, 29 insertions(+), 15 deletions(-)

diff --git a/app.go b/app.go
index 11c8d68..de6ef66 100644
--- a/app.go
+++ b/app.go
@@ -95,6 +95,7 @@ type Headscale struct {
 	ipAllocationMutex sync.Mutex
 
 	shutdownChan chan struct{}
+	wg           sync.WaitGroup
 }
 
 // Look up the TLS constant relative to user-supplied TLS client
@@ -153,6 +154,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
 		privateKey:        privKey,
 		aclRules:          tailcfg.FilterAllowAll, // default allowall
 		registrationCache: registrationCache,
+		wg:                sync.WaitGroup{},
 	}
 
 	err = app.initDB()
@@ -567,6 +569,8 @@ func (h *Headscale) Serve() error {
 	// https://github.com/soheilhy/cmux/issues/68
 	// https://github.com/soheilhy/cmux/issues/91
 
+	var grpcServer *grpc.Server
+	var grpcListener net.Listener
 	if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
 		log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr)
 
@@ -587,12 +591,12 @@ func (h *Headscale) Serve() error {
 			log.Warn().Msg("gRPC is running without security")
 		}
 
-		grpcServer := grpc.NewServer(grpcOptions...)
+		grpcServer = grpc.NewServer(grpcOptions...)
 
 		v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
 		reflection.Register(grpcServer)
 
-		grpcListener, err := net.Listen("tcp", h.cfg.GRPCAddr)
+		grpcListener, err = net.Listen("tcp", h.cfg.GRPCAddr)
 		if err != nil {
 			return fmt.Errorf("failed to bind to TCP address: %w", err)
 		}
@@ -668,7 +672,7 @@ func (h *Headscale) Serve() error {
 		syscall.SIGTERM,
 		syscall.SIGQUIT,
 		syscall.SIGHUP)
-	go func(c chan os.Signal) {
+	sig_func := func(c chan os.Signal) {
 		// Wait for a SIGINT or SIGKILL:
 		for {
 			sig := <-c
@@ -678,7 +682,7 @@ func (h *Headscale) Serve() error {
 					Str("signal", sig.String()).
 					Msg("Received SIGHUP, reloading ACL and Config")
 
-					// TODO(kradalby): Reload config on SIGHUP
+				// TODO(kradalby): Reload config on SIGHUP
 
 				if h.cfg.ACL.PolicyPath != "" {
 					aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
@@ -698,7 +702,8 @@ func (h *Headscale) Serve() error {
 					Str("signal", sig.String()).
 					Msg("Received signal to stop, shutting down gracefully")
 
-				h.shutdownChan <- struct{}{}
+				close(h.shutdownChan)
+				h.wg.Wait()
 
 				// Gracefully shut down servers
 				ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
@@ -710,6 +715,11 @@ func (h *Headscale) Serve() error {
 				}
 				grpcSocket.GracefulStop()
 
+				if grpcServer != nil {
+					grpcServer.GracefulStop()
+					grpcListener.Close()
+				}
+
 				// Close network listeners
 				promHTTPListener.Close()
 				httpListener.Close()
@@ -736,7 +746,8 @@ func (h *Headscale) Serve() error {
 				os.Exit(0)
 			}
 		}
-	}(sigc)
+	}
+	errorGroup.Go(func() error { sig_func(sigc); return nil })
 
 	return errorGroup.Wait()
 }
diff --git a/poll.go b/poll.go
index 6628a17..94941aa 100644
--- a/poll.go
+++ b/poll.go
@@ -290,6 +290,9 @@ func (h *Headscale) PollNetMapStream(
 	keepAliveChan chan []byte,
 	updateChan chan struct{},
 ) {
+	h.wg.Add(1)
+	defer h.wg.Done()
+
 	ctx := context.WithValue(req.Context(), machineNameContextKey, machine.Hostname)
 
 	ctx, cancel := context.WithCancel(ctx)
@@ -353,9 +356,9 @@ func (h *Headscale) PollNetMapStream(
 				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.
+			// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().
@@ -431,9 +434,9 @@ func (h *Headscale) PollNetMapStream(
 				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.
+			// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().
@@ -588,9 +591,9 @@ func (h *Headscale) PollNetMapStream(
 				Str("handler", "PollNetMapStream").
 				Str("machine", machine.Hostname).
 				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.
+			// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().

From 3f0639c87ddbb86fd9af3d210eafa98b80301ad1 Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Mon, 11 Jul 2022 20:33:24 +0200
Subject: [PATCH 11/18] graceful shutdown lint fixes

---
 app.go    | 32 ++++++++++++++++++--------------
 config.go | 13 +++++++++----
 poll.go   |  4 ++--
 3 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/app.go b/app.go
index de6ef66..5f5e261 100644
--- a/app.go
+++ b/app.go
@@ -94,8 +94,8 @@ type Headscale struct {
 
 	ipAllocationMutex sync.Mutex
 
-	shutdownChan chan struct{}
-	wg           sync.WaitGroup
+	shutdownChan       chan struct{}
+	pollNetMapStreamWG sync.WaitGroup
 }
 
 // Look up the TLS constant relative to user-supplied TLS client
@@ -148,13 +148,13 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
 	)
 
 	app := Headscale{
-		cfg:               cfg,
-		dbType:            cfg.DBtype,
-		dbString:          dbString,
-		privateKey:        privKey,
-		aclRules:          tailcfg.FilterAllowAll, // default allowall
-		registrationCache: registrationCache,
-		wg:                sync.WaitGroup{},
+		cfg:                cfg,
+		dbType:             cfg.DBtype,
+		dbString:           dbString,
+		privateKey:         privKey,
+		aclRules:           tailcfg.FilterAllowAll, // default allowall
+		registrationCache:  registrationCache,
+		pollNetMapStreamWG: sync.WaitGroup{},
 	}
 
 	err = app.initDB()
@@ -672,7 +672,7 @@ func (h *Headscale) Serve() error {
 		syscall.SIGTERM,
 		syscall.SIGQUIT,
 		syscall.SIGHUP)
-	sig_func := func(c chan os.Signal) {
+	sigFunc := func(c chan os.Signal) {
 		// Wait for a SIGINT or SIGKILL:
 		for {
 			sig := <-c
@@ -703,7 +703,7 @@ func (h *Headscale) Serve() error {
 					Msg("Received signal to stop, shutting down gracefully")
 
 				close(h.shutdownChan)
-				h.wg.Wait()
+				h.pollNetMapStreamWG.Wait()
 
 				// Gracefully shut down servers
 				ctx, cancel := context.WithTimeout(context.Background(), HTTPShutdownTimeout)
@@ -747,7 +747,11 @@ func (h *Headscale) Serve() error {
 			}
 		}
 	}
-	errorGroup.Go(func() error { sig_func(sigc); return nil })
+	errorGroup.Go(func() error {
+		sigFunc(sigc)
+
+		return nil
+	})
 
 	return errorGroup.Wait()
 }
@@ -771,13 +775,13 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
 		}
 
 		switch h.cfg.TLS.LetsEncrypt.ChallengeType {
-		case "TLS-ALPN-01":
+		case tlsALPN01ChallengeType:
 			// 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 certManager.TLSConfig(), nil
 
-		case "HTTP-01":
+		case http01ChallengeType:
 			// 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.
diff --git a/config.go b/config.go
index 6789f6f..6935840 100644
--- a/config.go
+++ b/config.go
@@ -18,6 +18,11 @@ import (
 	"tailscale.com/types/dnstype"
 )
 
+const (
+	tlsALPN01ChallengeType = "TLS-ALPN-01"
+	http01ChallengeType    = "HTTP-01"
+)
+
 // Config contains the initial Headscale configuration.
 type Config struct {
 	ServerURL                      string
@@ -136,7 +141,7 @@ func LoadConfig(path string, isFile bool) error {
 	viper.AutomaticEnv()
 
 	viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
-	viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01")
+	viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
 	viper.SetDefault("tls_client_auth_mode", "relaxed")
 
 	viper.SetDefault("log_level", "info")
@@ -179,15 +184,15 @@ func LoadConfig(path string, isFile bool) error {
 	}
 
 	if (viper.GetString("tls_letsencrypt_hostname") != "") &&
-		(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") &&
+		(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
 		(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
 		// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
 		log.Warn().
 			Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
 	}
 
-	if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") &&
-		(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") {
+	if (viper.GetString("tls_letsencrypt_challenge_type") != http01ChallengeType) &&
+		(viper.GetString("tls_letsencrypt_challenge_type") != tlsALPN01ChallengeType) {
 		errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
 	}
 
diff --git a/poll.go b/poll.go
index 94941aa..b9a757a 100644
--- a/poll.go
+++ b/poll.go
@@ -290,8 +290,8 @@ func (h *Headscale) PollNetMapStream(
 	keepAliveChan chan []byte,
 	updateChan chan struct{},
 ) {
-	h.wg.Add(1)
-	defer h.wg.Done()
+	h.pollNetMapStreamWG.Add(1)
+	defer h.pollNetMapStreamWG.Done()
 
 	ctx := context.WithValue(req.Context(), machineNameContextKey, machine.Hostname)
 

From 395caaad421797ac7fba6a0ca0e7df87d13262f2 Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Mon, 11 Jul 2022 23:25:13 +0200
Subject: [PATCH 12/18] decompose OIDCCallback method

---
 oidc.go | 240 +++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 187 insertions(+), 53 deletions(-)

diff --git a/oidc.go b/oidc.go
index 8b5f024..5509bd4 100644
--- a/oidc.go
+++ b/oidc.go
@@ -136,6 +136,82 @@ func (h *Headscale) OIDCCallback(
 	writer http.ResponseWriter,
 	req *http.Request,
 ) {
+	code, state, ok := validateOIDCCallbackParams(writer, req)
+	if !ok {
+		return
+	}
+
+	rawIDToken, ok := h.getIDTokenForOIDCCallback(writer, code, state)
+	if !ok {
+		return
+	}
+
+	idToken, ok := h.verifyIDTokenForOIDCCallback(writer, rawIDToken)
+	if !ok {
+		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"))
+	// 	return
+	// }
+
+	claims, ok := extractIDTokenClaims(writer, idToken)
+	if !ok {
+		return
+	}
+
+	if ok := validateOIDCAllowedDomains(writer, h.cfg.OIDC.AllowedDomains, claims); !ok {
+		return
+	}
+
+	if ok := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); !ok {
+		return
+	}
+
+	machineKey, ok := h.validateMachineForOIDCCallback(writer, state, claims)
+	if !ok {
+		return
+	}
+
+	namespaceName, ok := getNamespaceName(writer, claims, h.cfg.OIDC.StripEmaildomain)
+	if !ok {
+		return
+	}
+
+	// register the machine if it's new
+	log.Debug().Msg("Registering new machine after successful callback")
+
+	namespace, ok := h.findOrCreateNewNamespaceForOIDCCallback(writer, namespaceName)
+	if !ok {
+		return
+	}
+
+	if ok := h.registerMachineForOIDCCallback(writer, namespace, machineKey); !ok {
+		return
+	}
+
+	content, ok := renderOIDCCallbackTemplate(writer, claims)
+	if !ok {
+		return
+	}
+
+	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	if _, err := writer.Write(content.Bytes()); err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to write response")
+	}
+}
+
+func validateOIDCCallbackParams(
+	writer http.ResponseWriter,
+	req *http.Request,
+) (string, string, bool) {
 	code := req.URL.Query().Get("code")
 	state := req.URL.Query().Get("state")
 
@@ -150,9 +226,16 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return "", "", false
 	}
 
+	return code, state, true
+}
+
+func (h *Headscale) getIDTokenForOIDCCallback(
+	writer http.ResponseWriter,
+	code, state string,
+) (string, bool) {
 	oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
 	if err != nil {
 		log.Error().
@@ -169,7 +252,7 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return "", false
 	}
 
 	log.Trace().
@@ -190,11 +273,17 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return "", false
 	}
 
-	verifier := h.oidcProvider.Verifier(&oidc.Config{ClientID: h.cfg.OIDC.ClientID})
+	return rawIDToken, true
+}
 
+func (h *Headscale) verifyIDTokenForOIDCCallback(
+	writer http.ResponseWriter,
+	rawIDToken string,
+) (*oidc.IDToken, bool) {
+	verifier := h.oidcProvider.Verifier(&oidc.Config{ClientID: h.cfg.OIDC.ClientID})
 	idToken, err := verifier.Verify(context.Background(), rawIDToken)
 	if err != nil {
 		log.Error().
@@ -211,19 +300,18 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
-	// 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"))
-	// 	return
-	// }
+	return idToken, true
+}
 
-	// Extract custom claims
+func extractIDTokenClaims(
+	writer http.ResponseWriter,
+	idToken *oidc.IDToken,
+) (*IDTokenClaims, bool) {
 	var claims IDTokenClaims
-	if err = idToken.Claims(&claims); err != nil {
+	if err := idToken.Claims(claims); err != nil {
 		log.Error().
 			Err(err).
 			Caller().
@@ -238,13 +326,22 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
-	// If AllowedDomains is provided, check that the authenticated principal ends with @<alloweddomain>.
-	if len(h.cfg.OIDC.AllowedDomains) > 0 {
+	return &claims, true
+}
+
+// validateOIDCAllowedDomains checks that if AllowedDomains is provided,
+// that the authenticated principal ends with @<alloweddomain>.
+func validateOIDCAllowedDomains(
+	writer http.ResponseWriter,
+	allowedDomains []string,
+	claims *IDTokenClaims,
+) bool {
+	if len(allowedDomains) > 0 {
 		if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
-			!IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) {
+			!IsStringInSlice(allowedDomains, claims.Email[at+1:]) {
 			log.Error().Msg("authenticated principal does not match any allowed domain")
 			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 			writer.WriteHeader(http.StatusBadRequest)
@@ -256,13 +353,22 @@ func (h *Headscale) OIDCCallback(
 					Msg("Failed to write response")
 			}
 
-			return
+			return false
 		}
 	}
 
-	// If AllowedUsers is provided, check that the authenticated princial is part of that list.
-	if len(h.cfg.OIDC.AllowedUsers) > 0 &&
-		!IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) {
+	return true
+}
+
+// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
+// that the authenticated principal is part of that list.
+func validateOIDCAllowedUsers(
+	writer http.ResponseWriter,
+	allowedUsers []string,
+	claims *IDTokenClaims,
+) bool {
+	if len(allowedUsers) > 0 &&
+		!IsStringInSlice(allowedUsers, claims.Email) {
 		log.Error().Msg("authenticated principal does not match any allowed user")
 		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 		writer.WriteHeader(http.StatusBadRequest)
@@ -274,12 +380,23 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return false
 	}
 
+	return true
+}
+
+// validateMachine retrieves machine information if it exist
+// The error is not important, because if it does not
+// exist, then this is a new machine and we will move
+// on to registration.
+func (h *Headscale) validateMachineForOIDCCallback(
+	writer http.ResponseWriter,
+	state string,
+	claims *IDTokenClaims,
+) (*key.MachinePublic, bool) {
 	// retrieve machinekey from state cache
 	machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
-
 	if !machineKeyFound {
 		log.Error().
 			Msg("requested machine state key expired before authorisation completed")
@@ -293,13 +410,12 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
-	machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
-
 	var machineKey key.MachinePublic
-	err = machineKey.UnmarshalText(
+	machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
+	err := machineKey.UnmarshalText(
 		[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
 	)
 	if err != nil {
@@ -315,7 +431,7 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
 	if !machineKeyOK {
@@ -330,7 +446,7 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
 	// retrieve machine information if it exist
@@ -353,7 +469,7 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to refresh machine")
 			http.Error(writer, "Failed to refresh machine", http.StatusInternalServerError)
 
-			return
+			return nil, false
 		}
 
 		var content bytes.Buffer
@@ -377,7 +493,7 @@ func (h *Headscale) OIDCCallback(
 					Msg("Failed to write response")
 			}
 
-			return
+			return nil, false
 		}
 
 		writer.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -390,12 +506,20 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
+	return &machineKey, true
+}
+
+func getNamespaceName(
+	writer http.ResponseWriter,
+	claims *IDTokenClaims,
+	stripEmaildomain bool,
+) (string, bool) {
 	namespaceName, err := NormalizeToFQDNRules(
 		claims.Email,
-		h.cfg.OIDC.StripEmaildomain,
+		stripEmaildomain,
 	)
 	if err != nil {
 		log.Error().Err(err).Caller().Msgf("couldn't normalize email")
@@ -409,12 +533,16 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return "", false
 	}
 
-	// register the machine if it's new
-	log.Debug().Msg("Registering new machine after successful callback")
+	return namespaceName, true
+}
 
+func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
+	writer http.ResponseWriter,
+	namespaceName string,
+) (*Namespace, bool) {
 	namespace, err := h.GetNamespace(namespaceName)
 	if errors.Is(err, errNamespaceNotFound) {
 		namespace, err = h.CreateNamespace(namespaceName)
@@ -434,7 +562,7 @@ func (h *Headscale) OIDCCallback(
 					Msg("Failed to write response")
 			}
 
-			return
+			return nil, false
 		}
 	} else if err != nil {
 		log.Error().
@@ -452,17 +580,24 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
-	machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
+	return namespace, true
+}
 
-	_, err = h.RegisterMachineFromAuthCallback(
+func (h *Headscale) registerMachineForOIDCCallback(
+	writer http.ResponseWriter,
+	namespace *Namespace,
+	machineKey *key.MachinePublic,
+) bool {
+	machineKeyStr := MachinePublicKeyStripPrefix(*machineKey)
+
+	if _, err := h.RegisterMachineFromAuthCallback(
 		machineKeyStr,
 		namespace.Name,
 		RegisterMethodOIDC,
-	)
-	if err != nil {
+	); err != nil {
 		log.Error().
 			Caller().
 			Err(err).
@@ -477,9 +612,16 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return false
 	}
 
+	return true
+}
+
+func renderOIDCCallbackTemplate(
+	writer http.ResponseWriter,
+	claims *IDTokenClaims,
+) (*bytes.Buffer, bool) {
 	var content bytes.Buffer
 	if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
 		User: claims.Email,
@@ -501,16 +643,8 @@ func (h *Headscale) OIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return
+		return nil, false
 	}
 
-	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(content.Bytes())
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
+	return &content, true
 }

From 56858a56dbd50cc1c0547542a0b78948318e0f30 Mon Sep 17 00:00:00 2001
From: Grigoriy Mikhalkin <grigoriymikhalkin@gmail.com>
Date: Thu, 21 Jul 2022 23:47:59 +0200
Subject: [PATCH 13/18] Revert "decompose OIDCCallback method"

This reverts commit 395caaad421797ac7fba6a0ca0e7df87d13262f2.
---
 oidc.go | 240 +++++++++++++-------------------------------------------
 poll.go |  18 ++---
 2 files changed, 62 insertions(+), 196 deletions(-)

diff --git a/oidc.go b/oidc.go
index 5509bd4..8b5f024 100644
--- a/oidc.go
+++ b/oidc.go
@@ -136,82 +136,6 @@ func (h *Headscale) OIDCCallback(
 	writer http.ResponseWriter,
 	req *http.Request,
 ) {
-	code, state, ok := validateOIDCCallbackParams(writer, req)
-	if !ok {
-		return
-	}
-
-	rawIDToken, ok := h.getIDTokenForOIDCCallback(writer, code, state)
-	if !ok {
-		return
-	}
-
-	idToken, ok := h.verifyIDTokenForOIDCCallback(writer, rawIDToken)
-	if !ok {
-		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"))
-	// 	return
-	// }
-
-	claims, ok := extractIDTokenClaims(writer, idToken)
-	if !ok {
-		return
-	}
-
-	if ok := validateOIDCAllowedDomains(writer, h.cfg.OIDC.AllowedDomains, claims); !ok {
-		return
-	}
-
-	if ok := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); !ok {
-		return
-	}
-
-	machineKey, ok := h.validateMachineForOIDCCallback(writer, state, claims)
-	if !ok {
-		return
-	}
-
-	namespaceName, ok := getNamespaceName(writer, claims, h.cfg.OIDC.StripEmaildomain)
-	if !ok {
-		return
-	}
-
-	// register the machine if it's new
-	log.Debug().Msg("Registering new machine after successful callback")
-
-	namespace, ok := h.findOrCreateNewNamespaceForOIDCCallback(writer, namespaceName)
-	if !ok {
-		return
-	}
-
-	if ok := h.registerMachineForOIDCCallback(writer, namespace, machineKey); !ok {
-		return
-	}
-
-	content, ok := renderOIDCCallbackTemplate(writer, claims)
-	if !ok {
-		return
-	}
-
-	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	if _, err := writer.Write(content.Bytes()); err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-func validateOIDCCallbackParams(
-	writer http.ResponseWriter,
-	req *http.Request,
-) (string, string, bool) {
 	code := req.URL.Query().Get("code")
 	state := req.URL.Query().Get("state")
 
@@ -226,16 +150,9 @@ func validateOIDCCallbackParams(
 				Msg("Failed to write response")
 		}
 
-		return "", "", false
+		return
 	}
 
-	return code, state, true
-}
-
-func (h *Headscale) getIDTokenForOIDCCallback(
-	writer http.ResponseWriter,
-	code, state string,
-) (string, bool) {
 	oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code)
 	if err != nil {
 		log.Error().
@@ -252,7 +169,7 @@ func (h *Headscale) getIDTokenForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return "", false
+		return
 	}
 
 	log.Trace().
@@ -273,17 +190,11 @@ func (h *Headscale) getIDTokenForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return "", false
+		return
 	}
 
-	return rawIDToken, true
-}
-
-func (h *Headscale) verifyIDTokenForOIDCCallback(
-	writer http.ResponseWriter,
-	rawIDToken string,
-) (*oidc.IDToken, bool) {
 	verifier := h.oidcProvider.Verifier(&oidc.Config{ClientID: h.cfg.OIDC.ClientID})
+
 	idToken, err := verifier.Verify(context.Background(), rawIDToken)
 	if err != nil {
 		log.Error().
@@ -300,18 +211,19 @@ func (h *Headscale) verifyIDTokenForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	return idToken, true
-}
+	// 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"))
+	// 	return
+	// }
 
-func extractIDTokenClaims(
-	writer http.ResponseWriter,
-	idToken *oidc.IDToken,
-) (*IDTokenClaims, bool) {
+	// Extract custom claims
 	var claims IDTokenClaims
-	if err := idToken.Claims(claims); err != nil {
+	if err = idToken.Claims(&claims); err != nil {
 		log.Error().
 			Err(err).
 			Caller().
@@ -326,22 +238,13 @@ func extractIDTokenClaims(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	return &claims, true
-}
-
-// validateOIDCAllowedDomains checks that if AllowedDomains is provided,
-// that the authenticated principal ends with @<alloweddomain>.
-func validateOIDCAllowedDomains(
-	writer http.ResponseWriter,
-	allowedDomains []string,
-	claims *IDTokenClaims,
-) bool {
-	if len(allowedDomains) > 0 {
+	// If AllowedDomains is provided, check that the authenticated principal ends with @<alloweddomain>.
+	if len(h.cfg.OIDC.AllowedDomains) > 0 {
 		if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
-			!IsStringInSlice(allowedDomains, claims.Email[at+1:]) {
+			!IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) {
 			log.Error().Msg("authenticated principal does not match any allowed domain")
 			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 			writer.WriteHeader(http.StatusBadRequest)
@@ -353,22 +256,13 @@ func validateOIDCAllowedDomains(
 					Msg("Failed to write response")
 			}
 
-			return false
+			return
 		}
 	}
 
-	return true
-}
-
-// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
-// that the authenticated principal is part of that list.
-func validateOIDCAllowedUsers(
-	writer http.ResponseWriter,
-	allowedUsers []string,
-	claims *IDTokenClaims,
-) bool {
-	if len(allowedUsers) > 0 &&
-		!IsStringInSlice(allowedUsers, claims.Email) {
+	// If AllowedUsers is provided, check that the authenticated princial is part of that list.
+	if len(h.cfg.OIDC.AllowedUsers) > 0 &&
+		!IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) {
 		log.Error().Msg("authenticated principal does not match any allowed user")
 		writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 		writer.WriteHeader(http.StatusBadRequest)
@@ -380,23 +274,12 @@ func validateOIDCAllowedUsers(
 				Msg("Failed to write response")
 		}
 
-		return false
+		return
 	}
 
-	return true
-}
-
-// validateMachine retrieves machine information if it exist
-// The error is not important, because if it does not
-// exist, then this is a new machine and we will move
-// on to registration.
-func (h *Headscale) validateMachineForOIDCCallback(
-	writer http.ResponseWriter,
-	state string,
-	claims *IDTokenClaims,
-) (*key.MachinePublic, bool) {
 	// retrieve machinekey from state cache
 	machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
+
 	if !machineKeyFound {
 		log.Error().
 			Msg("requested machine state key expired before authorisation completed")
@@ -410,12 +293,13 @@ func (h *Headscale) validateMachineForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	var machineKey key.MachinePublic
 	machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
-	err := machineKey.UnmarshalText(
+
+	var machineKey key.MachinePublic
+	err = machineKey.UnmarshalText(
 		[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
 	)
 	if err != nil {
@@ -431,7 +315,7 @@ func (h *Headscale) validateMachineForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
 	if !machineKeyOK {
@@ -446,7 +330,7 @@ func (h *Headscale) validateMachineForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
 	// retrieve machine information if it exist
@@ -469,7 +353,7 @@ func (h *Headscale) validateMachineForOIDCCallback(
 				Msg("Failed to refresh machine")
 			http.Error(writer, "Failed to refresh machine", http.StatusInternalServerError)
 
-			return nil, false
+			return
 		}
 
 		var content bytes.Buffer
@@ -493,7 +377,7 @@ func (h *Headscale) validateMachineForOIDCCallback(
 					Msg("Failed to write response")
 			}
 
-			return nil, false
+			return
 		}
 
 		writer.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -506,20 +390,12 @@ func (h *Headscale) validateMachineForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	return &machineKey, true
-}
-
-func getNamespaceName(
-	writer http.ResponseWriter,
-	claims *IDTokenClaims,
-	stripEmaildomain bool,
-) (string, bool) {
 	namespaceName, err := NormalizeToFQDNRules(
 		claims.Email,
-		stripEmaildomain,
+		h.cfg.OIDC.StripEmaildomain,
 	)
 	if err != nil {
 		log.Error().Err(err).Caller().Msgf("couldn't normalize email")
@@ -533,16 +409,12 @@ func getNamespaceName(
 				Msg("Failed to write response")
 		}
 
-		return "", false
+		return
 	}
 
-	return namespaceName, true
-}
+	// register the machine if it's new
+	log.Debug().Msg("Registering new machine after successful callback")
 
-func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
-	writer http.ResponseWriter,
-	namespaceName string,
-) (*Namespace, bool) {
 	namespace, err := h.GetNamespace(namespaceName)
 	if errors.Is(err, errNamespaceNotFound) {
 		namespace, err = h.CreateNamespace(namespaceName)
@@ -562,7 +434,7 @@ func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
 					Msg("Failed to write response")
 			}
 
-			return nil, false
+			return
 		}
 	} else if err != nil {
 		log.Error().
@@ -580,24 +452,17 @@ func (h *Headscale) findOrCreateNewNamespaceForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	return namespace, true
-}
+	machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
 
-func (h *Headscale) registerMachineForOIDCCallback(
-	writer http.ResponseWriter,
-	namespace *Namespace,
-	machineKey *key.MachinePublic,
-) bool {
-	machineKeyStr := MachinePublicKeyStripPrefix(*machineKey)
-
-	if _, err := h.RegisterMachineFromAuthCallback(
+	_, err = h.RegisterMachineFromAuthCallback(
 		machineKeyStr,
 		namespace.Name,
 		RegisterMethodOIDC,
-	); err != nil {
+	)
+	if err != nil {
 		log.Error().
 			Caller().
 			Err(err).
@@ -612,16 +477,9 @@ func (h *Headscale) registerMachineForOIDCCallback(
 				Msg("Failed to write response")
 		}
 
-		return false
+		return
 	}
 
-	return true
-}
-
-func renderOIDCCallbackTemplate(
-	writer http.ResponseWriter,
-	claims *IDTokenClaims,
-) (*bytes.Buffer, bool) {
 	var content bytes.Buffer
 	if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
 		User: claims.Email,
@@ -643,8 +501,16 @@ func renderOIDCCallbackTemplate(
 				Msg("Failed to write response")
 		}
 
-		return nil, false
+		return
 	}
 
-	return &content, true
+	writer.Header().Set("Content-Type", "text/html; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(content.Bytes())
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to write response")
+	}
 }
diff --git a/poll.go b/poll.go
index b9a757a..9c17b5c 100644
--- a/poll.go
+++ b/poll.go
@@ -356,9 +356,9 @@ func (h *Headscale) PollNetMapStream(
 				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.
+				// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().
@@ -434,9 +434,9 @@ func (h *Headscale) PollNetMapStream(
 				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.
+				// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().
@@ -591,9 +591,9 @@ func (h *Headscale) PollNetMapStream(
 				Str("handler", "PollNetMapStream").
 				Str("machine", machine.Hostname).
 				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.
+				// 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.UpdateMachineFromDatabase(machine)
 			if err != nil {
 				log.Error().

From a4d0efbe8d1cfc61b4b3934d8e15d6032be71781 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 21 Jul 2022 23:57:07 +0200
Subject: [PATCH 14/18] Fix API router

---
 app.go | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/app.go b/app.go
index 11c8d68..d7d5ea5 100644
--- a/app.go
+++ b/app.go
@@ -445,11 +445,9 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
 		router.HandleFunc("/bootstrap-dns", h.DERPBootstrapDNSHandler)
 	}
 
-	api := router.PathPrefix("/api").Subrouter()
-	api.Use(h.httpAuthenticationMiddleware)
-	{
-		api.HandleFunc("/v1/*any", grpcMux.ServeHTTP)
-	}
+	apiRouter := router.PathPrefix("/api").Subrouter()
+	apiRouter.Use(h.httpAuthenticationMiddleware)
+	apiRouter.PathPrefix("/v1/").HandlerFunc(grpcMux.ServeHTTP)
 
 	router.PathPrefix("/").HandlerFunc(stdoutHandler)
 

From 6c9f3420e250c610fa164af64ce8ac9d6c2d7779 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 21 Jul 2022 23:59:44 +0200
Subject: [PATCH 15/18] Updated changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1ec1b6..f43dfea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
 - Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
 - Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648) [677](https://github.com/juanfont/headscale/pull/677)
 - Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
+- Fix regression with HTTP API [#684](https://github.com/juanfont/headscale/pull/684)
 
 ## 0.15.0 (2022-03-20)
 

From 936adb7d2c0abfcd9333f5166d240deaf2fc16b5 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 22 Jul 2022 07:36:16 +0000
Subject: [PATCH 16/18] docs(README): update contributors

---
 README.md | 39 +++++++++++++++++++++++----------------
 1 file changed, 23 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 38578c3..d088767 100644
--- a/README.md
+++ b/README.md
@@ -283,6 +283,15 @@ make build
             <sub style="font-size:14px"><b>Fernando De Lucchi</b></sub>
         </a>
     </td>
+    <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
+        <a href=https://github.com/GrigoriyMikhalkin>
+            <img src=https://avatars.githubusercontent.com/u/3637857?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=GrigoriyMikhalkin/>
+            <br />
+            <sub style="font-size:14px"><b>GrigoriyMikhalkin</b></sub>
+        </a>
+    </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/hdhoang>
             <img src=https://avatars.githubusercontent.com/u/12537?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Hoàng Đức Hiếu/>
@@ -290,8 +299,6 @@ make build
             <sub style="font-size:14px"><b>Hoàng Đức Hiếu</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/bravechamp>
             <img src=https://avatars.githubusercontent.com/u/48980452?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=bravechamp/>
@@ -327,6 +334,8 @@ make build
             <sub style="font-size:14px"><b>Michael G.</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/ptman>
             <img src=https://avatars.githubusercontent.com/u/24669?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Paul Tötterman/>
@@ -334,8 +343,6 @@ make build
             <sub style="font-size:14px"><b>Paul Tötterman</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/samson4649>
             <img src=https://avatars.githubusercontent.com/u/12725953?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Samuel Lock/>
@@ -371,6 +378,8 @@ make build
             <sub style="font-size:14px"><b>Pavlos Vinieratos</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/SilverBut>
             <img src=https://avatars.githubusercontent.com/u/6560655?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Silver Bullet/>
@@ -378,8 +387,6 @@ make build
             <sub style="font-size:14px"><b>Silver Bullet</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/lachy2849>
             <img src=https://avatars.githubusercontent.com/u/98844035?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lachy2849/>
@@ -415,6 +422,8 @@ make build
             <sub style="font-size:14px"><b>Aofei Sheng</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/awoimbee>
             <img src=https://avatars.githubusercontent.com/u/22431493?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Arthur Woimbée/>
@@ -422,8 +431,6 @@ make build
             <sub style="font-size:14px"><b>Arthur Woimbée</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/stensonb>
             <img src=https://avatars.githubusercontent.com/u/933389?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Bryan Stenson/>
@@ -459,6 +466,8 @@ make build
             <sub style="font-size:14px"><b>Felix Yan</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/JJGadgets>
             <img src=https://avatars.githubusercontent.com/u/5709019?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=JJGadgets/>
@@ -466,8 +475,6 @@ make build
             <sub style="font-size:14px"><b>JJGadgets</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/madjam002>
             <img src=https://avatars.githubusercontent.com/u/679137?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jamie Greeff/>
@@ -503,6 +510,8 @@ make build
             <sub style="font-size:14px"><b>WhiteSource Renovate</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/ryanfowler>
             <img src=https://avatars.githubusercontent.com/u/2668821?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Ryan Fowler/>
@@ -510,8 +519,6 @@ make build
             <sub style="font-size:14px"><b>Ryan Fowler</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/shaananc>
             <img src=https://avatars.githubusercontent.com/u/2287839?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shaanan Cohney/>
@@ -547,6 +554,8 @@ make build
             <sub style="font-size:14px"><b>Tianon Gravi</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/woudsma>
             <img src=https://avatars.githubusercontent.com/u/6162978?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tjerk Woudsma/>
@@ -554,8 +563,6 @@ make build
             <sub style="font-size:14px"><b>Tjerk Woudsma</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/y0ngb1n>
             <img src=https://avatars.githubusercontent.com/u/25719408?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yang Bin/>
@@ -591,6 +598,8 @@ make build
             <sub style="font-size:14px"><b>henning mueller</b></sub>
         </a>
     </td>
+</tr>
+<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/ignoramous>
             <img src=https://avatars.githubusercontent.com/u/852289?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ignoramous/>
@@ -598,8 +607,6 @@ make build
             <sub style="font-size:14px"><b>ignoramous</b></sub>
         </a>
     </td>
-</tr>
-<tr>
     <td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
         <a href=https://github.com/lion24>
             <img src=https://avatars.githubusercontent.com/u/1382102?v=4 width="100;"  style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=lion24/>

From dc94570c4ad5a120b14751200605c181f43a94d3 Mon Sep 17 00:00:00 2001
From: Jiang Zhu <jiang.moe@outlook.com>
Date: Sat, 23 Jul 2022 01:33:11 +0800
Subject: [PATCH 17/18] more intuitive output of node ls

---
 cmd/headscale/cli/nodes.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go
index 059a16d..c2b1e95 100644
--- a/cmd/headscale/cli/nodes.go
+++ b/cmd/headscale/cli/nodes.go
@@ -465,6 +465,7 @@ func nodesToPtables(
 ) (pterm.TableData, error) {
 	tableHeader := []string{
 		"ID",
+		"Hostname",
 		"Name",
 		"NodeKey",
 		"Namespace",
@@ -566,6 +567,7 @@ func nodesToPtables(
 		nodeData := []string{
 			strconv.FormatUint(machine.Id, headscale.Base10),
 			machine.Name,
+			machine.GetGivenName(),
 			nodeKey.ShortString(),
 			namespace,
 			strings.Join([]string{IPV4Address, IPV6Address}, ", "),

From 49354f678efa1b58cc53727afb26daa249414c5f Mon Sep 17 00:00:00 2001
From: Jiang Zhu <jiang.moe@outlook.com>
Date: Sat, 23 Jul 2022 04:47:37 +0800
Subject: [PATCH 18/18] update CHANGELOG

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f43dfea..2c2ae62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@
 - Drop Gin as web framework in Headscale [648](https://github.com/juanfont/headscale/pull/648) [677](https://github.com/juanfont/headscale/pull/677)
 - Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
 - Fix regression with HTTP API [#684](https://github.com/juanfont/headscale/pull/684)
+- nodes ls now print both Hostname and Name(Issue [#647](https://github.com/juanfont/headscale/issues/647) PR [#687](https://github.com/juanfont/headscale/pull/687))
 
 ## 0.15.0 (2022-03-20)