From 6e8e2bf5083a268a278af5e331d2f0b3f262d157 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 11:14:38 +0200
Subject: [PATCH 01/63] Generate and read the Noise private key

---
 app.go              | 27 +++++++++++++++++++--------
 config-example.yaml |  7 +++++++
 config.go           |  4 ++++
 3 files changed, 30 insertions(+), 8 deletions(-)

diff --git a/app.go b/app.go
index 3e0012038d..8cfaa61575 100644
--- a/app.go
+++ b/app.go
@@ -72,12 +72,13 @@ const (
 
 // Headscale represents the base app of the service.
 type Headscale struct {
-	cfg        *Config
-	db         *gorm.DB
-	dbString   string
-	dbType     string
-	dbDebug    bool
-	privateKey *key.MachinePrivate
+	cfg             *Config
+	db              *gorm.DB
+	dbString        string
+	dbType          string
+	dbDebug         bool
+	privateKey      *key.MachinePrivate
+	noisePrivateKey *key.MachinePrivate
 
 	DERPMap    *tailcfg.DERPMap
 	DERPServer *DERPServer
@@ -120,11 +121,20 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
 }
 
 func NewHeadscale(cfg *Config) (*Headscale, error) {
-	privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
+	privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read or create private key: %w", err)
 	}
 
+	noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read or create noise private key: %w", err)
+	}
+
+	if privateKey.Equal(*noisePrivateKey) {
+		return nil, fmt.Errorf("private key and noise private key are the same")
+	}
+
 	var dbString string
 	switch cfg.DBtype {
 	case Postgres:
@@ -151,7 +161,8 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
 		cfg:                cfg,
 		dbType:             cfg.DBtype,
 		dbString:           dbString,
-		privateKey:         privKey,
+		privateKey:         privateKey,
+		noisePrivateKey:    noisePrivateKey,
 		aclRules:           tailcfg.FilterAllowAll, // default allowall
 		registrationCache:  registrationCache,
 		pollNetMapStreamWG: sync.WaitGroup{},
diff --git a/config-example.yaml b/config-example.yaml
index d3d155e2be..e090d910a6 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -41,6 +41,13 @@ grpc_allow_insecure: false
 # autogenerated if it's missing
 private_key_path: /var/lib/headscale/private.key
 
+# The Noise private key is used to encrypt the
+# traffic between headscale and Tailscale clients when
+# using the new Noise-based TS2021 protocol.
+# The noise private key file which will be
+# autogenerated if it's missing
+noise_private_key_path: /var/lib/headscale/noise_private.key
+
 # List of IP prefixes to allocate tailaddresses from.
 # Each prefix consists of either an IPv4 or IPv6 address,
 # and the associated prefix length, delimited by a slash.
diff --git a/config.go b/config.go
index 69358401e8..a792bab93b 100644
--- a/config.go
+++ b/config.go
@@ -34,6 +34,7 @@ type Config struct {
 	NodeUpdateCheckInterval        time.Duration
 	IPPrefixes                     []netaddr.IPPrefix
 	PrivateKeyPath                 string
+	NoisePrivateKeyPath            string
 	BaseDomain                     string
 	LogLevel                       zerolog.Level
 	DisableUpdateCheck             bool
@@ -487,6 +488,9 @@ func GetHeadscaleConfig() (*Config, error) {
 		PrivateKeyPath: AbsolutePathFromConfigPath(
 			viper.GetString("private_key_path"),
 		),
+		NoisePrivateKeyPath: AbsolutePathFromConfigPath(
+			viper.GetString("noise_private_key_path"),
+		),
 		BaseDomain: baseDomain,
 
 		DERP: derpConfig,

From 3e8f0e99849538568f329303f032b3655f4add99 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 11:24:05 +0200
Subject: [PATCH 02/63] Added support for Noise clients in /key handler

---
 api.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/api.go b/api.go
index 561545b874..52149d58d3 100644
--- a/api.go
+++ b/api.go
@@ -9,6 +9,7 @@ import (
 	"html/template"
 	"io"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -30,6 +31,12 @@ const (
 	ErrRegisterMethodCLIDoesNotSupportExpire = Error(
 		"machines registered with CLI does not support expire",
 	)
+
+	// The CapabilityVersion is used by Tailscale clients to indicate
+	// their codebase version. Tailscale clients can communicate over TS2021
+	// from CapabilityVersion 28.
+	// See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
+	NoiseCapabilityVersion = 28
 )
 
 func (h *Headscale) HealthHandler(
@@ -76,6 +83,45 @@ func (h *Headscale) KeyHandler(
 	writer http.ResponseWriter,
 	req *http.Request,
 ) {
+	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
+	clientCapabilityStr := req.URL.Query().Get("v")
+	if clientCapabilityStr != "" {
+		clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
+		if err != nil {
+			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
+			writer.WriteHeader(http.StatusBadRequest)
+			_, err := writer.Write([]byte("Wrong params"))
+			if err != nil {
+				log.Error().
+					Caller().
+					Err(err).
+					Msg("Failed to write response")
+			}
+
+			return
+		}
+
+		if clientCapabilityVersion >= NoiseCapabilityVersion {
+			// Tailscale has a different key for the TS2021 protocol
+			resp := tailcfg.OverTLSPublicKeyResponse{
+				LegacyPublicKey: h.privateKey.Public(),
+				PublicKey:       h.noisePrivateKey.Public(),
+			}
+			writer.Header().Set("Content-Type", "application/json")
+			writer.WriteHeader(http.StatusOK)
+			err = json.NewEncoder(writer).Encode(resp)
+			if err != nil {
+				log.Error().
+					Caller().
+					Err(err).
+					Msg("Failed to write response")
+			}
+
+			return
+		}
+	}
+
+	// Old clients don't send a 'v' parameter, so we send the legacy public key
 	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
 	writer.WriteHeader(http.StatusOK)
 	_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))

From 014e7abc6855973c7caaeac27aad72eab9b3b88a Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 14:46:23 +0200
Subject: [PATCH 03/63] Make private key errors constants

---
 app.go | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/app.go b/app.go
index 8cfaa61575..861b95587f 100644
--- a/app.go
+++ b/app.go
@@ -51,6 +51,10 @@ const (
 	errUnsupportedLetsEncryptChallengeType = Error(
 		"unknown value for Lets Encrypt challenge type",
 	)
+
+	ErrFailedPrivateKey      = Error("failed to read or create private key")
+	ErrFailedNoisePrivateKey = Error("failed to read or create Noise protocol private key")
+	ErrSamePrivateKeys       = Error("private key and noise private key are the same")
 )
 
 const (
@@ -123,16 +127,16 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
 func NewHeadscale(cfg *Config) (*Headscale, error) {
 	privateKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read or create private key: %w", err)
+		return nil, ErrFailedPrivateKey
 	}
 
 	noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read or create noise private key: %w", err)
+		return nil, ErrFailedNoisePrivateKey
 	}
 
 	if privateKey.Equal(*noisePrivateKey) {
-		return nil, fmt.Errorf("private key and noise private key are the same")
+		return nil, ErrSamePrivateKeys
 	}
 
 	var dbString string

From b261d19cfeadba59254ad7ca2caaea03708e9879 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 20:52:11 +0200
Subject: [PATCH 04/63] Added Noise upgrade handler and Noise mux

---
 noise.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 134 insertions(+)
 create mode 100644 noise.go

diff --git a/noise.go b/noise.go
new file mode 100644
index 0000000000..bd7d4bbd52
--- /dev/null
+++ b/noise.go
@@ -0,0 +1,134 @@
+package headscale
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"github.com/rs/zerolog/log"
+	"golang.org/x/net/http2"
+	"golang.org/x/net/http2/h2c"
+	"tailscale.com/control/controlbase"
+	"tailscale.com/net/netutil"
+)
+
+const (
+	errWrongConnectionUpgrade = Error("wrong connection upgrade")
+	errCannotHijack           = Error("cannot hijack connection")
+	errNoiseHandshakeFailed   = Error("noise handshake failed")
+)
+
+const (
+	// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
+	ts2021UpgradePath = "/ts2021"
+
+	// upgradeHeader is the value of the Upgrade HTTP header used to
+	// indicate the Tailscale control protocol.
+	upgradeHeaderValue = "tailscale-control-protocol"
+
+	// handshakeHeaderName is the HTTP request header that can
+	// optionally contain base64-encoded initial handshake
+	// payload, to save an RTT.
+	handshakeHeaderName = "X-Tailscale-Handshake"
+)
+
+// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
+// in order to use the Noise-based TS2021 protocol. Listens in /ts2021.
+func (h *Headscale) NoiseUpgradeHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr)
+
+	// Under normal circumpstances, we should be able to use the controlhttp.AcceptHTTP()
+	// function to do this - kindly left there by the Tailscale authors for us to use.
+	// (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go)
+	//
+	// When we used to use Gin, we had troubles here as Gin seems to do some
+	// fun stuff, and not flusing the writer properly.
+	// So have getNoiseConnection() that is essentially an AcceptHTTP, but in our side.
+	noiseConn, err := h.getNoiseConnection(writer, req)
+	if err != nil {
+		log.Error().Err(err).Msg("noise upgrade failed")
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
+
+		return
+	}
+
+	server := http.Server{}
+	server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{})
+	server.Serve(netutil.NewOneConnListener(noiseConn, nil))
+}
+
+// getNoiseConnection is basically AcceptHTTP from tailscale
+// TODO(juan): Figure out why we need to do this at all.
+func (h *Headscale) getNoiseConnection(
+	writer http.ResponseWriter,
+	req *http.Request,
+) (*controlbase.Conn, error) {
+	next := req.Header.Get("Upgrade")
+	if next == "" {
+		http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest)
+
+		return nil, errWrongConnectionUpgrade
+	}
+	if next != upgradeHeaderValue {
+		http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest)
+
+		return nil, errWrongConnectionUpgrade
+	}
+
+	initB64 := req.Header.Get(handshakeHeaderName)
+	if initB64 == "" {
+		log.Warn().
+			Caller().
+			Msg("no handshake header")
+		http.Error(writer, "missing Tailscale handshake header", http.StatusBadRequest)
+
+		return nil, errWrongConnectionUpgrade
+	}
+
+	init, err := base64.StdEncoding.DecodeString(initB64)
+	if err != nil {
+		log.Warn().Err(err).Msg("invalid handshake header")
+		http.Error(writer, "invalid tailscale handshake header", http.StatusBadRequest)
+
+		return nil, errWrongConnectionUpgrade
+	}
+
+	hijacker, ok := writer.(http.Hijacker)
+	if !ok {
+		log.Error().Caller().Err(err).Msgf("Hijack failed")
+		http.Error(writer, errCannotHijack.Error(), http.StatusInternalServerError)
+
+		return nil, errCannotHijack
+	}
+
+	// This is what changes from the original AcceptHTTP() function.
+	writer.Header().Set("Upgrade", upgradeHeaderValue)
+	writer.Header().Set("Connection", "upgrade")
+	writer.WriteHeader(http.StatusSwitchingProtocols)
+	// end
+
+	netConn, conn, err := hijacker.Hijack()
+	if err != nil {
+		log.Error().Caller().Err(err).Msgf("Hijack failed")
+		http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError)
+		return nil, errCannotHijack
+	}
+	if err := conn.Flush(); err != nil {
+		netConn.Close()
+
+		return nil, errCannotHijack
+	}
+
+	netConn = netutil.NewDrainBufConn(netConn, conn.Reader)
+
+	nc, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init)
+	if err != nil {
+		netConn.Close()
+
+		return nil, errNoiseHandshakeFailed
+	}
+
+	return nc, nil
+}

From be24bacb797315e5ca28392960c2b895e4228a69 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 20:55:37 +0200
Subject: [PATCH 05/63] Add noise mux and Noise path to base router

---
 app.go | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/app.go b/app.go
index 861b95587f..1a7ba72005 100644
--- a/app.go
+++ b/app.go
@@ -84,6 +84,8 @@ type Headscale struct {
 	privateKey      *key.MachinePrivate
 	noisePrivateKey *key.MachinePrivate
 
+	noiseMux *mux.Router
+
 	DERPMap    *tailcfg.DERPMap
 	DERPServer *DERPServer
 
@@ -430,6 +432,8 @@ func (h *Headscale) ensureUnixSocketIsAbsent() error {
 func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
 	router := mux.NewRouter()
 
+	router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
+
 	router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
 	router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
 	router.HandleFunc("/register/{nkey}", h.RegisterWebAPI).Methods(http.MethodGet)
@@ -459,6 +463,15 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
 	return router
 }
 
+func (h *Headscale) createNoiseMux() *mux.Router {
+	router := mux.NewRouter()
+
+	//router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
+	//router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost)
+
+	return router
+}
+
 // Serve launches a GIN server with the Headscale API.
 func (h *Headscale) Serve() error {
 	var err error
@@ -612,9 +625,16 @@ func (h *Headscale) Serve() error {
 	//
 	// HTTP setup
 	//
-
+	// This is the regular router that we expose
+	// over our main Addr. It also serves the legacy Tailcale API
 	router := h.createRouter(grpcGatewayMux)
 
+	// This router is served only over the Noise connection, and exposes only the new API.
+	//
+	// The HTTP2 server that exposes this router is created for
+	// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
+	h.noiseMux = h.createNoiseMux()
+
 	httpServer := &http.Server{
 		Addr:        h.cfg.Addr,
 		Handler:     router,

From fdd0c5040261d2d7aa1bf4f292aa01411fed2619 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 21:03:02 +0200
Subject: [PATCH 06/63] Added helper method to fetch machines by any nodekey +
 tests

---
 machine.go      | 13 +++++++++++
 machine_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+)

diff --git a/machine.go b/machine.go
index aebfbcefe5..b436fd8796 100644
--- a/machine.go
+++ b/machine.go
@@ -375,6 +375,19 @@ func (h *Headscale) GetMachineByNodeKey(
 	return &machine, nil
 }
 
+// GetMachineByAnyNodeKey finds a Machine by its current NodeKey or the old one, and returns the Machine struct.
+func (h *Headscale) GetMachineByAnyNodeKey(
+	nodeKey key.NodePublic, oldNodeKey key.NodePublic,
+) (*Machine, error) {
+	machine := Machine{}
+	if result := h.db.Preload("Namespace").First(&machine, "node_key = ? OR node_key = ?",
+		NodePublicKeyStripPrefix(nodeKey), NodePublicKeyStripPrefix(oldNodeKey)); result.Error != nil {
+		return nil, result.Error
+	}
+
+	return &machine, nil
+}
+
 // UpdateMachineFromDatabase takes a Machine struct pointer (typically already loaded from database
 // and updates it with the latest data from the database.
 func (h *Headscale) UpdateMachineFromDatabase(machine *Machine) error {
diff --git a/machine_test.go b/machine_test.go
index 53d065ff8f..5da0906f25 100644
--- a/machine_test.go
+++ b/machine_test.go
@@ -11,6 +11,7 @@ import (
 	"gopkg.in/check.v1"
 	"inet.af/netaddr"
 	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
 )
 
 func (s *Suite) TestGetMachine(c *check.C) {
@@ -65,6 +66,63 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
 	c.Assert(err, check.IsNil)
 }
 
+func (s *Suite) TestGetMachineByNodeKey(c *check.C) {
+	namespace, err := app.CreateNamespace("test")
+	c.Assert(err, check.IsNil)
+
+	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+	c.Assert(err, check.IsNil)
+
+	_, err = app.GetMachineByID(0)
+	c.Assert(err, check.NotNil)
+
+	nodeKey := key.NewNode()
+
+	machine := Machine{
+		ID:             0,
+		MachineKey:     "foo",
+		NodeKey:        NodePublicKeyStripPrefix(nodeKey.Public()),
+		DiscoKey:       "faa",
+		Hostname:       "testmachine",
+		NamespaceID:    namespace.ID,
+		RegisterMethod: RegisterMethodAuthKey,
+		AuthKeyID:      uint(pak.ID),
+	}
+	app.db.Save(&machine)
+
+	_, err = app.GetMachineByNodeKey(nodeKey.Public())
+	c.Assert(err, check.IsNil)
+}
+
+func (s *Suite) TestGetMachineByAnyNodeKey(c *check.C) {
+	namespace, err := app.CreateNamespace("test")
+	c.Assert(err, check.IsNil)
+
+	pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
+	c.Assert(err, check.IsNil)
+
+	_, err = app.GetMachineByID(0)
+	c.Assert(err, check.NotNil)
+
+	nodeKey := key.NewNode()
+	oldNodeKey := key.NewNode()
+
+	machine := Machine{
+		ID:             0,
+		MachineKey:     "foo",
+		NodeKey:        NodePublicKeyStripPrefix(nodeKey.Public()),
+		DiscoKey:       "faa",
+		Hostname:       "testmachine",
+		NamespaceID:    namespace.ID,
+		RegisterMethod: RegisterMethodAuthKey,
+		AuthKeyID:      uint(pak.ID),
+	}
+	app.db.Save(&machine)
+
+	_, err = app.GetMachineByAnyNodeKey(nodeKey.Public(), oldNodeKey.Public())
+	c.Assert(err, check.IsNil)
+}
+
 func (s *Suite) TestDeleteMachine(c *check.C) {
 	namespace, err := app.CreateNamespace("test")
 	c.Assert(err, check.IsNil)

From 1880035f6fe3e2e65f01607526f433f75c9313e9 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 13 Aug 2022 21:12:19 +0200
Subject: [PATCH 07/63] Add registration handler over Noise protocol

---
 app.go       |   2 +-
 noise_api.go | 427 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 428 insertions(+), 1 deletion(-)
 create mode 100644 noise_api.go

diff --git a/app.go b/app.go
index 1a7ba72005..9c0f11d49b 100644
--- a/app.go
+++ b/app.go
@@ -466,7 +466,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router {
 func (h *Headscale) createNoiseMux() *mux.Router {
 	router := mux.NewRouter()
 
-	//router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
+	router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
 	//router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost)
 
 	return router
diff --git a/noise_api.go b/noise_api.go
new file mode 100644
index 0000000000..475e6022db
--- /dev/null
+++ b/noise_api.go
@@ -0,0 +1,427 @@
+package headscale
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
+	"tailscale.com/tailcfg"
+)
+
+// // NoiseRegistrationHandler handles the actual registration process of a machine
+func (h *Headscale) NoiseRegistrationHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr)
+	if req.Method != http.MethodPost {
+		http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
+		return
+	}
+	body, _ := io.ReadAll(req.Body)
+	registerRequest := tailcfg.RegisterRequest{}
+	if err := json.Unmarshal(body, &registerRequest); err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse RegisterRequest")
+		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+
+	log.Trace().Caller().
+		Str("node_key", registerRequest.NodeKey.ShortString()).
+		Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
+		Msg("New node is registering")
+
+	now := time.Now().UTC()
+	machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		// If the machine has AuthKey set, handle registration via PreAuthKeys
+		if registerRequest.Auth.AuthKey != "" {
+			h.handleNoiseAuthKey(writer, req, registerRequest)
+
+			return
+		}
+
+		// Check if the node is waiting for interactive login.
+		//
+		// TODO(juan): We could use this field to improve our protocol implementation,
+		// and hold the request until the client closes it, or the interactive
+		// login is completed (i.e., the user registers the machine).
+		// This is not implemented yet, as it is no strictly required. The only side-effect
+		// is that the client will hammer headscale with requests until it gets a
+		// successful RegisterResponse.
+		if registerRequest.Followup != "" {
+			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
+				log.Debug().
+					Caller().
+					Str("machine", registerRequest.Hostinfo.Hostname).
+					Str("node_key", registerRequest.NodeKey.ShortString()).
+					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+					Str("follow_up", registerRequest.Followup).
+					Msg("Machine is waiting for interactive login")
+
+				ticker := time.NewTicker(registrationHoldoff)
+				select {
+				case <-req.Context().Done():
+					return
+				case <-ticker.C:
+					h.handleNoiseMachineRegistrationNew(writer, req, registerRequest)
+
+					return
+				}
+			}
+		}
+
+		log.Info().
+			Caller().
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Str("node_key", registerRequest.NodeKey.ShortString()).
+			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+			Str("follow_up", registerRequest.Followup).
+			Msg("New Noise machine not yet in the database")
+
+		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "RegistrationHandler").
+				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
+				Err(err)
+
+			return
+		}
+
+		// The machine did not have a key to authenticate, which means
+		// that we rely on a method that calls back some how (OpenID or CLI)
+		// We create the machine and then keep it around until a callback
+		// happens
+		newMachine := Machine{
+			MachineKey: "",
+			Hostname:   registerRequest.Hostinfo.Hostname,
+			GivenName:  givenName,
+			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
+			LastSeen:   &now,
+			Expiry:     &time.Time{},
+		}
+
+		if !registerRequest.Expiry.IsZero() {
+			log.Trace().
+				Caller().
+				Str("machine", registerRequest.Hostinfo.Hostname).
+				Time("expiry", registerRequest.Expiry).
+				Msg("Non-zero expiry time requested")
+			newMachine.Expiry = &registerRequest.Expiry
+		}
+
+		h.registrationCache.Set(
+			NodePublicKeyStripPrefix(registerRequest.NodeKey),
+			newMachine,
+			registerCacheExpiration,
+		)
+
+		h.handleNoiseMachineRegistrationNew(writer, req, registerRequest)
+
+		return
+	}
+
+	// The machine is already registered, so we need to pass through reauth or key update.
+	if machine != nil {
+		// If the NodeKey stored in headscale is the same as the key presented in a registration
+		// request, then we have a node that is either:
+		// - Trying to log out (sending a expiry in the past)
+		// - A valid, registered machine, looking for the node map
+		// - Expired machine wanting to reauthenticate
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
+			// 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 !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
+				h.handleNoiseNodeLogOut(writer, req, *machine)
+
+				return
+			}
+
+			// If machine is not expired, and is register, we have a already accepted this machine,
+			// let it proceed with a valid registration
+			if !machine.isExpired() {
+				h.handleNoiseNodeValidRegistration(writer, req, *machine)
+
+				return
+			}
+		}
+
+		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
+			!machine.isExpired() {
+			h.handleNoiseNodeRefreshKey(writer, req, registerRequest, *machine)
+
+			return
+		}
+
+		// The node has expired
+		h.handleNoiseNodeExpired(writer, req, registerRequest, *machine)
+
+		return
+	}
+}
+
+func (h *Headscale) handleNoiseAuthKey(
+	w http.ResponseWriter,
+	r *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+) {
+	log.Debug().
+		Caller().
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname)
+	resp := tailcfg.RegisterResponse{}
+
+	pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Err(err).
+			Msg("Failed authentication via AuthKey")
+		resp.MachineAuthorized = false
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusUnauthorized)
+		json.NewEncoder(w).Encode(resp)
+
+		log.Error().
+			Caller().
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Msg("Failed authentication via AuthKey over Noise")
+
+		if pak != nil {
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+				Inc()
+		} else {
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
+		}
+
+		return
+	}
+
+	log.Debug().
+		Caller().
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Msg("Authentication key was valid, proceeding to acquire IP addresses")
+
+	nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
+
+	// retrieve 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.
+	machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
+	if machine != nil {
+		log.Trace().
+			Caller().
+			Str("machine", machine.Hostname).
+			Msg("machine already registered, refreshing with new auth key")
+
+		machine.NodeKey = nodeKey
+		machine.AuthKeyID = uint(pak.ID)
+		h.RefreshMachine(machine, registerRequest.Expiry)
+	} else {
+		now := time.Now().UTC()
+
+		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "RegistrationHandler").
+				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
+				Err(err)
+
+			return
+		}
+
+		machineToRegister := Machine{
+			Hostname:       registerRequest.Hostinfo.Hostname,
+			GivenName:      givenName,
+			NamespaceID:    pak.Namespace.ID,
+			MachineKey:     "",
+			RegisterMethod: RegisterMethodAuthKey,
+			Expiry:         &registerRequest.Expiry,
+			NodeKey:        nodeKey,
+			LastSeen:       &now,
+			AuthKeyID:      uint(pak.ID),
+		}
+
+		machine, err = h.RegisterMachine(
+			machineToRegister,
+		)
+		if err != nil {
+			log.Error().
+				Caller().
+				Err(err).
+				Msg("could not register machine")
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+				Inc()
+			http.Error(w, "Internal error", http.StatusInternalServerError)
+
+			return
+		}
+	}
+
+	h.UsePreAuthKey(pak)
+
+	resp.MachineAuthorized = true
+	resp.User = *pak.Namespace.toUser()
+
+	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
+		Inc()
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+
+	log.Info().
+		Caller().
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
+		Msg("Successfully authenticated via AuthKey on Noise")
+}
+
+func (h *Headscale) handleNoiseNodeValidRegistration(
+	w http.ResponseWriter,
+	r *http.Request,
+	machine Machine,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The machine registration is valid, respond with redirect to /map
+	log.Debug().
+		Str("machine", machine.Hostname).
+		Msg("Client is registered and we have the current NodeKey. All clear to /map")
+
+	resp.AuthURL = ""
+	resp.MachineAuthorized = true
+	resp.User = *machine.Namespace.toUser()
+	resp.Login = *machine.Namespace.toLogin()
+
+	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
+		Inc()
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+}
+
+func (h *Headscale) handleNoiseMachineRegistrationNew(
+	w http.ResponseWriter,
+	r *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The machine registration is new, redirect the client to the registration URL
+	log.Debug().
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		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, "/"),
+			NodePublicKeyStripPrefix(registerRequest.NodeKey),
+		)
+	} else {
+		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+}
+
+func (h *Headscale) handleNoiseNodeLogOut(
+	w http.ResponseWriter,
+	r *http.Request,
+	machine Machine,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	log.Info().
+		Str("machine", machine.Hostname).
+		Msg("Client requested logout")
+
+	h.ExpireMachine(&machine)
+
+	resp.AuthURL = ""
+	resp.MachineAuthorized = false
+	resp.User = *machine.Namespace.toUser()
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+}
+
+func (h *Headscale) handleNoiseNodeRefreshKey(
+	w http.ResponseWriter,
+	r *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machine Machine,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	log.Debug().
+		Str("machine", machine.Hostname).
+		Msg("We have the OldNodeKey in the database. This is a key refresh")
+	machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
+	h.db.Save(&machine)
+
+	resp.AuthURL = ""
+	resp.User = *machine.Namespace.toUser()
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+}
+
+func (h *Headscale) handleNoiseNodeExpired(
+	w http.ResponseWriter,
+	r *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machine Machine,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The client has registered before, but has expired
+	log.Debug().
+		Caller().
+		Str("machine", machine.Hostname).
+		Msg("Machine registration has expired. Sending a authurl to register")
+
+	if registerRequest.Auth.AuthKey != "" {
+		h.handleNoiseAuthKey(w, r, registerRequest)
+
+		return
+	}
+
+	if h.cfg.OIDC.Issuer != "" {
+		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
+	} else {
+		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
+	}
+
+	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
+		Inc()
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(resp)
+}

From c7cea9ef16faa67959a7500be2e31e6c3e838a5c Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 03:07:28 +0200
Subject: [PATCH 08/63] updated paths

---
 noise_api.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/noise_api.go b/noise_api.go
index 475e6022db..4bc28fc43e 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -412,10 +412,10 @@ func (h *Headscale) handleNoiseNodeExpired(
 
 	if h.cfg.OIDC.Issuer != "" {
 		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
+			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
 	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
+		resp.AuthURL = fmt.Sprintf("%s/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
 	}
 
 	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).

From 39b85b02bbef9f0ecd74112524e883d3b3dcbd59 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 03:20:53 +0200
Subject: [PATCH 09/63] Move getMapResponse into reusable function by TS2019
 and TS2021

---
 api.go  | 80 +++++++++++++++++++++++++++++++++------------------------
 poll.go |  4 +--
 2 files changed, 48 insertions(+), 36 deletions(-)

diff --git a/api.go b/api.go
index 82e13c7764..86d77b6abc 100644
--- a/api.go
+++ b/api.go
@@ -396,20 +396,59 @@ func (h *Headscale) RegistrationHandler(
 	}
 }
 
-func (h *Headscale) getMapResponse(
+func (h *Headscale) getLegacyMapResponseData(
 	machineKey key.MachinePublic,
 	mapRequest tailcfg.MapRequest,
 	machine *Machine,
 ) ([]byte, error) {
+	resp, err := h.generateMapResponse(mapRequest, machine)
+	if err != nil {
+		return nil, err
+	}
+
+	var respBody []byte
+	if mapRequest.Compress == "zstd" {
+		src, err := json.Marshal(resp)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "getMapResponse").
+				Err(err).
+				Msg("Failed to marshal response for the client")
+
+			return nil, err
+		}
+
+		encoder, _ := zstd.NewWriter(nil)
+		srcCompressed := encoder.EncodeAll(src, nil)
+		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
+	} else {
+		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, reservedResponseHeaderSize)
+	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
+	data = append(data, respBody...)
+
+	return data, nil
+}
+
+func (h *Headscale) generateMapResponse(
+	mapRequest tailcfg.MapRequest,
+	machine *Machine,
+) (*tailcfg.MapResponse, error) {
 	log.Trace().
-		Str("func", "getMapResponse").
+		Str("func", "generateMapResponse").
 		Str("machine", mapRequest.Hostinfo.Hostname).
 		Msg("Creating Map response")
 	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
 	if err != nil {
 		log.Error().
 			Caller().
-			Str("func", "getMapResponse").
+			Str("func", "generateMapResponse").
 			Err(err).
 			Msg("Cannot convert to node")
 
@@ -420,7 +459,7 @@ func (h *Headscale) getMapResponse(
 	if err != nil {
 		log.Error().
 			Caller().
-			Str("func", "getMapResponse").
+			Str("func", "generateMapResponse").
 			Err(err).
 			Msg("Cannot fetch peers")
 
@@ -433,7 +472,7 @@ func (h *Headscale) getMapResponse(
 	if err != nil {
 		log.Error().
 			Caller().
-			Str("func", "getMapResponse").
+			Str("func", "generateMapResponse").
 			Err(err).
 			Msg("Failed to convert peers to Tailscale nodes")
 
@@ -463,39 +502,12 @@ func (h *Headscale) getMapResponse(
 	}
 
 	log.Trace().
-		Str("func", "getMapResponse").
+		Str("func", "generateMapResponse").
 		Str("machine", mapRequest.Hostinfo.Hostname).
 		// Interface("payload", resp).
 		Msgf("Generated map response: %s", tailMapResponseToString(resp))
 
-	var respBody []byte
-	if mapRequest.Compress == "zstd" {
-		src, err := json.Marshal(resp)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "getMapResponse").
-				Err(err).
-				Msg("Failed to marshal response for the client")
-
-			return nil, err
-		}
-
-		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed := encoder.EncodeAll(src, nil)
-		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
-	} else {
-		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, reservedResponseHeaderSize)
-	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
-	data = append(data, respBody...)
-
-	return data, nil
+	return &resp, nil
 }
 
 func (h *Headscale) getMapKeepAliveResponse(
diff --git a/poll.go b/poll.go
index 9c17b5cbd3..a51c93629c 100644
--- a/poll.go
+++ b/poll.go
@@ -143,7 +143,7 @@ func (h *Headscale) PollNetMapHandler(
 		}
 	}
 
-	data, err := h.getMapResponse(machineKey, mapRequest, machine)
+	data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine)
 	if err != nil {
 		log.Error().
 			Str("handler", "PollNetMap").
@@ -491,7 +491,7 @@ func (h *Headscale) PollNetMapStream(
 					Time("last_successful_update", lastUpdate).
 					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
 					Msgf("There has been updates since the last successful update to %s", machine.Hostname)
-				data, err := h.getMapResponse(machineKey, mapRequest, machine)
+				data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine)
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").

From 9994fce9d5781da62ba783c616e398ec39c56b44 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:00:43 +0200
Subject: [PATCH 10/63] Fixed some linting errors

---
 noise_api.go | 73 +++++++++++++++++++++++++++++-----------------------
 1 file changed, 41 insertions(+), 32 deletions(-)

diff --git a/noise_api.go b/noise_api.go
index 4bc28fc43e..fc0f0911b2 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -14,7 +14,7 @@ import (
 	"tailscale.com/tailcfg"
 )
 
-// // NoiseRegistrationHandler handles the actual registration process of a machine
+// // NoiseRegistrationHandler handles the actual registration process of a machine.
 func (h *Headscale) NoiseRegistrationHandler(
 	writer http.ResponseWriter,
 	req *http.Request,
@@ -175,8 +175,8 @@ func (h *Headscale) NoiseRegistrationHandler(
 }
 
 func (h *Headscale) handleNoiseAuthKey(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	registerRequest tailcfg.RegisterRequest,
 ) {
 	log.Debug().
@@ -194,9 +194,9 @@ func (h *Headscale) handleNoiseAuthKey(
 			Msg("Failed authentication via AuthKey")
 		resp.MachineAuthorized = false
 
-		w.Header().Set("Content-Type", "application/json")
-		w.WriteHeader(http.StatusUnauthorized)
-		json.NewEncoder(w).Encode(resp)
+		writer.Header().Set("Content-Type", "application/json")
+		writer.WriteHeader(http.StatusUnauthorized)
+		json.NewEncoder(writer).Encode(resp)
 
 		log.Error().
 			Caller().
@@ -270,7 +270,7 @@ func (h *Headscale) handleNoiseAuthKey(
 				Msg("could not register machine")
 			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
 				Inc()
-			http.Error(w, "Internal error", http.StatusInternalServerError)
+			http.Error(writer, "Internal error", http.StatusInternalServerError)
 
 			return
 		}
@@ -284,9 +284,9 @@ func (h *Headscale) handleNoiseAuthKey(
 	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
 		Inc()
 
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	json.NewEncoder(writer).Encode(resp)
 
 	log.Info().
 		Caller().
@@ -320,8 +320,8 @@ func (h *Headscale) handleNoiseNodeValidRegistration(
 }
 
 func (h *Headscale) handleNoiseMachineRegistrationNew(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	registerRequest tailcfg.RegisterRequest,
 ) {
 	resp := tailcfg.RegisterResponse{}
@@ -337,18 +337,18 @@ func (h *Headscale) handleNoiseMachineRegistrationNew(
 			NodePublicKeyStripPrefix(registerRequest.NodeKey),
 		)
 	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
+		resp.AuthURL = fmt.Sprintf("%s/register/%s",
 			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
 	}
 
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	json.NewEncoder(writer).Encode(resp)
 }
 
 func (h *Headscale) handleNoiseNodeLogOut(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	machine Machine,
 ) {
 	resp := tailcfg.RegisterResponse{}
@@ -363,14 +363,20 @@ func (h *Headscale) handleNoiseNodeLogOut(
 	resp.MachineAuthorized = false
 	resp.User = *machine.Namespace.toUser()
 
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	err := json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("could not encode response")
+	}
 }
 
 func (h *Headscale) handleNoiseNodeRefreshKey(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	registerRequest tailcfg.RegisterRequest,
 	machine Machine,
 ) {
@@ -385,14 +391,14 @@ func (h *Headscale) handleNoiseNodeRefreshKey(
 	resp.AuthURL = ""
 	resp.User = *machine.Namespace.toUser()
 
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	json.NewEncoder(writer).Encode(resp)
 }
 
 func (h *Headscale) handleNoiseNodeExpired(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	registerRequest tailcfg.RegisterRequest,
 	machine Machine,
 ) {
@@ -405,7 +411,7 @@ func (h *Headscale) handleNoiseNodeExpired(
 		Msg("Machine registration has expired. Sending a authurl to register")
 
 	if registerRequest.Auth.AuthKey != "" {
-		h.handleNoiseAuthKey(w, r, registerRequest)
+		h.handleNoiseAuthKey(writer, req, registerRequest)
 
 		return
 	}
@@ -421,7 +427,10 @@ func (h *Headscale) handleNoiseNodeExpired(
 	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
 		Inc()
 
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	err := json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().Caller().Err(err).Msg("Failed to encode response")
+	}
 }

From 281ae59b5aa266d0110fa49159e370e884f912f2 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:18:33 +0200
Subject: [PATCH 11/63] Update integration tests to work with Noise protocol

---
 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_embedded_derp/config.yaml | 1 +
 4 files changed, 4 insertions(+)

diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml
index e8934230be..e02d706729 100644
--- a/integration_test/etc/alt-config.dump.gold.yaml
+++ b/integration_test/etc/alt-config.dump.gold.yaml
@@ -38,6 +38,7 @@ oidc:
     - email
   strip_email_domain: true
 private_key_path: private.key
+noise_private_key_path: noise_private.key
 server_url: http://headscale:18080
 tls_client_auth_mode: relaxed
 tls_letsencrypt_cache_dir: /var/www/.cache
diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml
index fa1bfcb34c..8a6d7396fd 100644
--- a/integration_test/etc/alt-config.yaml
+++ b/integration_test/etc/alt-config.yaml
@@ -14,6 +14,7 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
+noise_private_key_path: noise_private.key
 listen_addr: 0.0.0.0:18080
 metrics_listen_addr: 127.0.0.1:19090
 server_url: http://headscale:18080
diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml
index 17bb0ca095..f474e89032 100644
--- a/integration_test/etc/config.dump.gold.yaml
+++ b/integration_test/etc/config.dump.gold.yaml
@@ -38,6 +38,7 @@ oidc:
     - email
   strip_email_domain: true
 private_key_path: private.key
+noise_private_key_path: noise_private.key
 server_url: http://headscale:8080
 tls_client_auth_mode: relaxed
 tls_letsencrypt_cache_dir: /var/www/.cache
diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index e6ad3b000d..86946116aa 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -14,6 +14,7 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
+noise_private_key_path: noise_private.key
 listen_addr: 0.0.0.0:8443
 server_url: https://headscale:8443
 tls_cert_path: "/etc/headscale/tls/server.crt"

From ade1b7377972830030e4e3fe84996191e3a7faae Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:35:14 +0200
Subject: [PATCH 12/63] Output an error when a user runs headscale without
 noise_private_key_path defined

---
 cmd/headscale/cli/root.go | 4 ++--
 config.go                 | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go
index 270ca555c5..2c28c584eb 100644
--- a/cmd/headscale/cli/root.go
+++ b/cmd/headscale/cli/root.go
@@ -28,12 +28,12 @@ func initConfig() {
 	if cfgFile != "" {
 		err := headscale.LoadConfig(cfgFile, true)
 		if err != nil {
-			log.Fatal().Caller().Err(err)
+			log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile)
 		}
 	} else {
 		err := headscale.LoadConfig("", false)
 		if err != nil {
-			log.Fatal().Caller().Err(err)
+			log.Fatal().Caller().Err(err).Msgf("Error loading config")
 		}
 	}
 
diff --git a/config.go b/config.go
index a792bab93b..99e7a4cf25 100644
--- a/config.go
+++ b/config.go
@@ -184,6 +184,10 @@ func LoadConfig(path string, isFile bool) error {
 		errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
 	}
 
+	if !viper.IsSet("noise_private_key_path") {
+		errorText += "Fatal config error: headscale now requires a new `noise_private_key_path` field in the config file for the Tailscale v2 protocol\n"
+	}
+
 	if (viper.GetString("tls_letsencrypt_hostname") != "") &&
 		(viper.GetString("tls_letsencrypt_challenge_type") == tlsALPN01ChallengeType) &&
 		(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {

From 3bea20850a7042fd7a8b209f509cef78854fc3f0 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:40:22 +0200
Subject: [PATCH 13/63] Some linting fixes

---
 noise.go     |  5 +++--
 noise_api.go | 11 ++++++-----
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/noise.go b/noise.go
index bd7d4bbd52..53deae3353 100644
--- a/noise.go
+++ b/noise.go
@@ -113,6 +113,7 @@ func (h *Headscale) getNoiseConnection(
 	if err != nil {
 		log.Error().Caller().Err(err).Msgf("Hijack failed")
 		http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError)
+
 		return nil, errCannotHijack
 	}
 	if err := conn.Flush(); err != nil {
@@ -123,12 +124,12 @@ func (h *Headscale) getNoiseConnection(
 
 	netConn = netutil.NewDrainBufConn(netConn, conn.Reader)
 
-	nc, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init)
+	noiseConn, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init)
 	if err != nil {
 		netConn.Close()
 
 		return nil, errNoiseHandshakeFailed
 	}
 
-	return nc, nil
+	return noiseConn, nil
 }
diff --git a/noise_api.go b/noise_api.go
index fc0f0911b2..f20305ca48 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -22,6 +22,7 @@ func (h *Headscale) NoiseRegistrationHandler(
 	log.Trace().Caller().Msgf("Noise registration handler for client %s", req.RemoteAddr)
 	if req.Method != http.MethodPost {
 		http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
+
 		return
 	}
 	body, _ := io.ReadAll(req.Body)
@@ -296,8 +297,8 @@ func (h *Headscale) handleNoiseAuthKey(
 }
 
 func (h *Headscale) handleNoiseNodeValidRegistration(
-	w http.ResponseWriter,
-	r *http.Request,
+	writer http.ResponseWriter,
+	req *http.Request,
 	machine Machine,
 ) {
 	resp := tailcfg.RegisterResponse{}
@@ -314,9 +315,9 @@ func (h *Headscale) handleNoiseNodeValidRegistration(
 
 	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
 		Inc()
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
+	writer.Header().Set("Content-Type", "application/json")
+	writer.WriteHeader(http.StatusOK)
+	json.NewEncoder(writer).Encode(resp)
 }
 
 func (h *Headscale) handleNoiseMachineRegistrationNew(

From eb8d8f142c88dbe71e990c6a7bbaba031f4217c2 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:44:07 +0200
Subject: [PATCH 14/63] And more linting stuff

---
 noise.go     |  5 ++++-
 noise_api.go | 46 +++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 45 insertions(+), 6 deletions(-)

diff --git a/noise.go b/noise.go
index 53deae3353..16754c6997 100644
--- a/noise.go
+++ b/noise.go
@@ -56,7 +56,10 @@ func (h *Headscale) NoiseUpgradeHandler(
 
 	server := http.Server{}
 	server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{})
-	server.Serve(netutil.NewOneConnListener(noiseConn, nil))
+	err = server.Serve(netutil.NewOneConnListener(noiseConn, nil))
+	if err != nil {
+		log.Error().Err(err).Msg("noise server launch failed")
+	}
 }
 
 // getNoiseConnection is basically AcceptHTTP from tailscale
diff --git a/noise_api.go b/noise_api.go
index f20305ca48..29ab98dfd6 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -197,7 +197,13 @@ func (h *Headscale) handleNoiseAuthKey(
 
 		writer.Header().Set("Content-Type", "application/json")
 		writer.WriteHeader(http.StatusUnauthorized)
-		json.NewEncoder(writer).Encode(resp)
+		err = json.NewEncoder(writer).Encode(resp)
+		if err != nil {
+			log.Error().
+				Caller().
+				Err(err).
+				Msg("Failed to encode response")
+		}
 
 		log.Error().
 			Caller().
@@ -234,7 +240,16 @@ func (h *Headscale) handleNoiseAuthKey(
 
 		machine.NodeKey = nodeKey
 		machine.AuthKeyID = uint(pak.ID)
-		h.RefreshMachine(machine, registerRequest.Expiry)
+		err = h.RefreshMachine(machine, registerRequest.Expiry)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("machine", machine.Hostname).
+				Err(err).
+				Msg("Failed to refresh machine")
+
+			return
+		}
 	} else {
 		now := time.Now().UTC()
 
@@ -277,7 +292,18 @@ func (h *Headscale) handleNoiseAuthKey(
 		}
 	}
 
-	h.UsePreAuthKey(pak)
+	err = h.UsePreAuthKey(pak)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to use pre-auth key")
+		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+			Inc()
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
 
 	resp.MachineAuthorized = true
 	resp.User = *pak.Namespace.toUser()
@@ -358,7 +384,17 @@ func (h *Headscale) handleNoiseNodeLogOut(
 		Str("machine", machine.Hostname).
 		Msg("Client requested logout")
 
-	h.ExpireMachine(&machine)
+	err := h.ExpireMachine(&machine)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("func", "handleMachineLogOut").
+			Err(err).
+			Msg("Failed to expire machine")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
 
 	resp.AuthURL = ""
 	resp.MachineAuthorized = false
@@ -366,7 +402,7 @@ func (h *Headscale) handleNoiseNodeLogOut(
 
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(http.StatusOK)
-	err := json.NewEncoder(writer).Encode(resp)
+	err = json.NewEncoder(writer).Encode(resp)
 	if err != nil {
 		log.Error().
 			Caller().

From 20d2615081e79a40c711476a4464c78dddc59b51 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 12:47:04 +0200
Subject: [PATCH 15/63] Check json encoder errors

---
 noise_api.go | 34 ++++++++++++++++++++++++++++++----
 1 file changed, 30 insertions(+), 4 deletions(-)

diff --git a/noise_api.go b/noise_api.go
index 29ab98dfd6..6bf555e6c9 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -313,7 +313,15 @@ func (h *Headscale) handleNoiseAuthKey(
 
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(http.StatusOK)
-	json.NewEncoder(writer).Encode(resp)
+	err = json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to encode response")
+
+		return
+	}
 
 	log.Info().
 		Caller().
@@ -343,7 +351,13 @@ func (h *Headscale) handleNoiseNodeValidRegistration(
 		Inc()
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(http.StatusOK)
-	json.NewEncoder(writer).Encode(resp)
+	err := json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to encode response")
+	}
 }
 
 func (h *Headscale) handleNoiseMachineRegistrationNew(
@@ -370,7 +384,13 @@ func (h *Headscale) handleNoiseMachineRegistrationNew(
 
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(http.StatusOK)
-	json.NewEncoder(writer).Encode(resp)
+	err := json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to encode response")
+	}
 }
 
 func (h *Headscale) handleNoiseNodeLogOut(
@@ -430,7 +450,13 @@ func (h *Headscale) handleNoiseNodeRefreshKey(
 
 	writer.Header().Set("Content-Type", "application/json")
 	writer.WriteHeader(http.StatusOK)
-	json.NewEncoder(writer).Encode(resp)
+	err := json.NewEncoder(writer).Encode(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to encode response")
+	}
 }
 
 func (h *Headscale) handleNoiseNodeExpired(

From ff46f3ff494aff9d13c367a735db537e7a073697 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 16:13:17 +0200
Subject: [PATCH 16/63] Move reusable method to common api file

---
 api.go        | 74 -----------------------------------------------
 api_common.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 80 insertions(+), 74 deletions(-)
 create mode 100644 api_common.go

diff --git a/api.go b/api.go
index 86d77b6abc..9006e759b3 100644
--- a/api.go
+++ b/api.go
@@ -436,80 +436,6 @@ func (h *Headscale) getLegacyMapResponseData(
 	return data, nil
 }
 
-func (h *Headscale) generateMapResponse(
-	mapRequest tailcfg.MapRequest,
-	machine *Machine,
-) (*tailcfg.MapResponse, error) {
-	log.Trace().
-		Str("func", "generateMapResponse").
-		Str("machine", mapRequest.Hostinfo.Hostname).
-		Msg("Creating Map response")
-	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "generateMapResponse").
-			Err(err).
-			Msg("Cannot convert to node")
-
-		return nil, err
-	}
-
-	peers, err := h.getValidPeers(machine)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "generateMapResponse").
-			Err(err).
-			Msg("Cannot fetch peers")
-
-		return nil, err
-	}
-
-	profiles := getMapResponseUserProfiles(*machine, peers)
-
-	nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "generateMapResponse").
-			Err(err).
-			Msg("Failed to convert peers to Tailscale nodes")
-
-		return nil, err
-	}
-
-	dnsConfig := getMapResponseDNSConfig(
-		h.cfg.DNSConfig,
-		h.cfg.BaseDomain,
-		*machine,
-		peers,
-	)
-
-	resp := tailcfg.MapResponse{
-		KeepAlive:    false,
-		Node:         node,
-		Peers:        nodePeers,
-		DNSConfig:    dnsConfig,
-		Domain:       h.cfg.BaseDomain,
-		PacketFilter: h.aclRules,
-		DERPMap:      h.DERPMap,
-		UserProfiles: profiles,
-		Debug: &tailcfg.Debug{
-			DisableLogTail:      !h.cfg.LogTail.Enabled,
-			RandomizeClientPort: h.cfg.RandomizeClientPort,
-		},
-	}
-
-	log.Trace().
-		Str("func", "generateMapResponse").
-		Str("machine", mapRequest.Hostinfo.Hostname).
-		// Interface("payload", resp).
-		Msgf("Generated map response: %s", tailMapResponseToString(resp))
-
-	return &resp, nil
-}
-
 func (h *Headscale) getMapKeepAliveResponse(
 	machineKey key.MachinePublic,
 	mapRequest tailcfg.MapRequest,
diff --git a/api_common.go b/api_common.go
new file mode 100644
index 0000000000..5ffbed0274
--- /dev/null
+++ b/api_common.go
@@ -0,0 +1,80 @@
+package headscale
+
+import (
+	"github.com/rs/zerolog/log"
+	"tailscale.com/tailcfg"
+)
+
+func (h *Headscale) generateMapResponse(
+	mapRequest tailcfg.MapRequest,
+	machine *Machine,
+) (*tailcfg.MapResponse, error) {
+	log.Trace().
+		Str("func", "generateMapResponse").
+		Str("machine", mapRequest.Hostinfo.Hostname).
+		Msg("Creating Map response")
+	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("func", "generateMapResponse").
+			Err(err).
+			Msg("Cannot convert to node")
+
+		return nil, err
+	}
+
+	peers, err := h.getValidPeers(machine)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("func", "generateMapResponse").
+			Err(err).
+			Msg("Cannot fetch peers")
+
+		return nil, err
+	}
+
+	profiles := getMapResponseUserProfiles(*machine, peers)
+
+	nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("func", "generateMapResponse").
+			Err(err).
+			Msg("Failed to convert peers to Tailscale nodes")
+
+		return nil, err
+	}
+
+	dnsConfig := getMapResponseDNSConfig(
+		h.cfg.DNSConfig,
+		h.cfg.BaseDomain,
+		*machine,
+		peers,
+	)
+
+	resp := tailcfg.MapResponse{
+		KeepAlive:    false,
+		Node:         node,
+		Peers:        nodePeers,
+		DNSConfig:    dnsConfig,
+		Domain:       h.cfg.BaseDomain,
+		PacketFilter: h.aclRules,
+		DERPMap:      h.DERPMap,
+		UserProfiles: profiles,
+		Debug: &tailcfg.Debug{
+			DisableLogTail:      !h.cfg.LogTail.Enabled,
+			RandomizeClientPort: h.cfg.RandomizeClientPort,
+		},
+	}
+
+	log.Trace().
+		Str("func", "generateMapResponse").
+		Str("machine", mapRequest.Hostinfo.Hostname).
+		// Interface("payload", resp).
+		Msgf("Generated map response: %s", tailMapResponseToString(resp))
+
+	return &resp, nil
+}

From cab828c9d49dfa2fdf8c94e9156719befd2b58f6 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 16:52:57 +0200
Subject: [PATCH 17/63] Fixed unit tests to load config

---
 cmd/headscale/headscale_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go
index 917ae432cb..b0667f7813 100644
--- a/cmd/headscale/headscale_test.go
+++ b/cmd/headscale/headscale_test.go
@@ -165,7 +165,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
 	// defer os.RemoveAll(tmpDir)
 
 	configYaml := []byte(
-		"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"",
+		"---\nnoise_private_key_path: \"noise_private.key\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"\n",
 	)
 	writeConfig(c, tmpDir, configYaml)
 
@@ -192,7 +192,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
 
 	// Check configuration validation errors (2)
 	configYaml = []byte(
-		"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
+		"---\nnoise_private_key_path: \"noise_private.key\"\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
 	)
 	writeConfig(c, tmpDir, configYaml)
 	err = headscale.LoadConfig(tmpDir, false)

From 78a179c9719dc5d2c2e6de0ed7812d8d2163fefc Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 16:53:54 +0200
Subject: [PATCH 18/63] Minor update in docs

---
 docs/running-headscale-container.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md
index b36f3bbff8..4a9f15188c 100644
--- a/docs/running-headscale-container.md
+++ b/docs/running-headscale-container.md
@@ -53,6 +53,8 @@ server_url: http://your-host-name:8080 # Change to your hostname or host IP
 metrics_listen_addr: 0.0.0.0:9090
 # The default /var/lib/headscale path is not writable in the container
 private_key_path: /etc/headscale/private.key
+# The default /var/lib/headscale path is not writable in the container
+noise_private_key_path: /var/lib/headscale/noise_private.key
 # The default /var/lib/headscale path is not writable  in the container
 db_path: /etc/headscale/db.sqlite
 ```

From 0d0042b7e6a61d0cf6564351c68ec76e1910c01b Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 17:04:07 +0200
Subject: [PATCH 19/63] Added zstd constant for linting

---
 api.go   | 4 ++--
 utils.go | 2 ++
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/api.go b/api.go
index 9006e759b3..6324cca12e 100644
--- a/api.go
+++ b/api.go
@@ -407,7 +407,7 @@ func (h *Headscale) getLegacyMapResponseData(
 	}
 
 	var respBody []byte
-	if mapRequest.Compress == "zstd" {
+	if mapRequest.Compress == ZstdCompression {
 		src, err := json.Marshal(resp)
 		if err != nil {
 			log.Error().
@@ -445,7 +445,7 @@ func (h *Headscale) getMapKeepAliveResponse(
 	}
 	var respBody []byte
 	var err error
-	if mapRequest.Compress == "zstd" {
+	if mapRequest.Compress == ZstdCompression {
 		src, err := json.Marshal(mapResponse)
 		if err != nil {
 			log.Error().
diff --git a/utils.go b/utils.go
index b43625359d..089e867b22 100644
--- a/utils.go
+++ b/utils.go
@@ -59,6 +59,8 @@ const (
 	privateHexPrefix = "privkey:"
 
 	PermissionFallback = 0o700
+
+	ZstdCompression = "zstd"
 )
 
 func MachinePublicKeyStripPrefix(machineKey key.MachinePublic) string {

From c10142f7670655cd70b7f42c1503d18b64ec5ac3 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 17:05:04 +0200
Subject: [PATCH 20/63] Added noise poll handler

---
 app.go        |   2 +-
 noise_poll.go | 698 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 699 insertions(+), 1 deletion(-)
 create mode 100644 noise_poll.go

diff --git a/app.go b/app.go
index 9c0f11d49b..f632264c44 100644
--- a/app.go
+++ b/app.go
@@ -467,7 +467,7 @@ func (h *Headscale) createNoiseMux() *mux.Router {
 	router := mux.NewRouter()
 
 	router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
-	//router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost)
+	router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost)
 
 	return router
 }
diff --git a/noise_poll.go b/noise_poll.go
new file mode 100644
index 0000000000..1489fff0ab
--- /dev/null
+++ b/noise_poll.go
@@ -0,0 +1,698 @@
+package headscale
+
+import (
+	"context"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/klauspost/compress/zstd"
+	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
+	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
+)
+
+// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
+//
+// This is the busiest endpoint, as it keeps the HTTP long poll that updates
+// the clients when something in the network changes.
+//
+// The clients POST stuff like HostInfo and their Endpoints here, but
+// 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) NoisePollNetMapHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	log.Trace().
+		Str("handler", "NoisePollNetMap").
+		Msg("PollNetMapHandler called")
+	body, _ := io.ReadAll(req.Body)
+
+	mapRequest := tailcfg.MapRequest{}
+	if err := json.Unmarshal(body, &mapRequest); err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse MapRequest")
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+
+	machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{})
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			log.Warn().
+				Str("handler", "NoisePollNetMap").
+				Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String())
+			http.Error(writer, "Internal error", http.StatusNotFound)
+
+			return
+		}
+		log.Error().
+			Str("handler", "NoisePollNetMap").
+			Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String())
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+	log.Trace().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Found machine in database")
+
+	machine.Hostname = mapRequest.Hostinfo.Hostname
+	machine.HostInfo = HostInfo(*mapRequest.Hostinfo)
+	machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
+	now := time.Now().UTC()
+
+	// update ACLRules with peer informations (to update server tags if necessary)
+	if h.aclPolicy != nil {
+		err = h.UpdateACLRules()
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "handleAuthKey").
+				Str("machine", machine.Hostname).
+				Err(err)
+		}
+	}
+
+	// From Tailscale client:
+	//
+	// ReadOnly is whether the client just wants to fetch the MapResponse,
+	// without updating their Endpoints. The Endpoints field will be ignored and
+	// LastSeen will not be updated and peers will not be notified of changes.
+	//
+	// The intended use is for clients to discover the DERP map at start-up
+	// before their first real endpoint update.
+	if !mapRequest.ReadOnly {
+		machine.Endpoints = mapRequest.Endpoints
+		machine.LastSeen = &now
+	}
+
+	if err := h.db.Updates(machine).Error; err != nil {
+		if err != nil {
+			log.Error().
+				Str("handler", "NoisePollNetMap").
+				Str("machine", machine.Hostname).
+				Err(err).
+				Msg("Failed to persist/update machine in the database")
+			http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+			return
+		}
+	}
+
+	resp, err := h.getNoiseMapResponse(mapRequest, machine)
+	if err != nil {
+		log.Error().
+			Str("handler", "NoisePollNetMap").
+			Str("machine", machine.Hostname).
+			Err(err).
+			Msg("Failed to get Map response")
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+
+	// We update our peers if the client is not sending ReadOnly in the MapRequest
+	// so we don't distribute its initial request (it comes with
+	// empty endpoints to peers)
+
+	// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
+	log.Debug().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Bool("readOnly", mapRequest.ReadOnly).
+		Bool("omitPeers", mapRequest.OmitPeers).
+		Bool("stream", mapRequest.Stream).
+		Msg("Noise client map request processed")
+
+	if mapRequest.ReadOnly {
+		log.Info().
+			Str("handler", "NoisePollNetMap").
+			Str("machine", machine.Hostname).
+			Msg("Client is starting up. Probably interested in a DERP map")
+		// w.Header().Set("Content-Type", "application/json")
+		// w.WriteHeader(http.StatusOK)
+		_, err = writer.Write(resp)
+		if err != nil {
+			log.Warn().Msgf("Could not send JSON response: %s", err)
+		}
+		if f, ok := writer.(http.Flusher); ok {
+			f.Flush()
+		}
+
+		log.Info().Msgf("Noise client map response sent for %s (len %d)", machine.Hostname, len(resp))
+
+		return
+	}
+
+	// There has been an update to _any_ of the nodes that the other nodes would
+	// need to know about
+	h.setLastStateChangeToNow(machine.Namespace.Name)
+
+	// The request is not ReadOnly, so we need to set up channels for updating
+	// peers via longpoll
+
+	// Only create update channel if it has not been created
+	log.Trace().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Loading or creating update channel")
+
+	const chanSize = 8
+	updateChan := make(chan struct{}, chanSize)
+
+	pollDataChan := make(chan []byte, chanSize)
+	defer closeChanWithLog(pollDataChan, machine.Hostname, "pollDataChan")
+
+	keepAliveChan := make(chan []byte)
+
+	if mapRequest.OmitPeers && !mapRequest.Stream {
+		log.Info().
+			Str("handler", "NoisePollNetMap").
+			Str("machine", machine.Hostname).
+			Msg("Client sent endpoint update and is ok with a response without peer list")
+
+		_, err := writer.Write(resp)
+		if err != nil {
+			log.Warn().Msgf("Could not send response: %s", err)
+
+			return
+		}
+
+		if f, ok := writer.(http.Flusher); ok {
+			f.Flush()
+		}
+
+		// 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(machine.Namespace.Name, machine.Hostname, "endpoint-update").
+			Inc()
+		updateChan <- struct{}{}
+
+		return
+	} else if mapRequest.OmitPeers && mapRequest.Stream {
+		log.Warn().
+			Str("handler", "NoisePollNetMap").
+			Str("machine", machine.Hostname).
+			Msg("Ignoring request, don't know how to handle it")
+		http.Error(writer, "Internal error", http.StatusBadRequest)
+
+		return
+	}
+
+	log.Info().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Client is ready to access the tailnet")
+	log.Info().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Sending initial map")
+	pollDataChan <- resp
+
+	log.Info().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Notifying peers")
+	updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update").
+		Inc()
+	updateChan <- struct{}{}
+
+	h.NoisePollNetMapStream(
+		writer,
+		req,
+		machine,
+		mapRequest,
+		pollDataChan,
+		keepAliveChan,
+		updateChan,
+	)
+
+	log.Trace().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Finished stream, closing PollNetMap session")
+}
+
+// PollNetMapStream takes care of /machine/:id/map
+// stream logic, ensuring we communicate updates and data
+// to the connected clients.
+func (h *Headscale) NoisePollNetMapStream(
+	writer http.ResponseWriter,
+	req *http.Request,
+	machine *Machine,
+	mapRequest tailcfg.MapRequest,
+	pollDataChan chan []byte,
+	keepAliveChan chan []byte,
+	updateChan chan struct{},
+) {
+	ctx := context.WithValue(context.Background(), machineNameContextKey, machine.Hostname)
+
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	go h.noiseScheduledPollWorker(
+		ctx,
+		updateChan,
+		keepAliveChan,
+		mapRequest,
+		machine,
+	)
+
+	for {
+		log.Trace().
+			Str("handler", "NoisePollNetMapStream").
+			Str("machine", machine.Hostname).
+			Msg("Waiting for data to stream...")
+
+		log.Trace().
+			Str("handler", "NoisePollNetMapStream").
+			Str("machine", machine.Hostname).
+			Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
+
+		select {
+		case data := <-pollDataChan:
+			log.Trace().
+				Str("handler", "NoisePollNetMapStream").
+				Str("machine", machine.Hostname).
+				Str("channel", "pollData").
+				Int("bytes", len(data)).
+				Msg("Sending data received via pollData channel")
+			_, err := writer.Write(data)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "pollData").
+					Err(err).
+					Msg("Cannot write data")
+
+				break
+			}
+			if f, ok := writer.(http.Flusher); ok {
+				f.Flush()
+			}
+			log.Trace().
+				Str("handler", "NoisePollNetMapStream").
+				Str("machine", machine.Hostname).
+				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.UpdateMachineFromDatabase(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "pollData").
+					Err(err).
+					Msg("Cannot update machine from database")
+
+				// client has been removed from database
+				// since the stream opened, terminate connection.
+				break
+			}
+			now := time.Now().UTC()
+			machine.LastSeen = &now
+
+			lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname).
+				Set(float64(now.Unix()))
+			machine.LastSuccessfulUpdate = &now
+
+			err = h.TouchMachine(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "pollData").
+					Err(err).
+					Msg("Cannot update machine LastSuccessfulUpdate")
+			} else {
+				log.Trace().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "pollData").
+					Int("bytes", len(data)).
+					Msg("Machine entry in database updated successfully after sending pollData")
+			}
+
+			break
+
+		case data := <-keepAliveChan:
+			log.Trace().
+				Str("handler", "NoisePollNetMapStream").
+				Str("machine", machine.Hostname).
+				Str("channel", "keepAlive").
+				Int("bytes", len(data)).
+				Msg("Sending keep alive message")
+
+			_, err := writer.Write(data)
+			if f, ok := writer.(http.Flusher); ok {
+				f.Flush()
+			}
+
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "keepAlive").
+					Err(err).
+					Msg("Cannot write keep alive message")
+
+				break
+			}
+			log.Trace().
+				Str("handler", "NoisePollNetMapStream").
+				Str("machine", machine.Hostname).
+				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.UpdateMachineFromDatabase(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "keepAlive").
+					Err(err).
+					Msg("Cannot update machine from database")
+
+				// client has been removed from database
+				// since the stream opened, terminate connection.
+				break
+			}
+			now := time.Now().UTC()
+			machine.LastSeen = &now
+			err = h.TouchMachine(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "keepAlive").
+					Err(err).
+					Msg("Cannot update machine LastSeen")
+			} else {
+				log.Trace().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "keepAlive").
+					Int("bytes", len(data)).
+					Msg("Machine updated successfully after sending keep alive")
+			}
+
+			break
+
+		case <-updateChan:
+			log.Trace().
+				Str("handler", "NoisePollNetMapStream").
+				Str("machine", machine.Hostname).
+				Str("channel", "update").
+				Msg("Received a request for update")
+			updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname).
+				Inc()
+			if h.isOutdated(machine) {
+				var lastUpdate time.Time
+				if machine.LastSuccessfulUpdate != nil {
+					lastUpdate = *machine.LastSuccessfulUpdate
+				}
+				log.Debug().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Time("last_successful_update", lastUpdate).
+					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
+					Msgf("There has been updates since the last successful update to %s", machine.Hostname)
+				data, err := h.getNoiseMapResponse(mapRequest, machine)
+				if err != nil {
+					log.Error().
+						Str("handler", "NoisePollNetMapStream").
+						Str("machine", machine.Hostname).
+						Str("channel", "update").
+						Err(err).
+						Msg("Could not get the map update")
+				}
+				_, err = writer.Write(data)
+				if err != nil {
+					log.Error().
+						Str("handler", "NoisePollNetMapStream").
+						Str("machine", machine.Hostname).
+						Str("channel", "update").
+						Err(err).
+						Msg("Could not write the map response")
+					updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed").
+						Inc()
+
+					break
+				}
+
+				if f, ok := writer.(http.Flusher); ok {
+					f.Flush()
+				}
+
+				log.Trace().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "update").
+					Msg("Updated Map has been sent")
+				updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "success").
+					Inc()
+
+				// Keep track of the last successful update,
+				// we sometimes end in a state were the update
+				// is not picked up by a client and we use this
+				// to determine if we should "force" an update.
+				// 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().
+						Str("handler", "NoisePollNetMapStream").
+						Str("machine", machine.Hostname).
+						Str("channel", "update").
+						Err(err).
+						Msg("Cannot update machine from database")
+
+					// client has been removed from database
+					// since the stream opened, terminate connection.
+					break
+				}
+				now := time.Now().UTC()
+
+				lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname).
+					Set(float64(now.Unix()))
+				machine.LastSuccessfulUpdate = &now
+
+				err = h.TouchMachine(machine)
+				if err != nil {
+					log.Error().
+						Str("handler", "NoisePollNetMapStream").
+						Str("machine", machine.Hostname).
+						Str("channel", "update").
+						Err(err).
+						Msg("Cannot update machine LastSuccessfulUpdate")
+				}
+			} else {
+				var lastUpdate time.Time
+				if machine.LastSuccessfulUpdate != nil {
+					lastUpdate = *machine.LastSuccessfulUpdate
+				}
+				log.Trace().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Time("last_successful_update", lastUpdate).
+					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
+					Msgf("%s is up to date", machine.Hostname)
+			}
+
+			break
+
+		case <-ctx.Done():
+			log.Info().
+				Str("handler", "NoisePollNetMapStream").
+				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.
+			err := h.UpdateMachineFromDatabase(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "Done").
+					Err(err).
+					Msg("Cannot update machine from database")
+
+				// client has been removed from database
+				// since the stream opened, terminate connection.
+				break
+			}
+			now := time.Now().UTC()
+			machine.LastSeen = &now
+			err = h.TouchMachine(machine)
+			if err != nil {
+				log.Error().
+					Str("handler", "NoisePollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "Done").
+					Err(err).
+					Msg("Cannot update machine LastSeen")
+			}
+
+			break
+		}
+	}
+}
+
+func (h *Headscale) noiseScheduledPollWorker(
+	ctx context.Context,
+	updateChan chan struct{},
+	keepAliveChan chan []byte,
+	mapRequest tailcfg.MapRequest,
+	machine *Machine,
+) {
+	keepAliveTicker := time.NewTicker(keepAliveInterval)
+	updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
+
+	defer closeChanWithLog(
+		updateChan,
+		fmt.Sprint(ctx.Value(machineNameContextKey)),
+		"updateChan",
+	)
+	defer closeChanWithLog(
+		keepAliveChan,
+		fmt.Sprint(ctx.Value(machineNameContextKey)),
+		"updateChan",
+	)
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+
+		case <-keepAliveTicker.C:
+			data, err := h.getNoiseMapKeepAliveResponse(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", machine.Hostname).
+				Msg("Sending keepalive")
+			keepAliveChan <- data
+
+		case <-updateCheckerTicker.C:
+			log.Debug().
+				Str("func", "scheduledPollWorker").
+				Str("machine", machine.Hostname).
+				Msg("Sending update request")
+			updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update").
+				Inc()
+			updateChan <- struct{}{}
+		}
+	}
+}
+
+func (h *Headscale) getNoiseMapKeepAliveResponse(req tailcfg.MapRequest) ([]byte, error) {
+	resp := tailcfg.MapResponse{
+		KeepAlive: true,
+	}
+
+	// The TS2021 protocol does not rely anymore on the machine key to
+	// encrypt in a NaCl box the map response. We just send it back
+	// unencrypted via the encrypted Noise channel.
+	// declare the incoming size on the first 4 bytes
+	respBody, err := json.Marshal(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot marshal map response")
+	}
+
+	var srcCompressed []byte
+	if req.Compress == ZstdCompression {
+		encoder, _ := zstd.NewWriter(nil)
+		srcCompressed = encoder.EncodeAll(respBody, nil)
+	} else {
+		srcCompressed = respBody
+	}
+
+	data := make([]byte, reservedResponseHeaderSize)
+	binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
+	data = append(data, srcCompressed...)
+
+	return data, nil
+}
+
+func (h *Headscale) getNoiseMapResponse(
+	req tailcfg.MapRequest,
+	machine *Machine,
+) ([]byte, error) {
+	log.Trace().
+		Str("func", "getNoiseMapResponse").
+		Str("machine", req.Hostinfo.Hostname).
+		Msg("Creating Map response")
+
+	resp, err := h.generateMapResponse(req, machine)
+	if err != nil {
+		log.Error().
+			Str("func", "getNoiseMapResponse").
+			Err(err).
+			Msg("Error generating the map response")
+
+		return nil, err
+	}
+
+	log.Trace().
+		Str("func", "getNoiseMapResponse").
+		Str("machine", req.Hostinfo.Hostname).
+		Msgf("Generated map response: %s", tailMapResponseToString(*resp))
+
+	// The TS2021 protocol does not rely anymore on the machine key to
+	// encrypt in a NaCl box the map response. We just send it back
+	// unencrypted via the encrypted Noise channel.
+	// declare the incoming size on the first 4 bytes
+	respBody, err := json.Marshal(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot marshal map response")
+	}
+
+	var srcCompressed []byte
+	if req.Compress == ZstdCompression {
+		encoder, _ := zstd.NewWriter(nil)
+		srcCompressed = encoder.EncodeAll(respBody, nil)
+	} else {
+		srcCompressed = respBody
+	}
+
+	data := make([]byte, reservedResponseHeaderSize)
+	binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
+	data = append(data, srcCompressed...)
+
+	return data, nil
+}

From 0f09e19e38fe2fa4c6156a9c8a8450817b9c5c2f Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 17:09:14 +0200
Subject: [PATCH 21/63] Updated go.mod checksum

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

diff --git a/flake.nix b/flake.nix
index 0e8bdc9448..0c257ff53e 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-RzmnAh81BN4tbzAGzJbb6CMuws8kuPJDw7aPkRRnSS8=";
+              vendorSha256 = "sha256-1VYegqEearzbqEX8ZLbsvHrRKbM/HIm/XIqQjMbvxkA=";
 
               ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
             };

From aaa33cf09345bdfba43e797cc8fd6efc66dbd411 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:07:05 +0200
Subject: [PATCH 22/63] Minor change in router

---
 api.go | 1 +
 app.go | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/api.go b/api.go
index 27cbede4aa..5ac0d3d969 100644
--- a/api.go
+++ b/api.go
@@ -679,6 +679,7 @@ func (h *Headscale) handleMachineRegistrationNew(
 
 	// The machine registration is new, redirect the client to the registration URL
 	log.Debug().
+		Caller().
 		Str("machine", registerRequest.Hostinfo.Hostname).
 		Msg("The node seems to be new, sending auth url")
 	if h.cfg.OIDC.Issuer != "" {
diff --git a/app.go b/app.go
index f632264c44..a5015c98f8 100644
--- a/app.go
+++ b/app.go
@@ -467,7 +467,7 @@ func (h *Headscale) createNoiseMux() *mux.Router {
 	router := mux.NewRouter()
 
 	router.HandleFunc("/machine/register", h.NoiseRegistrationHandler).Methods(http.MethodPost)
-	router.HandleFunc("/machine/map", h.NoisePollNetMapHandler).Methods(http.MethodPost)
+	router.HandleFunc("/machine/map", h.NoisePollNetMapHandler)
 
 	return router
 }

From ab18c721bb45143e93aa04f3d42fe5e9abe6dd42 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:07:29 +0200
Subject: [PATCH 23/63] Support for Noise machines in getPeers

---
 machine.go | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/machine.go b/machine.go
index b436fd8796..d9fd789d1a 100644
--- a/machine.go
+++ b/machine.go
@@ -600,11 +600,14 @@ func (machine Machine) toNode(
 	}
 
 	var machineKey key.MachinePublic
-	err = machineKey.UnmarshalText(
-		[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
-	)
-	if err != nil {
-		return nil, fmt.Errorf("failed to parse machine public key: %w", err)
+	if machine.MachineKey != "" {
+		// MachineKey is only used in the legacy protocol
+		err = machineKey.UnmarshalText(
+			[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
+		)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse machine public key: %w", err)
+		}
 	}
 
 	var discoKey key.DiscoPublic

From e640c6df05d6cde2d484314a89359f33f2124c48 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:10:08 +0200
Subject: [PATCH 24/63] Fixes in Noise poll (clients should work now)

---
 noise_api.go  |  3 ++-
 noise_poll.go | 56 +++++++++++++++++++++++++++++++++------------------
 2 files changed, 38 insertions(+), 21 deletions(-)

diff --git a/noise_api.go b/noise_api.go
index 6bf555e6c9..98a66d226c 100644
--- a/noise_api.go
+++ b/noise_api.go
@@ -369,8 +369,9 @@ func (h *Headscale) handleNoiseMachineRegistrationNew(
 
 	// The machine registration is new, redirect the client to the registration URL
 	log.Debug().
+		Caller().
 		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msg("The node is sending us a new NodeKey, sending auth url")
+		Msg("The node seems to be new, sending auth url")
 	if h.cfg.OIDC.Issuer != "" {
 		resp.AuthURL = fmt.Sprintf(
 			"%s/oidc/register/%s",
diff --git a/noise_poll.go b/noise_poll.go
index 1489fff0ab..1d9dfe373f 100644
--- a/noise_poll.go
+++ b/noise_poll.go
@@ -298,10 +298,19 @@ func (h *Headscale) NoisePollNetMapStream(
 					Err(err).
 					Msg("Cannot write data")
 
-				break
+				return
 			}
-			if f, ok := writer.(http.Flusher); ok {
-				f.Flush()
+
+			flusher, ok := writer.(http.Flusher)
+			if !ok {
+				log.Error().
+					Caller().
+					Str("handler", "PollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "pollData").
+					Msg("Cannot cast writer to http.Flusher")
+			} else {
+				flusher.Flush()
 			}
 			log.Trace().
 				Str("handler", "NoisePollNetMapStream").
@@ -323,7 +332,7 @@ func (h *Headscale) NoisePollNetMapStream(
 
 				// client has been removed from database
 				// since the stream opened, terminate connection.
-				break
+				return
 			}
 			now := time.Now().UTC()
 			machine.LastSeen = &now
@@ -353,27 +362,34 @@ func (h *Headscale) NoisePollNetMapStream(
 
 		case data := <-keepAliveChan:
 			log.Trace().
-				Str("handler", "NoisePollNetMapStream").
+				Str("handler", "PollNetMapStream").
 				Str("machine", machine.Hostname).
 				Str("channel", "keepAlive").
 				Int("bytes", len(data)).
 				Msg("Sending keep alive message")
-
 			_, err := writer.Write(data)
-			if f, ok := writer.(http.Flusher); ok {
-				f.Flush()
-			}
-
 			if err != nil {
 				log.Error().
-					Str("handler", "NoisePollNetMapStream").
+					Str("handler", "PollNetMapStream").
 					Str("machine", machine.Hostname).
 					Str("channel", "keepAlive").
 					Err(err).
 					Msg("Cannot write keep alive message")
 
-				break
+				return
 			}
+			flusher, ok := writer.(http.Flusher)
+			if !ok {
+				log.Error().
+					Caller().
+					Str("handler", "PollNetMapStream").
+					Str("machine", machine.Hostname).
+					Str("channel", "keepAlive").
+					Msg("Cannot cast writer to http.Flusher")
+			} else {
+				flusher.Flush()
+			}
+
 			log.Trace().
 				Str("handler", "NoisePollNetMapStream").
 				Str("machine", machine.Hostname).
@@ -394,7 +410,7 @@ func (h *Headscale) NoisePollNetMapStream(
 
 				// client has been removed from database
 				// since the stream opened, terminate connection.
-				break
+				return
 			}
 			now := time.Now().UTC()
 			machine.LastSeen = &now
@@ -415,7 +431,7 @@ func (h *Headscale) NoisePollNetMapStream(
 					Msg("Machine updated successfully after sending keep alive")
 			}
 
-			break
+			return
 
 		case <-updateChan:
 			log.Trace().
@@ -456,7 +472,7 @@ func (h *Headscale) NoisePollNetMapStream(
 					updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed").
 						Inc()
 
-					break
+					return
 				}
 
 				if f, ok := writer.(http.Flusher); ok {
@@ -489,7 +505,7 @@ func (h *Headscale) NoisePollNetMapStream(
 
 					// client has been removed from database
 					// since the stream opened, terminate connection.
-					break
+					return
 				}
 				now := time.Now().UTC()
 
@@ -519,7 +535,7 @@ func (h *Headscale) NoisePollNetMapStream(
 					Msgf("%s is up to date", machine.Hostname)
 			}
 
-			break
+			return
 
 		case <-ctx.Done():
 			log.Info().
@@ -540,7 +556,7 @@ func (h *Headscale) NoisePollNetMapStream(
 
 				// client has been removed from database
 				// since the stream opened, terminate connection.
-				break
+				return
 			}
 			now := time.Now().UTC()
 			machine.LastSeen = &now
@@ -554,7 +570,7 @@ func (h *Headscale) NoisePollNetMapStream(
 					Msg("Cannot update machine LastSeen")
 			}
 
-			break
+			return
 		}
 	}
 }
@@ -606,7 +622,7 @@ func (h *Headscale) noiseScheduledPollWorker(
 			log.Debug().
 				Str("func", "scheduledPollWorker").
 				Str("machine", machine.Hostname).
-				Msg("Sending update request")
+				Msg("Sending noise update request")
 			updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update").
 				Inc()
 			updateChan <- struct{}{}

From d0898ecabce313c09647dd1594fa2be53a546e1b Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:15:58 +0200
Subject: [PATCH 25/63] Move common parts of the protocol to dedicated file

---
 api.go                   | 281 ---------------------------------------
 protocol_common.go       |  66 +++++++++
 protocol_common_utils.go |  47 +++++++
 3 files changed, 113 insertions(+), 281 deletions(-)
 create mode 100644 protocol_common.go
 create mode 100644 protocol_common_utils.go

diff --git a/api.go b/api.go
index 5ac0d3d969..38e8e64bfa 100644
--- a/api.go
+++ b/api.go
@@ -4,19 +4,15 @@ import (
 	"bytes"
 	"encoding/binary"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"html/template"
-	"io"
 	"net/http"
-	"strconv"
 	"strings"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/klauspost/compress/zstd"
 	"github.com/rs/zerolog/log"
-	"gorm.io/gorm"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
 )
@@ -77,62 +73,6 @@ func (h *Headscale) HealthHandler(
 	respond(nil)
 }
 
-// KeyHandler provides the Headscale pub key
-// Listens in /key.
-func (h *Headscale) KeyHandler(
-	writer http.ResponseWriter,
-	req *http.Request,
-) {
-	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
-	clientCapabilityStr := req.URL.Query().Get("v")
-	if clientCapabilityStr != "" {
-		clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
-		if err != nil {
-			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
-			writer.WriteHeader(http.StatusBadRequest)
-			_, err := writer.Write([]byte("Wrong params"))
-			if err != nil {
-				log.Error().
-					Caller().
-					Err(err).
-					Msg("Failed to write response")
-			}
-
-			return
-		}
-
-		if clientCapabilityVersion >= NoiseCapabilityVersion {
-			// Tailscale has a different key for the TS2021 protocol
-			resp := tailcfg.OverTLSPublicKeyResponse{
-				LegacyPublicKey: h.privateKey.Public(),
-				PublicKey:       h.noisePrivateKey.Public(),
-			}
-			writer.Header().Set("Content-Type", "application/json")
-			writer.WriteHeader(http.StatusOK)
-			err = json.NewEncoder(writer).Encode(resp)
-			if err != nil {
-				log.Error().
-					Caller().
-					Err(err).
-					Msg("Failed to write response")
-			}
-
-			return
-		}
-	}
-
-	// Old clients don't send a 'v' parameter, so we send the legacy public key
-	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
 type registerWebAPITemplateConfig struct {
 	Key string
 }
@@ -211,191 +151,6 @@ func (h *Headscale) RegisterWebAPI(
 	}
 }
 
-// RegistrationHandler handles the actual registration process of a machine
-// Endpoint /machine/:mkey.
-func (h *Headscale) RegistrationHandler(
-	writer http.ResponseWriter,
-	req *http.Request,
-) {
-	vars := mux.Vars(req)
-	machineKeyStr, ok := vars["mkey"]
-	if !ok || machineKeyStr == "" {
-		log.Error().
-			Str("handler", "RegistrationHandler").
-			Msg("No machine ID in request")
-		http.Error(writer, "No machine ID in request", http.StatusBadRequest)
-
-		return
-	}
-
-	body, _ := io.ReadAll(req.Body)
-
-	var machineKey key.MachinePublic
-	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot parse machine key")
-		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
-		http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
-
-		return
-	}
-	registerRequest := tailcfg.RegisterRequest{}
-	err = decode(body, &registerRequest, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot decode message")
-		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
-		http.Error(writer, "Cannot decode message", http.StatusBadRequest)
-
-		return
-	}
-
-	now := time.Now().UTC()
-	machine, err := h.GetMachineByMachineKey(machineKey)
-	if errors.Is(err, gorm.ErrRecordNotFound) {
-		machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
-
-		// If the machine has AuthKey set, handle registration via PreAuthKeys
-		if registerRequest.Auth.AuthKey != "" {
-			h.handleAuthKey(writer, req, machineKey, registerRequest)
-
-			return
-		}
-
-		// Check if the node is waiting for interactive login.
-		//
-		// TODO(juan): We could use this field to improve our protocol implementation,
-		// and hold the request until the client closes it, or the interactive
-		// login is completed (i.e., the user registers the machine).
-		// This is not implemented yet, as it is no strictly required. The only side-effect
-		// is that the client will hammer headscale with requests until it gets a
-		// successful RegisterResponse.
-		if registerRequest.Followup != "" {
-			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
-				log.Debug().
-					Caller().
-					Str("machine", registerRequest.Hostinfo.Hostname).
-					Str("node_key", registerRequest.NodeKey.ShortString()).
-					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-					Str("follow_up", registerRequest.Followup).
-					Msg("Machine is waiting for interactive login")
-
-				ticker := time.NewTicker(registrationHoldoff)
-				select {
-				case <-req.Context().Done():
-					return
-				case <-ticker.C:
-					h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
-
-					return
-				}
-			}
-		}
-
-		log.Info().
-			Caller().
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Str("node_key", registerRequest.NodeKey.ShortString()).
-			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-			Str("follow_up", registerRequest.Followup).
-			Msg("New machine not yet in the database")
-
-		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "RegistrationHandler").
-				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
-				Err(err)
-
-			return
-		}
-
-		// The machine did not have a key to authenticate, which means
-		// that we rely on a method that calls back some how (OpenID or CLI)
-		// We create the machine and then keep it around until a callback
-		// happens
-		newMachine := Machine{
-			MachineKey: machineKeyStr,
-			Hostname:   registerRequest.Hostinfo.Hostname,
-			GivenName:  givenName,
-			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
-			LastSeen:   &now,
-			Expiry:     &time.Time{},
-		}
-
-		if !registerRequest.Expiry.IsZero() {
-			log.Trace().
-				Caller().
-				Str("machine", registerRequest.Hostinfo.Hostname).
-				Time("expiry", registerRequest.Expiry).
-				Msg("Non-zero expiry time requested")
-			newMachine.Expiry = &registerRequest.Expiry
-		}
-
-		h.registrationCache.Set(
-			newMachine.NodeKey,
-			newMachine,
-			registerCacheExpiration,
-		)
-
-		h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
-
-		return
-	}
-
-	// The machine is already registered, so we need to pass through reauth or key update.
-	if machine != nil {
-		// If the NodeKey stored in headscale is the same as the key presented in a registration
-		// request, then we have a node that is either:
-		// - Trying to log out (sending a expiry in the past)
-		// - A valid, registered machine, looking for the node map
-		// - Expired machine wanting to reauthenticate
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
-			// 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 !registerRequest.Expiry.IsZero() &&
-				registerRequest.Expiry.UTC().Before(now) {
-				h.handleMachineLogOut(writer, req, machineKey, *machine)
-
-				return
-			}
-
-			// If machine is not expired, and is register, we have a already accepted this machine,
-			// let it proceed with a valid registration
-			if !machine.isExpired() {
-				h.handleMachineValidRegistration(writer, req, machineKey, *machine)
-
-				return
-			}
-		}
-
-		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
-			!machine.isExpired() {
-			h.handleMachineRefreshKey(
-				writer,
-				req,
-				machineKey,
-				registerRequest,
-				*machine,
-			)
-
-			return
-		}
-
-		// The machine has expired
-		h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
-
-		return
-	}
-}
-
 func (h *Headscale) getLegacyMapResponseData(
 	machineKey key.MachinePublic,
 	mapRequest tailcfg.MapRequest,
@@ -436,42 +191,6 @@ func (h *Headscale) getLegacyMapResponseData(
 	return data, nil
 }
 
-func (h *Headscale) getMapKeepAliveResponse(
-	machineKey key.MachinePublic,
-	mapRequest tailcfg.MapRequest,
-) ([]byte, error) {
-	mapResponse := tailcfg.MapResponse{
-		KeepAlive: true,
-	}
-	var respBody []byte
-	var err error
-	if mapRequest.Compress == ZstdCompression {
-		src, err := json.Marshal(mapResponse)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "getMapKeepAliveResponse").
-				Err(err).
-				Msg("Failed to marshal keepalive response for the client")
-
-			return nil, err
-		}
-		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed := encoder.EncodeAll(src, nil)
-		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
-	} else {
-		respBody, err = encode(mapResponse, &machineKey, h.privateKey)
-		if err != nil {
-			return nil, err
-		}
-	}
-	data := make([]byte, reservedResponseHeaderSize)
-	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
-	data = append(data, respBody...)
-
-	return data, nil
-}
-
 func (h *Headscale) handleMachineLogOut(
 	writer http.ResponseWriter,
 	req *http.Request,
diff --git a/protocol_common.go b/protocol_common.go
new file mode 100644
index 0000000000..c8eab80e5c
--- /dev/null
+++ b/protocol_common.go
@@ -0,0 +1,66 @@
+package headscale
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/rs/zerolog/log"
+	"tailscale.com/tailcfg"
+)
+
+// KeyHandler provides the Headscale pub key
+// Listens in /key.
+func (h *Headscale) KeyHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
+	clientCapabilityStr := req.URL.Query().Get("v")
+	if clientCapabilityStr != "" {
+		clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
+		if err != nil {
+			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
+			writer.WriteHeader(http.StatusBadRequest)
+			_, err := writer.Write([]byte("Wrong params"))
+			if err != nil {
+				log.Error().
+					Caller().
+					Err(err).
+					Msg("Failed to write response")
+			}
+
+			return
+		}
+
+		if clientCapabilityVersion >= NoiseCapabilityVersion {
+			// Tailscale has a different key for the TS2021 protocol
+			resp := tailcfg.OverTLSPublicKeyResponse{
+				LegacyPublicKey: h.privateKey.Public(),
+				PublicKey:       h.noisePrivateKey.Public(),
+			}
+			writer.Header().Set("Content-Type", "application/json")
+			writer.WriteHeader(http.StatusOK)
+			err = json.NewEncoder(writer).Encode(resp)
+			if err != nil {
+				log.Error().
+					Caller().
+					Err(err).
+					Msg("Failed to write response")
+			}
+
+			return
+		}
+	}
+
+	// Old clients don't send a 'v' parameter, so we send the legacy public key
+	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err := writer.Write([]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())))
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to write response")
+	}
+}
diff --git a/protocol_common_utils.go b/protocol_common_utils.go
new file mode 100644
index 0000000000..5939b6a446
--- /dev/null
+++ b/protocol_common_utils.go
@@ -0,0 +1,47 @@
+package headscale
+
+import (
+	"encoding/binary"
+	"encoding/json"
+
+	"github.com/klauspost/compress/zstd"
+	"github.com/rs/zerolog/log"
+	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
+)
+
+func (h *Headscale) getMapKeepAliveResponse(
+	machineKey key.MachinePublic,
+	mapRequest tailcfg.MapRequest,
+) ([]byte, error) {
+	mapResponse := tailcfg.MapResponse{
+		KeepAlive: true,
+	}
+	var respBody []byte
+	var err error
+	if mapRequest.Compress == ZstdCompression {
+		src, err := json.Marshal(mapResponse)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "getMapKeepAliveResponse").
+				Err(err).
+				Msg("Failed to marshal keepalive response for the client")
+
+			return nil, err
+		}
+		encoder, _ := zstd.NewWriter(nil)
+		srcCompressed := encoder.EncodeAll(src, nil)
+		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
+	} else {
+		respBody, err = encode(mapResponse, &machineKey, h.privateKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+	data := make([]byte, reservedResponseHeaderSize)
+	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
+	data = append(data, respBody...)
+
+	return data, nil
+}

From db89fdea23b5e82a43d0f0d38d0de47e75f35543 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:16:29 +0200
Subject: [PATCH 26/63] Added file for legacy protocol

---
 protocol_legacy.go | 199 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 199 insertions(+)
 create mode 100644 protocol_legacy.go

diff --git a/protocol_legacy.go b/protocol_legacy.go
new file mode 100644
index 0000000000..5c23b172a5
--- /dev/null
+++ b/protocol_legacy.go
@@ -0,0 +1,199 @@
+package headscale
+
+import (
+	"errors"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
+	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
+)
+
+// RegistrationHandler handles the actual registration process of a machine
+// Endpoint /machine/:mkey.
+func (h *Headscale) RegistrationHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	vars := mux.Vars(req)
+	machineKeyStr, ok := vars["mkey"]
+	if !ok || machineKeyStr == "" {
+		log.Error().
+			Str("handler", "RegistrationHandler").
+			Msg("No machine ID in request")
+		http.Error(writer, "No machine ID in request", http.StatusBadRequest)
+
+		return
+	}
+
+	body, _ := io.ReadAll(req.Body)
+
+	var machineKey key.MachinePublic
+	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse machine key")
+		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
+		http.Error(writer, "Cannot parse machine key", http.StatusBadRequest)
+
+		return
+	}
+	registerRequest := tailcfg.RegisterRequest{}
+	err = decode(body, &registerRequest, &machineKey, h.privateKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot decode message")
+		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
+		http.Error(writer, "Cannot decode message", http.StatusBadRequest)
+
+		return
+	}
+
+	now := time.Now().UTC()
+	machine, err := h.GetMachineByMachineKey(machineKey)
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
+
+		// If the machine has AuthKey set, handle registration via PreAuthKeys
+		if registerRequest.Auth.AuthKey != "" {
+			h.handleAuthKey(writer, req, machineKey, registerRequest)
+
+			return
+		}
+
+		// Check if the node is waiting for interactive login.
+		//
+		// TODO(juan): We could use this field to improve our protocol implementation,
+		// and hold the request until the client closes it, or the interactive
+		// login is completed (i.e., the user registers the machine).
+		// This is not implemented yet, as it is no strictly required. The only side-effect
+		// is that the client will hammer headscale with requests until it gets a
+		// successful RegisterResponse.
+		if registerRequest.Followup != "" {
+			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
+				log.Debug().
+					Caller().
+					Str("machine", registerRequest.Hostinfo.Hostname).
+					Str("node_key", registerRequest.NodeKey.ShortString()).
+					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+					Str("follow_up", registerRequest.Followup).
+					Msg("Machine is waiting for interactive login")
+
+				ticker := time.NewTicker(registrationHoldoff)
+				select {
+				case <-req.Context().Done():
+					return
+				case <-ticker.C:
+					h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
+
+					return
+				}
+			}
+		}
+
+		log.Info().
+			Caller().
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Str("node_key", registerRequest.NodeKey.ShortString()).
+			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+			Str("follow_up", registerRequest.Followup).
+			Msg("New machine not yet in the database")
+
+		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "RegistrationHandler").
+				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
+				Err(err)
+
+			return
+		}
+
+		// The machine did not have a key to authenticate, which means
+		// that we rely on a method that calls back some how (OpenID or CLI)
+		// We create the machine and then keep it around until a callback
+		// happens
+		newMachine := Machine{
+			MachineKey: machineKeyStr,
+			Hostname:   registerRequest.Hostinfo.Hostname,
+			GivenName:  givenName,
+			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
+			LastSeen:   &now,
+			Expiry:     &time.Time{},
+		}
+
+		if !registerRequest.Expiry.IsZero() {
+			log.Trace().
+				Caller().
+				Str("machine", registerRequest.Hostinfo.Hostname).
+				Time("expiry", registerRequest.Expiry).
+				Msg("Non-zero expiry time requested")
+			newMachine.Expiry = &registerRequest.Expiry
+		}
+
+		h.registrationCache.Set(
+			newMachine.NodeKey,
+			newMachine,
+			registerCacheExpiration,
+		)
+
+		h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
+
+		return
+	}
+
+	// The machine is already registered, so we need to pass through reauth or key update.
+	if machine != nil {
+		// If the NodeKey stored in headscale is the same as the key presented in a registration
+		// request, then we have a node that is either:
+		// - Trying to log out (sending a expiry in the past)
+		// - A valid, registered machine, looking for the node map
+		// - Expired machine wanting to reauthenticate
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
+			// 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 !registerRequest.Expiry.IsZero() &&
+				registerRequest.Expiry.UTC().Before(now) {
+				h.handleMachineLogOut(writer, req, machineKey, *machine)
+
+				return
+			}
+
+			// If machine is not expired, and is register, we have a already accepted this machine,
+			// let it proceed with a valid registration
+			if !machine.isExpired() {
+				h.handleMachineValidRegistration(writer, req, machineKey, *machine)
+
+				return
+			}
+		}
+
+		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
+			!machine.isExpired() {
+			h.handleMachineRefreshKey(
+				writer,
+				req,
+				machineKey,
+				registerRequest,
+				*machine,
+			)
+
+			return
+		}
+
+		// The machine has expired
+		h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
+
+		return
+	}
+}

From 35f3dee1d0015b9280e689f269144510ec925b69 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 21:19:52 +0200
Subject: [PATCH 27/63] Move Noise API to new file

---
 noise_api.go => protocol_noise.go | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename noise_api.go => protocol_noise.go (100%)

diff --git a/noise_api.go b/protocol_noise.go
similarity index 100%
rename from noise_api.go
rename to protocol_noise.go

From f4bab6b2901b7486813b8c59e8217dbc90cf8002 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 22:50:39 +0200
Subject: [PATCH 28/63] Created common methods for keep and map poll responses

---
 protocol_common_utils.go | 92 +++++++++++++++++++++++++++++++---------
 1 file changed, 72 insertions(+), 20 deletions(-)

diff --git a/protocol_common_utils.go b/protocol_common_utils.go
index 5939b6a446..3eb547154d 100644
--- a/protocol_common_utils.go
+++ b/protocol_common_utils.go
@@ -10,35 +10,87 @@ import (
 	"tailscale.com/types/key"
 )
 
-func (h *Headscale) getMapKeepAliveResponse(
-	machineKey key.MachinePublic,
+func (h *Headscale) getMapResponseData(
+	mapRequest tailcfg.MapRequest,
+	machine *Machine,
+	isNoise bool,
+) ([]byte, error) {
+	mapResponse, err := h.generateMapResponse(mapRequest, machine)
+	if err != nil {
+		return nil, err
+	}
+
+	if isNoise {
+		return h.marshalResponse(mapResponse, mapRequest.Compress, key.MachinePublic{})
+	}
+
+	var machineKey key.MachinePublic
+	err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)))
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse client key")
+
+		return nil, err
+	}
+
+	return h.marshalResponse(mapResponse, mapRequest.Compress, machineKey)
+}
+
+func (h *Headscale) getMapKeepAliveResponseData(
 	mapRequest tailcfg.MapRequest,
+	machine *Machine,
+	isNoise bool,
 ) ([]byte, error) {
-	mapResponse := tailcfg.MapResponse{
+	keepAliveResponse := tailcfg.MapResponse{
 		KeepAlive: true,
 	}
+
+	if isNoise {
+		return h.marshalResponse(keepAliveResponse, mapRequest.Compress, key.MachinePublic{})
+	}
+
+	var machineKey key.MachinePublic
+	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)))
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse client key")
+
+		return nil, err
+	}
+
+	return h.marshalResponse(keepAliveResponse, mapRequest.Compress, machineKey)
+}
+
+func (h *Headscale) marshalResponse(
+	resp interface{},
+	compression string,
+	machineKey key.MachinePublic,
+) ([]byte, error) {
+	jsonBody, err := json.Marshal(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot marshal map response")
+	}
+
 	var respBody []byte
-	var err error
-	if mapRequest.Compress == ZstdCompression {
-		src, err := json.Marshal(mapResponse)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "getMapKeepAliveResponse").
-				Err(err).
-				Msg("Failed to marshal keepalive response for the client")
-
-			return nil, err
-		}
+	if compression == ZstdCompression {
 		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed := encoder.EncodeAll(src, nil)
-		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
+		respBody = encoder.EncodeAll(jsonBody, nil)
+		if !machineKey.IsZero() { // if legacy protocol
+			respBody = h.privateKey.SealTo(machineKey, respBody)
+		}
 	} else {
-		respBody, err = encode(mapResponse, &machineKey, h.privateKey)
-		if err != nil {
-			return nil, err
+		if !machineKey.IsZero() { // if legacy protocol
+			respBody = h.privateKey.SealTo(machineKey, jsonBody)
 		}
 	}
+
 	data := make([]byte, reservedResponseHeaderSize)
 	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
 	data = append(data, respBody...)

From df8ecdb6038ace48708d9030cea8f5da2aba41fa Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 22:57:03 +0200
Subject: [PATCH 29/63] Working on common codebase for poll, starting with
 legacy

---
 poll.go => protocol_common_poll.go | 128 ++++++-----------------------
 protocol_legacy_poll.go            |  94 +++++++++++++++++++++
 2 files changed, 121 insertions(+), 101 deletions(-)
 rename poll.go => protocol_common_poll.go (84%)
 create mode 100644 protocol_legacy_poll.go

diff --git a/poll.go b/protocol_common_poll.go
similarity index 84%
rename from poll.go
rename to protocol_common_poll.go
index a51c93629c..988d225aa1 100644
--- a/poll.go
+++ b/protocol_common_poll.go
@@ -2,17 +2,12 @@ package headscale
 
 import (
 	"context"
-	"errors"
 	"fmt"
-	"io"
 	"net/http"
 	"time"
 
-	"github.com/gorilla/mux"
 	"github.com/rs/zerolog/log"
-	"gorm.io/gorm"
 	"tailscale.com/tailcfg"
-	"tailscale.com/types/key"
 )
 
 const (
@@ -23,83 +18,13 @@ type contextKey string
 
 const machineNameContextKey = contextKey("machineName")
 
-// PollNetMapHandler takes care of /machine/:id/map
-//
-// This is the busiest endpoint, as it keeps the HTTP long poll that updates
-// the clients when something in the network changes.
-//
-// The clients POST stuff like HostInfo and their Endpoints here, but
-// 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(
+func (h *Headscale) handlePollCommon(
 	writer http.ResponseWriter,
 	req *http.Request,
+	machine *Machine,
+	mapRequest tailcfg.MapRequest,
+	isNoise bool,
 ) {
-	vars := mux.Vars(req)
-	machineKeyStr, ok := vars["mkey"]
-	if !ok || machineKeyStr == "" {
-		log.Error().
-			Str("handler", "PollNetMap").
-			Msg("No machine key in request")
-		http.Error(writer, "No machine key in request", http.StatusBadRequest)
-
-		return
-	}
-	log.Trace().
-		Str("handler", "PollNetMap").
-		Str("id", machineKeyStr).
-		Msg("PollNetMapHandler called")
-	body, _ := io.ReadAll(req.Body)
-
-	var machineKey key.MachinePublic
-	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
-	if err != nil {
-		log.Error().
-			Str("handler", "PollNetMap").
-			Err(err).
-			Msg("Cannot parse client key")
-
-		http.Error(writer, "Cannot parse client key", http.StatusBadRequest)
-
-		return
-	}
-	mapRequest := tailcfg.MapRequest{}
-	err = decode(body, &mapRequest, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Str("handler", "PollNetMap").
-			Err(err).
-			Msg("Cannot decode message")
-		http.Error(writer, "Cannot decode message", http.StatusBadRequest)
-
-		return
-	}
-
-	machine, err := h.GetMachineByMachineKey(machineKey)
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			log.Warn().
-				Str("handler", "PollNetMap").
-				Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
-
-			http.Error(writer, "", http.StatusUnauthorized)
-
-			return
-		}
-		log.Error().
-			Str("handler", "PollNetMap").
-			Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
-		http.Error(writer, "", http.StatusInternalServerError)
-
-		return
-	}
-	log.Trace().
-		Str("handler", "PollNetMap").
-		Str("id", machineKeyStr).
-		Str("machine", machine.Hostname).
-		Msg("Found machine in database")
-
 	machine.Hostname = mapRequest.Hostinfo.Hostname
 	machine.HostInfo = HostInfo(*mapRequest.Hostinfo)
 	machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
@@ -107,7 +32,7 @@ func (h *Headscale) PollNetMapHandler(
 
 	// update ACLRules with peer informations (to update server tags if necessary)
 	if h.aclPolicy != nil {
-		err = h.UpdateACLRules()
+		err := h.UpdateACLRules()
 		if err != nil {
 			log.Error().
 				Caller().
@@ -133,7 +58,7 @@ func (h *Headscale) PollNetMapHandler(
 		if err != nil {
 			log.Error().
 				Str("handler", "PollNetMap").
-				Str("id", machineKeyStr).
+				Str("node_key", machine.NodeKey).
 				Str("machine", machine.Hostname).
 				Err(err).
 				Msg("Failed to persist/update machine in the database")
@@ -143,11 +68,11 @@ func (h *Headscale) PollNetMapHandler(
 		}
 	}
 
-	data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine)
+	mapResp, err := h.getMapResponseData(mapRequest, machine, isNoise)
 	if err != nil {
 		log.Error().
 			Str("handler", "PollNetMap").
-			Str("id", machineKeyStr).
+			Str("node_key", machine.NodeKey).
 			Str("machine", machine.Hostname).
 			Err(err).
 			Msg("Failed to get Map response")
@@ -163,7 +88,6 @@ func (h *Headscale) PollNetMapHandler(
 	// 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", machineKeyStr).
 		Str("machine", machine.Hostname).
 		Bool("readOnly", mapRequest.ReadOnly).
 		Bool("omitPeers", mapRequest.OmitPeers).
@@ -178,7 +102,7 @@ func (h *Headscale) PollNetMapHandler(
 
 		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
 		writer.WriteHeader(http.StatusOK)
-		_, err := writer.Write(data)
+		_, err := writer.Write(mapResp)
 		if err != nil {
 			log.Error().
 				Caller().
@@ -186,6 +110,10 @@ func (h *Headscale) PollNetMapHandler(
 				Msg("Failed to write response")
 		}
 
+		if f, ok := writer.(http.Flusher); ok {
+			f.Flush()
+		}
+
 		return
 	}
 
@@ -198,8 +126,7 @@ func (h *Headscale) PollNetMapHandler(
 
 	// Only create update channel if it has not been created
 	log.Trace().
-		Str("handler", "PollNetMap").
-		Str("id", machineKeyStr).
+		Caller().
 		Str("machine", machine.Hostname).
 		Msg("Loading or creating update channel")
 
@@ -218,7 +145,7 @@ func (h *Headscale) PollNetMapHandler(
 			Msg("Client sent endpoint update and is ok with a response without peer list")
 		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
 		writer.WriteHeader(http.StatusOK)
-		_, err := writer.Write(data)
+		_, err := writer.Write(mapResp)
 		if err != nil {
 			log.Error().
 				Caller().
@@ -250,7 +177,7 @@ func (h *Headscale) PollNetMapHandler(
 		Str("handler", "PollNetMap").
 		Str("machine", machine.Hostname).
 		Msg("Sending initial map")
-	pollDataChan <- data
+	pollDataChan <- mapResp
 
 	log.Info().
 		Str("handler", "PollNetMap").
@@ -260,35 +187,34 @@ func (h *Headscale) PollNetMapHandler(
 		Inc()
 	updateChan <- struct{}{}
 
-	h.PollNetMapStream(
+	h.pollNetMapStream(
 		writer,
 		req,
 		machine,
 		mapRequest,
-		machineKey,
 		pollDataChan,
 		keepAliveChan,
 		updateChan,
+		isNoise,
 	)
+
 	log.Trace().
 		Str("handler", "PollNetMap").
-		Str("id", machineKeyStr).
 		Str("machine", machine.Hostname).
 		Msg("Finished stream, closing PollNetMap session")
 }
 
-// PollNetMapStream takes care of /machine/:id/map
-// stream logic, ensuring we communicate updates and data
-// to the connected clients.
-func (h *Headscale) PollNetMapStream(
+// pollNetMapStream stream logic for /machine/map,
+// ensuring we communicate updates and data to the connected clients.
+func (h *Headscale) pollNetMapStream(
 	writer http.ResponseWriter,
 	req *http.Request,
 	machine *Machine,
 	mapRequest tailcfg.MapRequest,
-	machineKey key.MachinePublic,
 	pollDataChan chan []byte,
 	keepAliveChan chan []byte,
 	updateChan chan struct{},
+	isNoise bool,
 ) {
 	h.pollNetMapStreamWG.Add(1)
 	defer h.pollNetMapStreamWG.Done()
@@ -302,9 +228,9 @@ func (h *Headscale) PollNetMapStream(
 		ctx,
 		updateChan,
 		keepAliveChan,
-		machineKey,
 		mapRequest,
 		machine,
+		isNoise,
 	)
 
 	log.Trace().
@@ -491,7 +417,7 @@ func (h *Headscale) PollNetMapStream(
 					Time("last_successful_update", lastUpdate).
 					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
 					Msgf("There has been updates since the last successful update to %s", machine.Hostname)
-				data, err := h.getLegacyMapResponseData(machineKey, mapRequest, machine)
+				data, err := h.getMapResponseData(mapRequest, machine, false)
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").
@@ -637,9 +563,9 @@ func (h *Headscale) scheduledPollWorker(
 	ctx context.Context,
 	updateChan chan struct{},
 	keepAliveChan chan []byte,
-	machineKey key.MachinePublic,
 	mapRequest tailcfg.MapRequest,
 	machine *Machine,
+	isNoise bool,
 ) {
 	keepAliveTicker := time.NewTicker(keepAliveInterval)
 	updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
@@ -661,7 +587,7 @@ func (h *Headscale) scheduledPollWorker(
 			return
 
 		case <-keepAliveTicker.C:
-			data, err := h.getMapKeepAliveResponse(machineKey, mapRequest)
+			data, err := h.getMapKeepAliveResponseData(mapRequest, machine, isNoise)
 			if err != nil {
 				log.Error().
 					Str("func", "keepAlive").
diff --git a/protocol_legacy_poll.go b/protocol_legacy_poll.go
new file mode 100644
index 0000000000..a42f399c83
--- /dev/null
+++ b/protocol_legacy_poll.go
@@ -0,0 +1,94 @@
+package headscale
+
+import (
+	"errors"
+	"io"
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
+	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
+)
+
+// PollNetMapHandler takes care of /machine/:id/map
+//
+// This is the busiest endpoint, as it keeps the HTTP long poll that updates
+// the clients when something in the network changes.
+//
+// The clients POST stuff like HostInfo and their Endpoints here, but
+// 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(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	vars := mux.Vars(req)
+	machineKeyStr, ok := vars["mkey"]
+	if !ok || machineKeyStr == "" {
+		log.Error().
+			Str("handler", "PollNetMap").
+			Msg("No machine key in request")
+		http.Error(writer, "No machine key in request", http.StatusBadRequest)
+
+		return
+	}
+	log.Trace().
+		Str("handler", "PollNetMap").
+		Str("id", machineKeyStr).
+		Msg("PollNetMapHandler called")
+	body, _ := io.ReadAll(req.Body)
+
+	var machineKey key.MachinePublic
+	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
+	if err != nil {
+		log.Error().
+			Str("handler", "PollNetMap").
+			Err(err).
+			Msg("Cannot parse client key")
+
+		http.Error(writer, "Cannot parse client key", http.StatusBadRequest)
+
+		return
+	}
+	mapRequest := tailcfg.MapRequest{}
+	err = decode(body, &mapRequest, &machineKey, h.privateKey)
+	if err != nil {
+		log.Error().
+			Str("handler", "PollNetMap").
+			Err(err).
+			Msg("Cannot decode message")
+		http.Error(writer, "Cannot decode message", http.StatusBadRequest)
+
+		return
+	}
+
+	machine, err := h.GetMachineByMachineKey(machineKey)
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			log.Warn().
+				Str("handler", "PollNetMap").
+				Msgf("Ignoring request, cannot find machine with key %s", machineKey.String())
+
+			http.Error(writer, "", http.StatusUnauthorized)
+
+			return
+		}
+		log.Error().
+			Str("handler", "PollNetMap").
+			Msgf("Failed to fetch machine from the database with Machine key: %s", machineKey.String())
+		http.Error(writer, "", http.StatusInternalServerError)
+
+		return
+	}
+
+	log.Trace().
+		Str("handler", "PollNetMap").
+		Str("id", machineKeyStr).
+		Str("machine", machine.Hostname).
+		Msg("Found machine in database")
+
+	h.handlePollCommon(writer, req, machine, mapRequest, false)
+}

From 7cc227d01e15e24283ac56a555992752b49e7a16 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 23:11:33 +0200
Subject: [PATCH 30/63] Added Noise field to logging

---
 protocol_common_poll.go | 42 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/protocol_common_poll.go b/protocol_common_poll.go
index 988d225aa1..11178cd328 100644
--- a/protocol_common_poll.go
+++ b/protocol_common_poll.go
@@ -36,6 +36,7 @@ func (h *Headscale) handlePollCommon(
 		if err != nil {
 			log.Error().
 				Caller().
+				Bool("noise", isNoise).
 				Str("func", "handleAuthKey").
 				Str("machine", machine.Hostname).
 				Err(err)
@@ -58,6 +59,7 @@ func (h *Headscale) handlePollCommon(
 		if err != nil {
 			log.Error().
 				Str("handler", "PollNetMap").
+				Bool("noise", isNoise).
 				Str("node_key", machine.NodeKey).
 				Str("machine", machine.Hostname).
 				Err(err).
@@ -72,6 +74,7 @@ func (h *Headscale) handlePollCommon(
 	if err != nil {
 		log.Error().
 			Str("handler", "PollNetMap").
+			Bool("noise", isNoise).
 			Str("node_key", machine.NodeKey).
 			Str("machine", machine.Hostname).
 			Err(err).
@@ -88,6 +91,7 @@ func (h *Headscale) handlePollCommon(
 	// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
 	log.Debug().
 		Str("handler", "PollNetMap").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Bool("readOnly", mapRequest.ReadOnly).
 		Bool("omitPeers", mapRequest.OmitPeers).
@@ -97,6 +101,7 @@ func (h *Headscale) handlePollCommon(
 	if mapRequest.ReadOnly {
 		log.Info().
 			Str("handler", "PollNetMap").
+			Bool("noise", isNoise).
 			Str("machine", machine.Hostname).
 			Msg("Client is starting up. Probably interested in a DERP map")
 
@@ -127,6 +132,7 @@ func (h *Headscale) handlePollCommon(
 	// Only create update channel if it has not been created
 	log.Trace().
 		Caller().
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Loading or creating update channel")
 
@@ -141,6 +147,7 @@ func (h *Headscale) handlePollCommon(
 	if mapRequest.OmitPeers && !mapRequest.Stream {
 		log.Info().
 			Str("handler", "PollNetMap").
+			Bool("noise", isNoise).
 			Str("machine", machine.Hostname).
 			Msg("Client sent endpoint update and is ok with a response without peer list")
 		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -162,6 +169,7 @@ func (h *Headscale) handlePollCommon(
 	} else if mapRequest.OmitPeers && mapRequest.Stream {
 		log.Warn().
 			Str("handler", "PollNetMap").
+			Bool("noise", isNoise).
 			Str("machine", machine.Hostname).
 			Msg("Ignoring request, don't know how to handle it")
 		http.Error(writer, "", http.StatusBadRequest)
@@ -171,16 +179,19 @@ func (h *Headscale) handlePollCommon(
 
 	log.Info().
 		Str("handler", "PollNetMap").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Client is ready to access the tailnet")
 	log.Info().
 		Str("handler", "PollNetMap").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Sending initial map")
 	pollDataChan <- mapResp
 
 	log.Info().
 		Str("handler", "PollNetMap").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Notifying peers")
 	updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update").
@@ -200,6 +211,7 @@ func (h *Headscale) handlePollCommon(
 
 	log.Trace().
 		Str("handler", "PollNetMap").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Finished stream, closing PollNetMap session")
 }
@@ -235,11 +247,13 @@ func (h *Headscale) pollNetMapStream(
 
 	log.Trace().
 		Str("handler", "PollNetMapStream").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Waiting for data to stream...")
 
 	log.Trace().
 		Str("handler", "PollNetMapStream").
+		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
 
@@ -248,6 +262,7 @@ func (h *Headscale) pollNetMapStream(
 		case data := <-pollDataChan:
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "pollData").
 				Int("bytes", len(data)).
@@ -256,6 +271,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "pollData").
 					Err(err).
@@ -269,6 +285,7 @@ func (h *Headscale) pollNetMapStream(
 				log.Error().
 					Caller().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "pollData").
 					Msg("Cannot cast writer to http.Flusher")
@@ -278,6 +295,7 @@ func (h *Headscale) pollNetMapStream(
 
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "pollData").
 				Int("bytes", len(data)).
@@ -289,6 +307,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "pollData").
 					Err(err).
@@ -309,6 +328,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "pollData").
 					Err(err).
@@ -319,6 +339,7 @@ func (h *Headscale) pollNetMapStream(
 
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "pollData").
 				Int("bytes", len(data)).
@@ -335,6 +356,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "keepAlive").
 					Err(err).
@@ -347,6 +369,7 @@ func (h *Headscale) pollNetMapStream(
 				log.Error().
 					Caller().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "keepAlive").
 					Msg("Cannot cast writer to http.Flusher")
@@ -356,6 +379,7 @@ func (h *Headscale) pollNetMapStream(
 
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "keepAlive").
 				Int("bytes", len(data)).
@@ -367,6 +391,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "keepAlive").
 					Err(err).
@@ -382,6 +407,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "keepAlive").
 					Err(err).
@@ -392,6 +418,7 @@ func (h *Headscale) pollNetMapStream(
 
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "keepAlive").
 				Int("bytes", len(data)).
@@ -400,6 +427,7 @@ func (h *Headscale) pollNetMapStream(
 		case <-updateChan:
 			log.Trace().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Str("channel", "update").
 				Msg("Received a request for update")
@@ -413,6 +441,7 @@ func (h *Headscale) pollNetMapStream(
 				}
 				log.Debug().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Time("last_successful_update", lastUpdate).
 					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
@@ -421,6 +450,7 @@ func (h *Headscale) pollNetMapStream(
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").
+						Bool("noise", isNoise).
 						Str("machine", machine.Hostname).
 						Str("channel", "update").
 						Err(err).
@@ -432,6 +462,7 @@ func (h *Headscale) pollNetMapStream(
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").
+						Bool("noise", isNoise).
 						Str("machine", machine.Hostname).
 						Str("channel", "update").
 						Err(err).
@@ -447,6 +478,7 @@ func (h *Headscale) pollNetMapStream(
 					log.Error().
 						Caller().
 						Str("handler", "PollNetMapStream").
+						Bool("noise", isNoise).
 						Str("machine", machine.Hostname).
 						Str("channel", "update").
 						Msg("Cannot cast writer to http.Flusher")
@@ -456,6 +488,7 @@ func (h *Headscale) pollNetMapStream(
 
 				log.Trace().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "update").
 					Msg("Updated Map has been sent")
@@ -473,6 +506,7 @@ func (h *Headscale) pollNetMapStream(
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").
+						Bool("noise", isNoise).
 						Str("machine", machine.Hostname).
 						Str("channel", "update").
 						Err(err).
@@ -492,6 +526,7 @@ func (h *Headscale) pollNetMapStream(
 				if err != nil {
 					log.Error().
 						Str("handler", "PollNetMapStream").
+						Bool("noise", isNoise).
 						Str("machine", machine.Hostname).
 						Str("channel", "update").
 						Err(err).
@@ -506,6 +541,7 @@ func (h *Headscale) pollNetMapStream(
 				}
 				log.Trace().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Time("last_successful_update", lastUpdate).
 					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
@@ -524,6 +560,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "Done").
 					Err(err).
@@ -539,6 +576,7 @@ func (h *Headscale) pollNetMapStream(
 			if err != nil {
 				log.Error().
 					Str("handler", "PollNetMapStream").
+					Bool("noise", isNoise).
 					Str("machine", machine.Hostname).
 					Str("channel", "Done").
 					Err(err).
@@ -551,6 +589,7 @@ func (h *Headscale) pollNetMapStream(
 		case <-h.shutdownChan:
 			log.Info().
 				Str("handler", "PollNetMapStream").
+				Bool("noise", isNoise).
 				Str("machine", machine.Hostname).
 				Msg("The long-poll handler is shutting down")
 
@@ -591,6 +630,7 @@ func (h *Headscale) scheduledPollWorker(
 			if err != nil {
 				log.Error().
 					Str("func", "keepAlive").
+					Bool("noise", isNoise).
 					Err(err).
 					Msg("Error generating the keep alive msg")
 
@@ -600,6 +640,7 @@ func (h *Headscale) scheduledPollWorker(
 			log.Debug().
 				Str("func", "keepAlive").
 				Str("machine", machine.Hostname).
+				Bool("noise", isNoise).
 				Msg("Sending keepalive")
 			keepAliveChan <- data
 
@@ -607,6 +648,7 @@ func (h *Headscale) scheduledPollWorker(
 			log.Debug().
 				Str("func", "scheduledPollWorker").
 				Str("machine", machine.Hostname).
+				Bool("noise", isNoise).
 				Msg("Sending update request")
 			updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update").
 				Inc()

From e29b344e0f85cc64d6b5e3fdfc1639fefa92fa98 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 23:12:18 +0200
Subject: [PATCH 31/63] Move Noise poll to new file, and use common poll

---
 noise_poll.go          | 714 -----------------------------------------
 protocol_noise_poll.go |  67 ++++
 2 files changed, 67 insertions(+), 714 deletions(-)
 delete mode 100644 noise_poll.go
 create mode 100644 protocol_noise_poll.go

diff --git a/noise_poll.go b/noise_poll.go
deleted file mode 100644
index 1d9dfe373f..0000000000
--- a/noise_poll.go
+++ /dev/null
@@ -1,714 +0,0 @@
-package headscale
-
-import (
-	"context"
-	"encoding/binary"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"net/http"
-	"time"
-
-	"github.com/klauspost/compress/zstd"
-	"github.com/rs/zerolog/log"
-	"gorm.io/gorm"
-	"tailscale.com/tailcfg"
-	"tailscale.com/types/key"
-)
-
-// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
-//
-// This is the busiest endpoint, as it keeps the HTTP long poll that updates
-// the clients when something in the network changes.
-//
-// The clients POST stuff like HostInfo and their Endpoints here, but
-// 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) NoisePollNetMapHandler(
-	writer http.ResponseWriter,
-	req *http.Request,
-) {
-	log.Trace().
-		Str("handler", "NoisePollNetMap").
-		Msg("PollNetMapHandler called")
-	body, _ := io.ReadAll(req.Body)
-
-	mapRequest := tailcfg.MapRequest{}
-	if err := json.Unmarshal(body, &mapRequest); err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot parse MapRequest")
-		http.Error(writer, "Internal error", http.StatusInternalServerError)
-
-		return
-	}
-
-	machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{})
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			log.Warn().
-				Str("handler", "NoisePollNetMap").
-				Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String())
-			http.Error(writer, "Internal error", http.StatusNotFound)
-
-			return
-		}
-		log.Error().
-			Str("handler", "NoisePollNetMap").
-			Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String())
-		http.Error(writer, "Internal error", http.StatusInternalServerError)
-
-		return
-	}
-	log.Trace().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Found machine in database")
-
-	machine.Hostname = mapRequest.Hostinfo.Hostname
-	machine.HostInfo = HostInfo(*mapRequest.Hostinfo)
-	machine.DiscoKey = DiscoPublicKeyStripPrefix(mapRequest.DiscoKey)
-	now := time.Now().UTC()
-
-	// update ACLRules with peer informations (to update server tags if necessary)
-	if h.aclPolicy != nil {
-		err = h.UpdateACLRules()
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "handleAuthKey").
-				Str("machine", machine.Hostname).
-				Err(err)
-		}
-	}
-
-	// From Tailscale client:
-	//
-	// ReadOnly is whether the client just wants to fetch the MapResponse,
-	// without updating their Endpoints. The Endpoints field will be ignored and
-	// LastSeen will not be updated and peers will not be notified of changes.
-	//
-	// The intended use is for clients to discover the DERP map at start-up
-	// before their first real endpoint update.
-	if !mapRequest.ReadOnly {
-		machine.Endpoints = mapRequest.Endpoints
-		machine.LastSeen = &now
-	}
-
-	if err := h.db.Updates(machine).Error; err != nil {
-		if err != nil {
-			log.Error().
-				Str("handler", "NoisePollNetMap").
-				Str("machine", machine.Hostname).
-				Err(err).
-				Msg("Failed to persist/update machine in the database")
-			http.Error(writer, "Internal error", http.StatusInternalServerError)
-
-			return
-		}
-	}
-
-	resp, err := h.getNoiseMapResponse(mapRequest, machine)
-	if err != nil {
-		log.Error().
-			Str("handler", "NoisePollNetMap").
-			Str("machine", machine.Hostname).
-			Err(err).
-			Msg("Failed to get Map response")
-		http.Error(writer, "Internal error", http.StatusInternalServerError)
-
-		return
-	}
-
-	// We update our peers if the client is not sending ReadOnly in the MapRequest
-	// so we don't distribute its initial request (it comes with
-	// empty endpoints to peers)
-
-	// Details on the protocol can be found in https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L696
-	log.Debug().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Bool("readOnly", mapRequest.ReadOnly).
-		Bool("omitPeers", mapRequest.OmitPeers).
-		Bool("stream", mapRequest.Stream).
-		Msg("Noise client map request processed")
-
-	if mapRequest.ReadOnly {
-		log.Info().
-			Str("handler", "NoisePollNetMap").
-			Str("machine", machine.Hostname).
-			Msg("Client is starting up. Probably interested in a DERP map")
-		// w.Header().Set("Content-Type", "application/json")
-		// w.WriteHeader(http.StatusOK)
-		_, err = writer.Write(resp)
-		if err != nil {
-			log.Warn().Msgf("Could not send JSON response: %s", err)
-		}
-		if f, ok := writer.(http.Flusher); ok {
-			f.Flush()
-		}
-
-		log.Info().Msgf("Noise client map response sent for %s (len %d)", machine.Hostname, len(resp))
-
-		return
-	}
-
-	// There has been an update to _any_ of the nodes that the other nodes would
-	// need to know about
-	h.setLastStateChangeToNow(machine.Namespace.Name)
-
-	// The request is not ReadOnly, so we need to set up channels for updating
-	// peers via longpoll
-
-	// Only create update channel if it has not been created
-	log.Trace().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Loading or creating update channel")
-
-	const chanSize = 8
-	updateChan := make(chan struct{}, chanSize)
-
-	pollDataChan := make(chan []byte, chanSize)
-	defer closeChanWithLog(pollDataChan, machine.Hostname, "pollDataChan")
-
-	keepAliveChan := make(chan []byte)
-
-	if mapRequest.OmitPeers && !mapRequest.Stream {
-		log.Info().
-			Str("handler", "NoisePollNetMap").
-			Str("machine", machine.Hostname).
-			Msg("Client sent endpoint update and is ok with a response without peer list")
-
-		_, err := writer.Write(resp)
-		if err != nil {
-			log.Warn().Msgf("Could not send response: %s", err)
-
-			return
-		}
-
-		if f, ok := writer.(http.Flusher); ok {
-			f.Flush()
-		}
-
-		// 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(machine.Namespace.Name, machine.Hostname, "endpoint-update").
-			Inc()
-		updateChan <- struct{}{}
-
-		return
-	} else if mapRequest.OmitPeers && mapRequest.Stream {
-		log.Warn().
-			Str("handler", "NoisePollNetMap").
-			Str("machine", machine.Hostname).
-			Msg("Ignoring request, don't know how to handle it")
-		http.Error(writer, "Internal error", http.StatusBadRequest)
-
-		return
-	}
-
-	log.Info().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Client is ready to access the tailnet")
-	log.Info().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Sending initial map")
-	pollDataChan <- resp
-
-	log.Info().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Notifying peers")
-	updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "full-update").
-		Inc()
-	updateChan <- struct{}{}
-
-	h.NoisePollNetMapStream(
-		writer,
-		req,
-		machine,
-		mapRequest,
-		pollDataChan,
-		keepAliveChan,
-		updateChan,
-	)
-
-	log.Trace().
-		Str("handler", "NoisePollNetMap").
-		Str("machine", machine.Hostname).
-		Msg("Finished stream, closing PollNetMap session")
-}
-
-// PollNetMapStream takes care of /machine/:id/map
-// stream logic, ensuring we communicate updates and data
-// to the connected clients.
-func (h *Headscale) NoisePollNetMapStream(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machine *Machine,
-	mapRequest tailcfg.MapRequest,
-	pollDataChan chan []byte,
-	keepAliveChan chan []byte,
-	updateChan chan struct{},
-) {
-	ctx := context.WithValue(context.Background(), machineNameContextKey, machine.Hostname)
-
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel()
-
-	go h.noiseScheduledPollWorker(
-		ctx,
-		updateChan,
-		keepAliveChan,
-		mapRequest,
-		machine,
-	)
-
-	for {
-		log.Trace().
-			Str("handler", "NoisePollNetMapStream").
-			Str("machine", machine.Hostname).
-			Msg("Waiting for data to stream...")
-
-		log.Trace().
-			Str("handler", "NoisePollNetMapStream").
-			Str("machine", machine.Hostname).
-			Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
-
-		select {
-		case data := <-pollDataChan:
-			log.Trace().
-				Str("handler", "NoisePollNetMapStream").
-				Str("machine", machine.Hostname).
-				Str("channel", "pollData").
-				Int("bytes", len(data)).
-				Msg("Sending data received via pollData channel")
-			_, err := writer.Write(data)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "pollData").
-					Err(err).
-					Msg("Cannot write data")
-
-				return
-			}
-
-			flusher, ok := writer.(http.Flusher)
-			if !ok {
-				log.Error().
-					Caller().
-					Str("handler", "PollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "pollData").
-					Msg("Cannot cast writer to http.Flusher")
-			} else {
-				flusher.Flush()
-			}
-			log.Trace().
-				Str("handler", "NoisePollNetMapStream").
-				Str("machine", machine.Hostname).
-				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.UpdateMachineFromDatabase(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "pollData").
-					Err(err).
-					Msg("Cannot update machine from database")
-
-				// client has been removed from database
-				// since the stream opened, terminate connection.
-				return
-			}
-			now := time.Now().UTC()
-			machine.LastSeen = &now
-
-			lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname).
-				Set(float64(now.Unix()))
-			machine.LastSuccessfulUpdate = &now
-
-			err = h.TouchMachine(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "pollData").
-					Err(err).
-					Msg("Cannot update machine LastSuccessfulUpdate")
-			} else {
-				log.Trace().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "pollData").
-					Int("bytes", len(data)).
-					Msg("Machine entry in database updated successfully after sending pollData")
-			}
-
-			break
-
-		case data := <-keepAliveChan:
-			log.Trace().
-				Str("handler", "PollNetMapStream").
-				Str("machine", machine.Hostname).
-				Str("channel", "keepAlive").
-				Int("bytes", len(data)).
-				Msg("Sending keep alive message")
-			_, err := writer.Write(data)
-			if err != nil {
-				log.Error().
-					Str("handler", "PollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "keepAlive").
-					Err(err).
-					Msg("Cannot write keep alive message")
-
-				return
-			}
-			flusher, ok := writer.(http.Flusher)
-			if !ok {
-				log.Error().
-					Caller().
-					Str("handler", "PollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "keepAlive").
-					Msg("Cannot cast writer to http.Flusher")
-			} else {
-				flusher.Flush()
-			}
-
-			log.Trace().
-				Str("handler", "NoisePollNetMapStream").
-				Str("machine", machine.Hostname).
-				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.UpdateMachineFromDatabase(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "keepAlive").
-					Err(err).
-					Msg("Cannot update machine from database")
-
-				// client has been removed from database
-				// since the stream opened, terminate connection.
-				return
-			}
-			now := time.Now().UTC()
-			machine.LastSeen = &now
-			err = h.TouchMachine(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "keepAlive").
-					Err(err).
-					Msg("Cannot update machine LastSeen")
-			} else {
-				log.Trace().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "keepAlive").
-					Int("bytes", len(data)).
-					Msg("Machine updated successfully after sending keep alive")
-			}
-
-			return
-
-		case <-updateChan:
-			log.Trace().
-				Str("handler", "NoisePollNetMapStream").
-				Str("machine", machine.Hostname).
-				Str("channel", "update").
-				Msg("Received a request for update")
-			updateRequestsReceivedOnChannel.WithLabelValues(machine.Namespace.Name, machine.Hostname).
-				Inc()
-			if h.isOutdated(machine) {
-				var lastUpdate time.Time
-				if machine.LastSuccessfulUpdate != nil {
-					lastUpdate = *machine.LastSuccessfulUpdate
-				}
-				log.Debug().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Time("last_successful_update", lastUpdate).
-					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
-					Msgf("There has been updates since the last successful update to %s", machine.Hostname)
-				data, err := h.getNoiseMapResponse(mapRequest, machine)
-				if err != nil {
-					log.Error().
-						Str("handler", "NoisePollNetMapStream").
-						Str("machine", machine.Hostname).
-						Str("channel", "update").
-						Err(err).
-						Msg("Could not get the map update")
-				}
-				_, err = writer.Write(data)
-				if err != nil {
-					log.Error().
-						Str("handler", "NoisePollNetMapStream").
-						Str("machine", machine.Hostname).
-						Str("channel", "update").
-						Err(err).
-						Msg("Could not write the map response")
-					updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "failed").
-						Inc()
-
-					return
-				}
-
-				if f, ok := writer.(http.Flusher); ok {
-					f.Flush()
-				}
-
-				log.Trace().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "update").
-					Msg("Updated Map has been sent")
-				updateRequestsSentToNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "success").
-					Inc()
-
-				// Keep track of the last successful update,
-				// we sometimes end in a state were the update
-				// is not picked up by a client and we use this
-				// to determine if we should "force" an update.
-				// 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().
-						Str("handler", "NoisePollNetMapStream").
-						Str("machine", machine.Hostname).
-						Str("channel", "update").
-						Err(err).
-						Msg("Cannot update machine from database")
-
-					// client has been removed from database
-					// since the stream opened, terminate connection.
-					return
-				}
-				now := time.Now().UTC()
-
-				lastStateUpdate.WithLabelValues(machine.Namespace.Name, machine.Hostname).
-					Set(float64(now.Unix()))
-				machine.LastSuccessfulUpdate = &now
-
-				err = h.TouchMachine(machine)
-				if err != nil {
-					log.Error().
-						Str("handler", "NoisePollNetMapStream").
-						Str("machine", machine.Hostname).
-						Str("channel", "update").
-						Err(err).
-						Msg("Cannot update machine LastSuccessfulUpdate")
-				}
-			} else {
-				var lastUpdate time.Time
-				if machine.LastSuccessfulUpdate != nil {
-					lastUpdate = *machine.LastSuccessfulUpdate
-				}
-				log.Trace().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Time("last_successful_update", lastUpdate).
-					Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)).
-					Msgf("%s is up to date", machine.Hostname)
-			}
-
-			return
-
-		case <-ctx.Done():
-			log.Info().
-				Str("handler", "NoisePollNetMapStream").
-				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.
-			err := h.UpdateMachineFromDatabase(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "Done").
-					Err(err).
-					Msg("Cannot update machine from database")
-
-				// client has been removed from database
-				// since the stream opened, terminate connection.
-				return
-			}
-			now := time.Now().UTC()
-			machine.LastSeen = &now
-			err = h.TouchMachine(machine)
-			if err != nil {
-				log.Error().
-					Str("handler", "NoisePollNetMapStream").
-					Str("machine", machine.Hostname).
-					Str("channel", "Done").
-					Err(err).
-					Msg("Cannot update machine LastSeen")
-			}
-
-			return
-		}
-	}
-}
-
-func (h *Headscale) noiseScheduledPollWorker(
-	ctx context.Context,
-	updateChan chan struct{},
-	keepAliveChan chan []byte,
-	mapRequest tailcfg.MapRequest,
-	machine *Machine,
-) {
-	keepAliveTicker := time.NewTicker(keepAliveInterval)
-	updateCheckerTicker := time.NewTicker(h.cfg.NodeUpdateCheckInterval)
-
-	defer closeChanWithLog(
-		updateChan,
-		fmt.Sprint(ctx.Value(machineNameContextKey)),
-		"updateChan",
-	)
-	defer closeChanWithLog(
-		keepAliveChan,
-		fmt.Sprint(ctx.Value(machineNameContextKey)),
-		"updateChan",
-	)
-
-	for {
-		select {
-		case <-ctx.Done():
-			return
-
-		case <-keepAliveTicker.C:
-			data, err := h.getNoiseMapKeepAliveResponse(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", machine.Hostname).
-				Msg("Sending keepalive")
-			keepAliveChan <- data
-
-		case <-updateCheckerTicker.C:
-			log.Debug().
-				Str("func", "scheduledPollWorker").
-				Str("machine", machine.Hostname).
-				Msg("Sending noise update request")
-			updateRequestsFromNode.WithLabelValues(machine.Namespace.Name, machine.Hostname, "scheduled-update").
-				Inc()
-			updateChan <- struct{}{}
-		}
-	}
-}
-
-func (h *Headscale) getNoiseMapKeepAliveResponse(req tailcfg.MapRequest) ([]byte, error) {
-	resp := tailcfg.MapResponse{
-		KeepAlive: true,
-	}
-
-	// The TS2021 protocol does not rely anymore on the machine key to
-	// encrypt in a NaCl box the map response. We just send it back
-	// unencrypted via the encrypted Noise channel.
-	// declare the incoming size on the first 4 bytes
-	respBody, err := json.Marshal(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot marshal map response")
-	}
-
-	var srcCompressed []byte
-	if req.Compress == ZstdCompression {
-		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed = encoder.EncodeAll(respBody, nil)
-	} else {
-		srcCompressed = respBody
-	}
-
-	data := make([]byte, reservedResponseHeaderSize)
-	binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
-	data = append(data, srcCompressed...)
-
-	return data, nil
-}
-
-func (h *Headscale) getNoiseMapResponse(
-	req tailcfg.MapRequest,
-	machine *Machine,
-) ([]byte, error) {
-	log.Trace().
-		Str("func", "getNoiseMapResponse").
-		Str("machine", req.Hostinfo.Hostname).
-		Msg("Creating Map response")
-
-	resp, err := h.generateMapResponse(req, machine)
-	if err != nil {
-		log.Error().
-			Str("func", "getNoiseMapResponse").
-			Err(err).
-			Msg("Error generating the map response")
-
-		return nil, err
-	}
-
-	log.Trace().
-		Str("func", "getNoiseMapResponse").
-		Str("machine", req.Hostinfo.Hostname).
-		Msgf("Generated map response: %s", tailMapResponseToString(*resp))
-
-	// The TS2021 protocol does not rely anymore on the machine key to
-	// encrypt in a NaCl box the map response. We just send it back
-	// unencrypted via the encrypted Noise channel.
-	// declare the incoming size on the first 4 bytes
-	respBody, err := json.Marshal(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot marshal map response")
-	}
-
-	var srcCompressed []byte
-	if req.Compress == ZstdCompression {
-		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed = encoder.EncodeAll(respBody, nil)
-	} else {
-		srcCompressed = respBody
-	}
-
-	data := make([]byte, reservedResponseHeaderSize)
-	binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
-	data = append(data, srcCompressed...)
-
-	return data, nil
-}
diff --git a/protocol_noise_poll.go b/protocol_noise_poll.go
new file mode 100644
index 0000000000..7556ae3025
--- /dev/null
+++ b/protocol_noise_poll.go
@@ -0,0 +1,67 @@
+package headscale
+
+import (
+	"encoding/json"
+	"errors"
+	"io"
+	"net/http"
+
+	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
+	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
+)
+
+// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
+//
+// This is the busiest endpoint, as it keeps the HTTP long poll that updates
+// the clients when something in the network changes.
+//
+// The clients POST stuff like HostInfo and their Endpoints here, but
+// 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) NoisePollNetMapHandler(
+	writer http.ResponseWriter,
+	req *http.Request,
+) {
+	log.Trace().
+		Str("handler", "NoisePollNetMap").
+		Msg("PollNetMapHandler called")
+	body, _ := io.ReadAll(req.Body)
+
+	mapRequest := tailcfg.MapRequest{}
+	if err := json.Unmarshal(body, &mapRequest); err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot parse MapRequest")
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+
+	machine, err := h.GetMachineByAnyNodeKey(mapRequest.NodeKey, key.NodePublic{})
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			log.Warn().
+				Str("handler", "NoisePollNetMap").
+				Msgf("Ignoring request, cannot find machine with key %s", mapRequest.NodeKey.String())
+			http.Error(writer, "Internal error", http.StatusNotFound)
+
+			return
+		}
+		log.Error().
+			Str("handler", "NoisePollNetMap").
+			Msgf("Failed to fetch machine from the database with node key: %s", mapRequest.NodeKey.String())
+		http.Error(writer, "Internal error", http.StatusInternalServerError)
+
+		return
+	}
+	log.Debug().
+		Str("handler", "NoisePollNetMap").
+		Str("machine", machine.Hostname).
+		Msg("Found Noise machine in database")
+
+	h.handlePollCommon(writer, req, machine, mapRequest, true)
+}

From 704a19b0a58117a392f0a172a422ef7640ee2303 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 23:13:07 +0200
Subject: [PATCH 32/63] Removed legacy method to generate MapResponse

---
 api.go | 42 ------------------------------------------
 1 file changed, 42 deletions(-)

diff --git a/api.go b/api.go
index 38e8e64bfa..ab8c2a87e8 100644
--- a/api.go
+++ b/api.go
@@ -2,7 +2,6 @@ package headscale
 
 import (
 	"bytes"
-	"encoding/binary"
 	"encoding/json"
 	"fmt"
 	"html/template"
@@ -11,7 +10,6 @@ import (
 	"time"
 
 	"github.com/gorilla/mux"
-	"github.com/klauspost/compress/zstd"
 	"github.com/rs/zerolog/log"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
@@ -151,46 +149,6 @@ func (h *Headscale) RegisterWebAPI(
 	}
 }
 
-func (h *Headscale) getLegacyMapResponseData(
-	machineKey key.MachinePublic,
-	mapRequest tailcfg.MapRequest,
-	machine *Machine,
-) ([]byte, error) {
-	resp, err := h.generateMapResponse(mapRequest, machine)
-	if err != nil {
-		return nil, err
-	}
-
-	var respBody []byte
-	if mapRequest.Compress == ZstdCompression {
-		src, err := json.Marshal(resp)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "getMapResponse").
-				Err(err).
-				Msg("Failed to marshal response for the client")
-
-			return nil, err
-		}
-
-		encoder, _ := zstd.NewWriter(nil)
-		srcCompressed := encoder.EncodeAll(src, nil)
-		respBody = h.privateKey.SealTo(machineKey, srcCompressed)
-	} else {
-		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, reservedResponseHeaderSize)
-	binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
-	data = append(data, respBody...)
-
-	return data, nil
-}
-
 func (h *Headscale) handleMachineLogOut(
 	writer http.ResponseWriter,
 	req *http.Request,

From f599bea216a4541d7f488e4f7f7e555123c48580 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 14 Aug 2022 23:15:41 +0200
Subject: [PATCH 33/63] Fixed issue when not using compression

---
 protocol_common_utils.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/protocol_common_utils.go b/protocol_common_utils.go
index 3eb547154d..516b616cfb 100644
--- a/protocol_common_utils.go
+++ b/protocol_common_utils.go
@@ -88,6 +88,8 @@ func (h *Headscale) marshalResponse(
 	} else {
 		if !machineKey.IsZero() { // if legacy protocol
 			respBody = h.privateKey.SealTo(machineKey, jsonBody)
+		} else {
+			respBody = jsonBody
 		}
 	}
 

From 5cf9eedf42e883437eba2d793700747e00ff3674 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 15 Aug 2022 10:43:39 +0200
Subject: [PATCH 34/63] Minor logging corrections

---
 noise.go                | 2 +-
 protocol_common_poll.go | 7 ++++---
 utils.go                | 5 ++++-
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/noise.go b/noise.go
index 16754c6997..c38005762c 100644
--- a/noise.go
+++ b/noise.go
@@ -58,7 +58,7 @@ func (h *Headscale) NoiseUpgradeHandler(
 	server.Handler = h2c.NewHandler(h.noiseMux, &http2.Server{})
 	err = server.Serve(netutil.NewOneConnListener(noiseConn, nil))
 	if err != nil {
-		log.Error().Err(err).Msg("noise server launch failed")
+		log.Info().Err(err).Msg("The HTTP2 server was closed")
 	}
 }
 
diff --git a/protocol_common_poll.go b/protocol_common_poll.go
index 11178cd328..f8265d386c 100644
--- a/protocol_common_poll.go
+++ b/protocol_common_poll.go
@@ -18,6 +18,8 @@ type contextKey string
 
 const machineNameContextKey = contextKey("machineName")
 
+// handlePollCommon is the common code for the legacy and Noise protocols to
+// managed the poll loop.
 func (h *Headscale) handlePollCommon(
 	writer http.ResponseWriter,
 	req *http.Request,
@@ -37,7 +39,6 @@ func (h *Headscale) handlePollCommon(
 			log.Error().
 				Caller().
 				Bool("noise", isNoise).
-				Str("func", "handleAuthKey").
 				Str("machine", machine.Hostname).
 				Err(err)
 		}
@@ -246,13 +247,13 @@ func (h *Headscale) pollNetMapStream(
 	)
 
 	log.Trace().
-		Str("handler", "PollNetMapStream").
+		Str("handler", "pollNetMapStream").
 		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msg("Waiting for data to stream...")
 
 	log.Trace().
-		Str("handler", "PollNetMapStream").
+		Str("handler", "pollNetMapStream").
 		Bool("noise", isNoise).
 		Str("machine", machine.Hostname).
 		Msgf("pollData is %#v, keepAliveChan is %#v, updateChan is %#v", pollDataChan, keepAliveChan, updateChan)
diff --git a/utils.go b/utils.go
index 089e867b22..7727a24569 100644
--- a/utils.go
+++ b/utils.go
@@ -118,7 +118,10 @@ func decode(
 	pubKey *key.MachinePublic,
 	privKey *key.MachinePrivate,
 ) error {
-	log.Trace().Int("length", len(msg)).Msg("Trying to decrypt")
+	log.Trace().
+		Str("pubkey", pubKey.ShortString()).
+		Int("length", len(msg)).
+		Msg("Trying to decrypt")
 
 	decrypted, ok := privKey.OpenFrom(*pubKey, msg)
 	if !ok {

From b8980b9ed36e69ada83aa0d3f027b96344e970ef Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 15 Aug 2022 10:44:22 +0200
Subject: [PATCH 35/63] More minor logging stuff

---
 protocol_legacy_poll.go | 2 +-
 protocol_noise_poll.go  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/protocol_legacy_poll.go b/protocol_legacy_poll.go
index a42f399c83..f7ef6548f7 100644
--- a/protocol_legacy_poll.go
+++ b/protocol_legacy_poll.go
@@ -88,7 +88,7 @@ func (h *Headscale) PollNetMapHandler(
 		Str("handler", "PollNetMap").
 		Str("id", machineKeyStr).
 		Str("machine", machine.Hostname).
-		Msg("Found machine in database")
+		Msg("A machine is entering polling via the legacy protocol")
 
 	h.handlePollCommon(writer, req, machine, mapRequest, false)
 }
diff --git a/protocol_noise_poll.go b/protocol_noise_poll.go
index 7556ae3025..8498dcfb40 100644
--- a/protocol_noise_poll.go
+++ b/protocol_noise_poll.go
@@ -61,7 +61,7 @@ func (h *Headscale) NoisePollNetMapHandler(
 	log.Debug().
 		Str("handler", "NoisePollNetMap").
 		Str("machine", machine.Hostname).
-		Msg("Found Noise machine in database")
+		Msg("A machine is entering polling via the Noise protocol")
 
 	h.handlePollCommon(writer, req, machine, mapRequest, true)
 }

From 8db7629edf64071d7dce682b0df333c785003b4e Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 15 Aug 2022 10:53:06 +0200
Subject: [PATCH 36/63] Fix config file in integration tests for Noise

---
 integration_test/etc/config.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml
index e6b34afa72..c6eede6b28 100644
--- a/integration_test/etc/config.yaml
+++ b/integration_test/etc/config.yaml
@@ -14,6 +14,7 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
+noise_private_key_path: noise_private.key
 listen_addr: 0.0.0.0:8080
 metrics_listen_addr: 127.0.0.1:9090
 server_url: http://headscale:8080

From 865f1ffb3c355ff8edda3aecea0aa6aa09d7a397 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 15 Aug 2022 11:25:47 +0200
Subject: [PATCH 37/63] Fix issues with DERP integration tests due to
 tailscale/tailscale#4323

---
 integration_test/etc_embedded_derp/config.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index 86946116aa..86a59848b9 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -15,8 +15,8 @@ dns_config:
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
 noise_private_key_path: noise_private.key
-listen_addr: 0.0.0.0:8443
-server_url: https://headscale:8443
+listen_addr: 0.0.0.0:443
+server_url: https://headscale:443
 tls_cert_path: "/etc/headscale/tls/server.crt"
 tls_key_path: "/etc/headscale/tls/server.key"
 tls_client_auth_mode: disabled

From b3cf5289f894009c2a040d31f28da144729fca4e Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 15 Aug 2022 23:35:06 +0200
Subject: [PATCH 38/63] Use CapVer to offer Noise only to supported clients

---
 api.go             |  6 ------
 protocol_common.go | 20 ++++++++++++++++++++
 2 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/api.go b/api.go
index ab8c2a87e8..56ca3db6de 100644
--- a/api.go
+++ b/api.go
@@ -25,12 +25,6 @@ const (
 	ErrRegisterMethodCLIDoesNotSupportExpire = Error(
 		"machines registered with CLI does not support expire",
 	)
-
-	// The CapabilityVersion is used by Tailscale clients to indicate
-	// their codebase version. Tailscale clients can communicate over TS2021
-	// from CapabilityVersion 28.
-	// See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
-	NoiseCapabilityVersion = 28
 )
 
 func (h *Headscale) HealthHandler(
diff --git a/protocol_common.go b/protocol_common.go
index c8eab80e5c..e196c3ee9a 100644
--- a/protocol_common.go
+++ b/protocol_common.go
@@ -9,6 +9,19 @@ import (
 	"tailscale.com/tailcfg"
 )
 
+const (
+	// The CapabilityVersion is used by Tailscale clients to indicate
+	// their codebase version. Tailscale clients can communicate over TS2021
+	// from CapabilityVersion 28, but we only have good support for it
+	// since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port).
+	//
+	// Related to this change, there is https://github.com/tailscale/tailscale/pull/5379,
+	// where CapabilityVersion 39 is introduced to indicate #4323 was merged.
+	//
+	// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
+	NoiseCapabilityVersion = 39
+)
+
 // KeyHandler provides the Headscale pub key
 // Listens in /key.
 func (h *Headscale) KeyHandler(
@@ -18,6 +31,10 @@ func (h *Headscale) KeyHandler(
 	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
 	clientCapabilityStr := req.URL.Query().Get("v")
 	if clientCapabilityStr != "" {
+		log.Debug().
+			Str("handler", "/key").
+			Str("v", clientCapabilityStr).
+			Msg("New noise client")
 		clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
 		if err != nil {
 			writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -52,6 +69,9 @@ func (h *Headscale) KeyHandler(
 			return
 		}
 	}
+	log.Debug().
+		Str("handler", "/key").
+		Msg("New legacy client")
 
 	// Old clients don't send a 'v' parameter, so we send the legacy public key
 	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")

From eb461d0713dbf9a3972eea14ac6c5f840d8e6ee1 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 00:18:02 +0200
Subject: [PATCH 39/63] Enable HEAD and unstable in integration tests

---
 integration_common_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/integration_common_test.go b/integration_common_test.go
index 0235f25b7a..2b82436609 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -30,8 +30,8 @@ var (
 	IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
 
 	tailscaleVersions = []string{
-		// "head",
-		// "unstable",
+		"head",
+		"unstable",
 		"1.28.0",
 		"1.26.2",
 		"1.24.2",

From 0db7fc5ab726937e7ab860fd917a70f84dae5927 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 13:39:15 +0200
Subject: [PATCH 40/63] Mark all namespaces to lastChange now

---
 app.go                  | 2 +-
 machine.go              | 8 ++++----
 protocol_common_poll.go | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/app.go b/app.go
index 7722bf795c..966fb3a9c5 100644
--- a/app.go
+++ b/app.go
@@ -270,7 +270,7 @@ func (h *Headscale) expireEphemeralNodesWorker() {
 		}
 
 		if expiredFound {
-			h.setLastStateChangeToNow(namespace.Name)
+			h.setLastStateChangeToNow()
 		}
 	}
 }
diff --git a/machine.go b/machine.go
index d9fd789d1a..1773e8ffc0 100644
--- a/machine.go
+++ b/machine.go
@@ -410,7 +410,7 @@ func (h *Headscale) SetTags(machine *Machine, tags []string) error {
 	if err := h.UpdateACLRules(); err != nil && !errors.Is(err, errEmptyPolicy) {
 		return err
 	}
-	h.setLastStateChangeToNow(machine.Namespace.Name)
+	h.setLastStateChangeToNow()
 
 	if err := h.db.Save(machine).Error; err != nil {
 		return fmt.Errorf("failed to update tags for machine in the database: %w", err)
@@ -424,7 +424,7 @@ func (h *Headscale) ExpireMachine(machine *Machine) error {
 	now := time.Now()
 	machine.Expiry = &now
 
-	h.setLastStateChangeToNow(machine.Namespace.Name)
+	h.setLastStateChangeToNow()
 
 	if err := h.db.Save(machine).Error; err != nil {
 		return fmt.Errorf("failed to expire machine in the database: %w", err)
@@ -451,7 +451,7 @@ func (h *Headscale) RenameMachine(machine *Machine, newName string) error {
 	}
 	machine.GivenName = newName
 
-	h.setLastStateChangeToNow(machine.Namespace.Name)
+	h.setLastStateChangeToNow()
 
 	if err := h.db.Save(machine).Error; err != nil {
 		return fmt.Errorf("failed to rename machine in the database: %w", err)
@@ -467,7 +467,7 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) error {
 	machine.LastSuccessfulUpdate = &now
 	machine.Expiry = &expiry
 
-	h.setLastStateChangeToNow(machine.Namespace.Name)
+	h.setLastStateChangeToNow()
 
 	if err := h.db.Save(machine).Error; err != nil {
 		return fmt.Errorf(
diff --git a/protocol_common_poll.go b/protocol_common_poll.go
index f8265d386c..65dcb55632 100644
--- a/protocol_common_poll.go
+++ b/protocol_common_poll.go
@@ -125,7 +125,7 @@ func (h *Headscale) handlePollCommon(
 
 	// There has been an update to _any_ of the nodes that the other nodes would
 	// need to know about
-	h.setLastStateChangeToNow(machine.Namespace.Name)
+	h.setLastStateChangeToNow()
 
 	// The request is not ReadOnly, so we need to set up channels for updating
 	// peers via longpoll

From c0fe1abf4ddbd5b7ccda5dc42d02cb0bcf0f6a26 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 17:51:43 +0200
Subject: [PATCH 41/63] Use node_key to find peers

---
 machine.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/machine.go b/machine.go
index 1773e8ffc0..89f0c7e77b 100644
--- a/machine.go
+++ b/machine.go
@@ -244,8 +244,8 @@ func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
 		Msg("Finding direct peers")
 
 	machines := Machines{}
-	if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
-		machine.MachineKey).Find(&machines).Error; err != nil {
+	if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("node_key <> ?",
+		machine.NodeKey).Find(&machines).Error; err != nil {
 		log.Error().Err(err).Msg("Error accessing db")
 
 		return Machines{}, err

From ce53bb0eee3b8cd16a6dca8b52492faf87cf0839 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 17:52:59 +0200
Subject: [PATCH 42/63] Minor changes to HEAD Dockerfile

---
 Dockerfile.tailscale-HEAD | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD
index b62f7e2bf6..c6e894daf7 100644
--- a/Dockerfile.tailscale-HEAD
+++ b/Dockerfile.tailscale-HEAD
@@ -7,7 +7,9 @@ RUN apt-get update \
 
 RUN git clone https://github.com/tailscale/tailscale.git
 
-WORKDIR tailscale
+WORKDIR /go/tailscale
+
+RUN git checkout main
 
 RUN sh build_dist.sh tailscale.com/cmd/tailscale
 RUN sh build_dist.sh tailscale.com/cmd/tailscaled

From b71a881d0e455b1543c101fd0547263ef1bde2e1 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 18:19:04 +0200
Subject: [PATCH 43/63] Retry magicdns tests

---
 integration_general_test.go | 35 +++++++++++++++++++++++++----------
 1 file changed, 25 insertions(+), 10 deletions(-)

diff --git a/integration_general_test.go b/integration_general_test.go
index d4c64c38af..3296762a67 100644
--- a/integration_general_test.go
+++ b/integration_general_test.go
@@ -683,6 +683,18 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
 		ips, err := getIPs(scales.tailscales)
 		assert.Nil(s.T(), err)
 
+		retry := func(times int, sleepInverval time.Duration, doWork func() (string, error)) (result string, err error) {
+			for attempts := 0; attempts < times; attempts++ {
+				result, err = doWork()
+				if err == nil {
+					return
+				}
+				time.Sleep(sleepInverval)
+			}
+
+			return
+		}
+
 		for hostname, tailscale := range scales.tailscales {
 			for _, peername := range hostnames {
 				if strings.Contains(peername, hostname) {
@@ -693,17 +705,20 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
 					command := []string{
 						"tailscale", "ip", peername,
 					}
+					result, err := retry(10, 1*time.Second, func() (string, error) {
+						log.Printf(
+							"Resolving name %s from %s\n",
+							peername,
+							hostname,
+						)
+						result, err := ExecuteCommand(
+							&tailscale,
+							command,
+							[]string{},
+						)
+						return result, err
+					})
 
-					log.Printf(
-						"Resolving name %s from %s\n",
-						peername,
-						hostname,
-					)
-					result, err := ExecuteCommand(
-						&tailscale,
-						command,
-						[]string{},
-					)
 					assert.Nil(t, err)
 					log.Printf("Result for %s: %s\n", hostname, result)
 

From ba07bac46a0c116987305f8161de151daf1d9626 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 16 Aug 2022 18:42:22 +0200
Subject: [PATCH 44/63] Use IPv4 in the tests

---
 integration_general_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/integration_general_test.go b/integration_general_test.go
index 3296762a67..840b816530 100644
--- a/integration_general_test.go
+++ b/integration_general_test.go
@@ -566,7 +566,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 					command := []string{
 						"tailscale", "file", "cp",
 						fmt.Sprintf("/tmp/file_from_%s", hostname),
-						fmt.Sprintf("%s:", ips[peername][1]),
+						fmt.Sprintf("%s:", ips[peername][0]),
 					}
 					retry(10, 1*time.Second, func() error {
 						log.Printf(

From 8a707de5f171957c60dd1317c5f18b44bd704e7c Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 17:53:04 +0200
Subject: [PATCH 45/63] Add local Docker DNS server (makes resolving
 http://headscale more reliable)

---
 integration_test/etc/config.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml
index c6eede6b28..9866d554ec 100644
--- a/integration_test/etc/config.yaml
+++ b/integration_test/etc/config.yaml
@@ -11,6 +11,7 @@ dns_config:
   magic_dns: true
   domains: []
   nameservers:
+    - 127.0.0.11
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key

From 7185f8dfeae70541df7bf5cd561fe84c93a0c5df Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 17:53:25 +0200
Subject: [PATCH 46/63] Only use released versions in public integration tests

---
 integration_common_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/integration_common_test.go b/integration_common_test.go
index 2b82436609..0235f25b7a 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -30,8 +30,8 @@ var (
 	IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
 
 	tailscaleVersions = []string{
-		"head",
-		"unstable",
+		// "head",
+		// "unstable",
 		"1.28.0",
 		"1.26.2",
 		"1.24.2",

From f43a83aad7e0a1a4a3b2559c663a064a5fa2d828 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 17:53:36 +0200
Subject: [PATCH 47/63] Find out IPv4 for taildrop

---
 integration_general_test.go | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/integration_general_test.go b/integration_general_test.go
index 840b816530..d363ee269d 100644
--- a/integration_general_test.go
+++ b/integration_general_test.go
@@ -562,11 +562,23 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 				if peername == hostname {
 					continue
 				}
+
+				var ip4 netaddr.IP
+				for _, ip := range ips[peername] {
+					if ip.Is4() {
+						ip4 = ip
+						break
+					}
+				}
+				if ip4.IsZero() {
+					panic("no ipv4 address found")
+				}
+
 				s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
 					command := []string{
 						"tailscale", "file", "cp",
 						fmt.Sprintf("/tmp/file_from_%s", hostname),
-						fmt.Sprintf("%s:", ips[peername][0]),
+						fmt.Sprintf("%s:", ip4),
 					}
 					retry(10, 1*time.Second, func() error {
 						log.Printf(

From cf731fafab53a81fa17d72ba828a3a2f306ac92a Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 17:56:01 +0200
Subject: [PATCH 48/63] Catch retry error in taildrop send

---
 integration_general_test.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/integration_general_test.go b/integration_general_test.go
index d363ee269d..9f2ca3ea61 100644
--- a/integration_general_test.go
+++ b/integration_general_test.go
@@ -580,7 +580,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 						fmt.Sprintf("/tmp/file_from_%s", hostname),
 						fmt.Sprintf("%s:", ip4),
 					}
-					retry(10, 1*time.Second, func() error {
+					err := retry(10, 1*time.Second, func() error {
 						log.Printf(
 							"Sending file from %s to %s\n",
 							hostname,
@@ -594,6 +594,7 @@ func (s *IntegrationTestSuite) TestTailDrop() {
 						)
 						return err
 					})
+
 					assert.Nil(t, err)
 				})
 			}

From f9a2a2b57a062d5ef9801a18c77469b89bb4a1d3 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 18:07:15 +0200
Subject: [PATCH 49/63] Add docker DNS IP to the remaining files

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

diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml
index e02d706729..e71c957658 100644
--- a/integration_test/etc/alt-config.dump.gold.yaml
+++ b/integration_test/etc/alt-config.dump.gold.yaml
@@ -18,6 +18,7 @@ dns_config:
   domains: []
   magic_dns: true
   nameservers:
+    - 127.0.0.11
     - 1.1.1.1
 ephemeral_node_inactivity_timeout: 30m
 node_update_check_interval: 10s
diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml
index 8a6d7396fd..35dd9e4802 100644
--- a/integration_test/etc/alt-config.yaml
+++ b/integration_test/etc/alt-config.yaml
@@ -11,6 +11,7 @@ dns_config:
   magic_dns: true
   domains: []
   nameservers:
+    - 127.0.0.11
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml
index f474e89032..715396566b 100644
--- a/integration_test/etc/config.dump.gold.yaml
+++ b/integration_test/etc/config.dump.gold.yaml
@@ -18,6 +18,7 @@ dns_config:
   domains: []
   magic_dns: true
   nameservers:
+    - 127.0.0.11
     - 1.1.1.1
 ephemeral_node_inactivity_timeout: 30m
 node_update_check_interval: 10s

From 9d430d3c727fad9b6adc6efb28eab8718562f524 Mon Sep 17 00:00:00 2001
From: Juan Font <juanfontalonso@gmail.com>
Date: Thu, 18 Aug 2022 21:33:56 +0200
Subject: [PATCH 50/63] Update noise.go

Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
---
 noise.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/noise.go b/noise.go
index c38005762c..cf18fbfbc1 100644
--- a/noise.go
+++ b/noise.go
@@ -39,7 +39,7 @@ func (h *Headscale) NoiseUpgradeHandler(
 ) {
 	log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr)
 
-	// Under normal circumpstances, we should be able to use the controlhttp.AcceptHTTP()
+	// Under normal circumstances, we should be able to use the controlhttp.AcceptHTTP()
 	// function to do this - kindly left there by the Tailscale authors for us to use.
 	// (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go)
 	//

From a87a9636e3f1432b3f944fc7dd81a7adba198d75 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 14:19:29 +0200
Subject: [PATCH 51/63] Expanded response marshal methods to support legacy and
 Noise

---
 protocol_common_utils.go | 29 ++++++++++++++++++++++++-----
 1 file changed, 24 insertions(+), 5 deletions(-)

diff --git a/protocol_common_utils.go b/protocol_common_utils.go
index 516b616cfb..3dc435f323 100644
--- a/protocol_common_utils.go
+++ b/protocol_common_utils.go
@@ -21,7 +21,7 @@ func (h *Headscale) getMapResponseData(
 	}
 
 	if isNoise {
-		return h.marshalResponse(mapResponse, mapRequest.Compress, key.MachinePublic{})
+		return h.marshalMapResponse(mapResponse, key.MachinePublic{}, mapRequest.Compress)
 	}
 
 	var machineKey key.MachinePublic
@@ -35,7 +35,7 @@ func (h *Headscale) getMapResponseData(
 		return nil, err
 	}
 
-	return h.marshalResponse(mapResponse, mapRequest.Compress, machineKey)
+	return h.marshalMapResponse(mapResponse, machineKey, mapRequest.Compress)
 }
 
 func (h *Headscale) getMapKeepAliveResponseData(
@@ -48,7 +48,7 @@ func (h *Headscale) getMapKeepAliveResponseData(
 	}
 
 	if isNoise {
-		return h.marshalResponse(keepAliveResponse, mapRequest.Compress, key.MachinePublic{})
+		return h.marshalMapResponse(keepAliveResponse, key.MachinePublic{}, mapRequest.Compress)
 	}
 
 	var machineKey key.MachinePublic
@@ -62,13 +62,32 @@ func (h *Headscale) getMapKeepAliveResponseData(
 		return nil, err
 	}
 
-	return h.marshalResponse(keepAliveResponse, mapRequest.Compress, machineKey)
+	return h.marshalMapResponse(keepAliveResponse, machineKey, mapRequest.Compress)
 }
 
 func (h *Headscale) marshalResponse(
 	resp interface{},
-	compression string,
 	machineKey key.MachinePublic,
+) ([]byte, error) {
+	jsonBody, err := json.Marshal(resp)
+	if err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Cannot marshal response")
+	}
+
+	if machineKey.IsZero() { // if Noise
+		return jsonBody, nil
+	}
+
+	return h.privateKey.SealTo(machineKey, jsonBody), nil
+}
+
+func (h *Headscale) marshalMapResponse(
+	resp interface{},
+	machineKey key.MachinePublic,
+	compression string,
 ) ([]byte, error) {
 	jsonBody, err := json.Marshal(resp)
 	if err != nil {

From e2bffd4f5a63cb91a184941196e93e989201b4ce Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 14:20:24 +0200
Subject: [PATCH 52/63] Make legacy protocol use common methods for client
 registration

---
 api.go             | 433 -----------------------------
 protocol_common.go | 660 +++++++++++++++++++++++++++++++++++++++++++++
 protocol_legacy.go | 142 +---------
 3 files changed, 661 insertions(+), 574 deletions(-)

diff --git a/api.go b/api.go
index 56ca3db6de..18ac72fe7f 100644
--- a/api.go
+++ b/api.go
@@ -3,16 +3,12 @@ package headscale
 import (
 	"bytes"
 	"encoding/json"
-	"fmt"
 	"html/template"
 	"net/http"
-	"strings"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/rs/zerolog/log"
-	"tailscale.com/tailcfg"
-	"tailscale.com/types/key"
 )
 
 const (
@@ -142,432 +138,3 @@ func (h *Headscale) RegisterWebAPI(
 			Msg("Failed to write response")
 	}
 }
-
-func (h *Headscale) handleMachineLogOut(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	log.Info().
-		Str("machine", machine.Hostname).
-		Msg("Client requested logout")
-
-	err := h.ExpireMachine(&machine)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "handleMachineLogOut").
-			Err(err).
-			Msg("Failed to expire machine")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	resp.AuthURL = ""
-	resp.MachineAuthorized = false
-	resp.User = *machine.Namespace.toUser()
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot encode message")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-func (h *Headscale) handleMachineValidRegistration(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The machine registration is valid, respond with redirect to /map
-	log.Debug().
-		Str("machine", machine.Hostname).
-		Msg("Client is registered and we have the current NodeKey. All clear to /map")
-
-	resp.AuthURL = ""
-	resp.MachineAuthorized = true
-	resp.User = *machine.Namespace.toUser()
-	resp.Login = *machine.Namespace.toLogin()
-
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot encode message")
-		machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
-			Inc()
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
-		Inc()
-
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-func (h *Headscale) handleMachineExpired(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	registerRequest tailcfg.RegisterRequest,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The client has registered before, but has expired
-	log.Debug().
-		Str("machine", machine.Hostname).
-		Msg("Machine registration has expired. Sending a authurl to register")
-
-	if registerRequest.Auth.AuthKey != "" {
-		h.handleAuthKey(writer, req, machineKey, registerRequest)
-
-		return
-	}
-
-	if h.cfg.OIDC.Issuer != "" {
-		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"),
-			NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"),
-			NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	}
-
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot encode message")
-		machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
-			Inc()
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
-		Inc()
-
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-func (h *Headscale) handleMachineRefreshKey(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	registerRequest tailcfg.RegisterRequest,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	log.Debug().
-		Str("machine", machine.Hostname).
-		Msg("We have the OldNodeKey in the database. This is a key refresh")
-	machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
-
-	if err := h.db.Save(&machine).Error; err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to update machine key in the database")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	resp.AuthURL = ""
-	resp.User = *machine.Namespace.toUser()
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot encode message")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-func (h *Headscale) handleMachineRegistrationNew(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	registerRequest tailcfg.RegisterRequest,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The machine registration is new, redirect the client to the registration URL
-	log.Debug().
-		Caller().
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msg("The node seems to be new, sending auth url")
-	if h.cfg.OIDC.Issuer != "" {
-		resp.AuthURL = fmt.Sprintf(
-			"%s/oidc/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"),
-			NodePublicKeyStripPrefix(registerRequest.NodeKey),
-		)
-	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"),
-			NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	}
-
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Cannot encode message")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-}
-
-// TODO: check if any locks are needed around IP allocation.
-func (h *Headscale) handleAuthKey(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machineKey key.MachinePublic,
-	registerRequest tailcfg.RegisterRequest,
-) {
-	machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
-
-	log.Debug().
-		Str("func", "handleAuthKey").
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
-	resp := tailcfg.RegisterResponse{}
-
-	pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "handleAuthKey").
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Err(err).
-			Msg("Failed authentication via AuthKey")
-		resp.MachineAuthorized = false
-		respBody, err := encode(resp, &machineKey, h.privateKey)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "handleAuthKey").
-				Str("machine", registerRequest.Hostinfo.Hostname).
-				Err(err).
-				Msg("Cannot encode message")
-			http.Error(writer, "Internal server error", http.StatusInternalServerError)
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-				Inc()
-
-			return
-		}
-
-		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-		writer.WriteHeader(http.StatusUnauthorized)
-		_, err = writer.Write(respBody)
-		if err != nil {
-			log.Error().
-				Caller().
-				Err(err).
-				Msg("Failed to write response")
-		}
-
-		log.Error().
-			Caller().
-			Str("func", "handleAuthKey").
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Msg("Failed authentication via AuthKey")
-
-		if pak != nil {
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-				Inc()
-		} else {
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
-		}
-
-		return
-	}
-
-	log.Debug().
-		Str("func", "handleAuthKey").
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msg("Authentication key was valid, proceeding to acquire IP addresses")
-
-	nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
-
-	// retrieve 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.
-	machine, _ := h.GetMachineByMachineKey(machineKey)
-	if machine != nil {
-		log.Trace().
-			Caller().
-			Str("machine", machine.Hostname).
-			Msg("machine already registered, refreshing with new auth key")
-
-		machine.NodeKey = nodeKey
-		machine.AuthKeyID = uint(pak.ID)
-		err := h.RefreshMachine(machine, registerRequest.Expiry)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("machine", machine.Hostname).
-				Err(err).
-				Msg("Failed to refresh machine")
-
-			return
-		}
-	} else {
-		now := time.Now().UTC()
-
-		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "RegistrationHandler").
-				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
-				Err(err)
-
-			return
-		}
-
-		machineToRegister := Machine{
-			Hostname:       registerRequest.Hostinfo.Hostname,
-			GivenName:      givenName,
-			NamespaceID:    pak.Namespace.ID,
-			MachineKey:     machineKeyStr,
-			RegisterMethod: RegisterMethodAuthKey,
-			Expiry:         &registerRequest.Expiry,
-			NodeKey:        nodeKey,
-			LastSeen:       &now,
-			AuthKeyID:      uint(pak.ID),
-		}
-
-		machine, err = h.RegisterMachine(
-			machineToRegister,
-		)
-		if err != nil {
-			log.Error().
-				Caller().
-				Err(err).
-				Msg("could not register machine")
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-				Inc()
-			http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-			return
-		}
-	}
-
-	err = h.UsePreAuthKey(pak)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to use pre-auth key")
-		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-			Inc()
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	resp.MachineAuthorized = true
-	resp.User = *pak.Namespace.toUser()
-	respBody, err := encode(resp, &machineKey, h.privateKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "handleAuthKey").
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Err(err).
-			Msg("Cannot encode message")
-		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-			Inc()
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
-		Inc()
-	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-	writer.WriteHeader(http.StatusOK)
-	_, err = writer.Write(respBody)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to write response")
-	}
-
-	log.Info().
-		Str("func", "handleAuthKey").
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
-		Msg("Successfully authenticated via AuthKey")
-}
diff --git a/protocol_common.go b/protocol_common.go
index e196c3ee9a..998b202638 100644
--- a/protocol_common.go
+++ b/protocol_common.go
@@ -2,11 +2,17 @@ package headscale
 
 import (
 	"encoding/json"
+	"errors"
+	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/rs/zerolog/log"
+	"gorm.io/gorm"
 	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
 )
 
 const (
@@ -84,3 +90,657 @@ func (h *Headscale) KeyHandler(
 			Msg("Failed to write response")
 	}
 }
+
+// handleRegisterCommon is the common logic for registering a client in the legacy and Noise protocols
+//
+// When using Noise, the machineKey is Zero.
+func (h *Headscale) handleRegisterCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machineKey key.MachinePublic,
+) {
+
+	now := time.Now().UTC()
+	machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+
+		// If the machine has AuthKey set, handle registration via PreAuthKeys
+		if registerRequest.Auth.AuthKey != "" {
+			h.handleAuthKeyCommon(writer, req, registerRequest, machineKey)
+
+			return
+		}
+
+		// Check if the node is waiting for interactive login.
+		//
+		// TODO(juan): We could use this field to improve our protocol implementation,
+		// and hold the request until the client closes it, or the interactive
+		// login is completed (i.e., the user registers the machine).
+		// This is not implemented yet, as it is no strictly required. The only side-effect
+		// is that the client will hammer headscale with requests until it gets a
+		// successful RegisterResponse.
+		if registerRequest.Followup != "" {
+			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
+				log.Debug().
+					Caller().
+					Str("machine", registerRequest.Hostinfo.Hostname).
+					Str("node_key", registerRequest.NodeKey.ShortString()).
+					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+					Str("follow_up", registerRequest.Followup).
+					Bool("noise", machineKey.IsZero()).
+					Msg("Machine is waiting for interactive login")
+
+				ticker := time.NewTicker(registrationHoldoff)
+				select {
+				case <-req.Context().Done():
+					return
+				case <-ticker.C:
+					h.handleNewMachineCommon(writer, req, registerRequest, machineKey)
+
+					return
+				}
+			}
+		}
+
+		log.Info().
+			Caller().
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Str("node_key", registerRequest.NodeKey.ShortString()).
+			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
+			Str("follow_up", registerRequest.Followup).
+			Bool("noise", machineKey.IsZero()).
+			Msg("New machine not yet in the database")
+
+		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "RegistrationHandler").
+				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
+				Err(err)
+
+			return
+		}
+
+		// The machine did not have a key to authenticate, which means
+		// that we rely on a method that calls back some how (OpenID or CLI)
+		// We create the machine and then keep it around until a callback
+		// happens
+		newMachine := Machine{
+			MachineKey: MachinePublicKeyStripPrefix(machineKey),
+			Hostname:   registerRequest.Hostinfo.Hostname,
+			GivenName:  givenName,
+			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
+			LastSeen:   &now,
+			Expiry:     &time.Time{},
+		}
+
+		if !registerRequest.Expiry.IsZero() {
+			log.Trace().
+				Caller().
+				Bool("noise", machineKey.IsZero()).
+				Str("machine", registerRequest.Hostinfo.Hostname).
+				Time("expiry", registerRequest.Expiry).
+				Msg("Non-zero expiry time requested")
+			newMachine.Expiry = &registerRequest.Expiry
+		}
+
+		h.registrationCache.Set(
+			newMachine.NodeKey,
+			newMachine,
+			registerCacheExpiration,
+		)
+
+		h.handleNewMachineCommon(writer, req, registerRequest, machineKey)
+
+		return
+	}
+
+	// The machine is already registered, so we need to pass through reauth or key update.
+	if machine != nil {
+		// If the NodeKey stored in headscale is the same as the key presented in a registration
+		// request, then we have a node that is either:
+		// - Trying to log out (sending a expiry in the past)
+		// - A valid, registered machine, looking for the node map
+		// - Expired machine wanting to reauthenticate
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
+			// 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 !registerRequest.Expiry.IsZero() &&
+				registerRequest.Expiry.UTC().Before(now) {
+				h.handleMachineLogOutCommon(writer, req, *machine, machineKey)
+
+				return
+			}
+
+			// If machine is not expired, and is register, we have a already accepted this machine,
+			// let it proceed with a valid registration
+			if !machine.isExpired() {
+				h.handleMachineValidRegistrationCommon(writer, req, *machine, machineKey)
+
+				return
+			}
+		}
+
+		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
+		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
+			!machine.isExpired() {
+			h.handleMachineRefreshKeyCommon(
+				writer,
+				req,
+				registerRequest,
+				*machine,
+				machineKey,
+			)
+
+			return
+		}
+
+		// The machine has expired
+		h.handleMachineExpiredCommon(writer, req, registerRequest, *machine, machineKey)
+
+		return
+	}
+
+}
+
+// handleAuthKeyCommon contains the logic to manage auth key client registration
+// It is used both by the legacy and the new Noise protocol.
+// When using Noise, the machineKey is Zero.
+//
+// TODO: check if any locks are needed around IP allocation.
+func (h *Headscale) handleAuthKeyCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machineKey key.MachinePublic,
+) {
+	log.Debug().
+		Str("func", "handleAuthKeyCommon").
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Bool("noise", machineKey.IsZero()).
+		Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
+	resp := tailcfg.RegisterResponse{}
+
+	pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Str("func", "handleAuthKeyCommon").
+			Bool("noise", machineKey.IsZero()).
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Err(err).
+			Msg("Failed authentication via AuthKey")
+		resp.MachineAuthorized = false
+
+		respBody, err := h.marshalResponse(resp, machineKey)
+		if err != nil {
+			log.Error().
+				Caller().
+				Str("func", "handleAuthKeyCommon").
+				Bool("noise", machineKey.IsZero()).
+				Str("machine", registerRequest.Hostinfo.Hostname).
+				Err(err).
+				Msg("Cannot encode message")
+			http.Error(writer, "Internal server error", http.StatusInternalServerError)
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+				Inc()
+
+			return
+		}
+
+		writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+		writer.WriteHeader(http.StatusUnauthorized)
+		_, err = writer.Write(respBody)
+		if err != nil {
+			log.Error().
+				Caller().
+				Bool("noise", machineKey.IsZero()).
+				Err(err).
+				Msg("Failed to write response")
+		}
+
+		log.Error().
+			Caller().
+			Str("func", "handleAuthKeyCommon").
+			Bool("noise", machineKey.IsZero()).
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Msg("Failed authentication via AuthKey")
+
+		if pak != nil {
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+				Inc()
+		} else {
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
+		}
+
+		return
+	}
+
+	log.Debug().
+		Str("func", "handleAuthKeyCommon").
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Msg("Authentication key was valid, proceeding to acquire IP addresses")
+
+	nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
+
+	// retrieve 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.
+	machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
+	if machine != nil {
+		log.Trace().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Str("machine", machine.Hostname).
+			Msg("machine was already registered before, refreshing with new auth key")
+
+		machine.NodeKey = nodeKey
+		machine.AuthKeyID = uint(pak.ID)
+		err := h.RefreshMachine(machine, registerRequest.Expiry)
+		if err != nil {
+			log.Error().
+				Caller().
+				Bool("noise", machineKey.IsZero()).
+				Str("machine", machine.Hostname).
+				Err(err).
+				Msg("Failed to refresh machine")
+
+			return
+		}
+	} else {
+		now := time.Now().UTC()
+
+		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
+		if err != nil {
+			log.Error().
+				Caller().
+				Bool("noise", machineKey.IsZero()).
+				Str("func", "RegistrationHandler").
+				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
+				Err(err)
+
+			return
+		}
+
+		machineToRegister := Machine{
+			Hostname:       registerRequest.Hostinfo.Hostname,
+			GivenName:      givenName,
+			NamespaceID:    pak.Namespace.ID,
+			MachineKey:     MachinePublicKeyStripPrefix(machineKey),
+			RegisterMethod: RegisterMethodAuthKey,
+			Expiry:         &registerRequest.Expiry,
+			NodeKey:        nodeKey,
+			LastSeen:       &now,
+			AuthKeyID:      uint(pak.ID),
+		}
+
+		machine, err = h.RegisterMachine(
+			machineToRegister,
+		)
+		if err != nil {
+			log.Error().
+				Caller().
+				Bool("noise", machineKey.IsZero()).
+				Err(err).
+				Msg("could not register machine")
+			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+				Inc()
+			http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+			return
+		}
+	}
+
+	err = h.UsePreAuthKey(pak)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Failed to use pre-auth key")
+		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+			Inc()
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	resp.MachineAuthorized = true
+	resp.User = *pak.Namespace.toUser()
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Str("func", "handleAuthKeyCommon").
+			Str("machine", registerRequest.Hostinfo.Hostname).
+			Err(err).
+			Msg("Cannot encode message")
+		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
+			Inc()
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
+		Inc()
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Str("func", "handleAuthKeyCommon").
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
+		Msg("Successfully authenticated via AuthKey")
+}
+
+// handleNewMachineCommon exposes for both legacy and Noise the functionality to get a URL
+// for authorizing the machine. This url is then showed to the user by the local Tailscale client.
+func (h *Headscale) handleNewMachineCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machineKey key.MachinePublic,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The machine registration is new, redirect the client to the registration URL
+	log.Debug().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Msg("The node seems to be new, sending auth url")
+	if h.cfg.OIDC.Issuer != "" {
+		resp.AuthURL = fmt.Sprintf(
+			"%s/oidc/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"),
+			NodePublicKeyStripPrefix(registerRequest.NodeKey),
+		)
+	} else {
+		resp.AuthURL = fmt.Sprintf("%s/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"),
+			NodePublicKeyStripPrefix(registerRequest.NodeKey))
+	}
+
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Cannot encode message")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Bool("noise", machineKey.IsZero()).
+			Caller().
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", registerRequest.Hostinfo.Hostname).
+		Msg("Successfully sent auth url")
+}
+
+func (h *Headscale) handleMachineLogOutCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	machine Machine,
+	machineKey key.MachinePublic,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	log.Info().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Client requested logout")
+
+	err := h.ExpireMachine(&machine)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Str("func", "handleMachineLogOutCommon").
+			Err(err).
+			Msg("Failed to expire machine")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	resp.AuthURL = ""
+	resp.MachineAuthorized = false
+	resp.User = *machine.Namespace.toUser()
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Cannot encode message")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Bool("noise", machineKey.IsZero()).
+			Caller().
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Successfully logged out")
+}
+
+func (h *Headscale) handleMachineValidRegistrationCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	machine Machine,
+	machineKey key.MachinePublic,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The machine registration is valid, respond with redirect to /map
+	log.Debug().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Client is registered and we have the current NodeKey. All clear to /map")
+
+	resp.AuthURL = ""
+	resp.MachineAuthorized = true
+	resp.User = *machine.Namespace.toUser()
+	resp.Login = *machine.Namespace.toLogin()
+
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Cannot encode message")
+		machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
+			Inc()
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
+		Inc()
+
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Machine successfully authorized")
+}
+
+func (h *Headscale) handleMachineRefreshKeyCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machine Machine,
+	machineKey key.MachinePublic,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	log.Debug().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("We have the OldNodeKey in the database. This is a key refresh")
+	machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
+
+	if err := h.db.Save(&machine).Error; err != nil {
+		log.Error().
+			Caller().
+			Err(err).
+			Msg("Failed to update machine key in the database")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	resp.AuthURL = ""
+	resp.User = *machine.Namespace.toUser()
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Cannot encode message")
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("node_key", registerRequest.NodeKey.ShortString()).
+		Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
+		Str("machine", machine.Hostname).
+		Msg("Machine successfully refreshed")
+
+}
+
+func (h *Headscale) handleMachineExpiredCommon(
+	writer http.ResponseWriter,
+	req *http.Request,
+	registerRequest tailcfg.RegisterRequest,
+	machine Machine,
+	machineKey key.MachinePublic,
+) {
+	resp := tailcfg.RegisterResponse{}
+
+	// The client has registered before, but has expired
+	log.Debug().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Machine registration has expired. Sending a authurl to register")
+
+	if registerRequest.Auth.AuthKey != "" {
+		h.handleAuthKeyCommon(writer, req, registerRequest, machineKey)
+
+		return
+	}
+
+	if h.cfg.OIDC.Issuer != "" {
+		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"),
+			NodePublicKeyStripPrefix(registerRequest.NodeKey))
+	} else {
+		resp.AuthURL = fmt.Sprintf("%s/register/%s",
+			strings.TrimSuffix(h.cfg.ServerURL, "/"),
+			NodePublicKeyStripPrefix(registerRequest.NodeKey))
+	}
+
+	respBody, err := h.marshalResponse(resp, machineKey)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Cannot encode message")
+		machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
+			Inc()
+		http.Error(writer, "Internal server error", http.StatusInternalServerError)
+
+		return
+	}
+	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
+		Inc()
+
+	writer.Header().Set("Content-Type", "application/json; charset=utf-8")
+	writer.WriteHeader(http.StatusOK)
+	_, err = writer.Write(respBody)
+	if err != nil {
+		log.Error().
+			Caller().
+			Bool("noise", machineKey.IsZero()).
+			Err(err).
+			Msg("Failed to write response")
+	}
+
+	log.Info().
+		Caller().
+		Bool("noise", machineKey.IsZero()).
+		Str("machine", machine.Hostname).
+		Msg("Auth URL for reauthenticate successfully sent")
+}
diff --git a/protocol_legacy.go b/protocol_legacy.go
index 5c23b172a5..b943dc9f22 100644
--- a/protocol_legacy.go
+++ b/protocol_legacy.go
@@ -1,14 +1,11 @@
 package headscale
 
 import (
-	"errors"
 	"io"
 	"net/http"
-	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/rs/zerolog/log"
-	"gorm.io/gorm"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
 )
@@ -57,143 +54,6 @@ func (h *Headscale) RegistrationHandler(
 		return
 	}
 
-	now := time.Now().UTC()
-	machine, err := h.GetMachineByMachineKey(machineKey)
-	if errors.Is(err, gorm.ErrRecordNotFound) {
-		machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
+	h.handleRegisterCommon(writer, req, registerRequest, machineKey)
 
-		// If the machine has AuthKey set, handle registration via PreAuthKeys
-		if registerRequest.Auth.AuthKey != "" {
-			h.handleAuthKey(writer, req, machineKey, registerRequest)
-
-			return
-		}
-
-		// Check if the node is waiting for interactive login.
-		//
-		// TODO(juan): We could use this field to improve our protocol implementation,
-		// and hold the request until the client closes it, or the interactive
-		// login is completed (i.e., the user registers the machine).
-		// This is not implemented yet, as it is no strictly required. The only side-effect
-		// is that the client will hammer headscale with requests until it gets a
-		// successful RegisterResponse.
-		if registerRequest.Followup != "" {
-			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
-				log.Debug().
-					Caller().
-					Str("machine", registerRequest.Hostinfo.Hostname).
-					Str("node_key", registerRequest.NodeKey.ShortString()).
-					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-					Str("follow_up", registerRequest.Followup).
-					Msg("Machine is waiting for interactive login")
-
-				ticker := time.NewTicker(registrationHoldoff)
-				select {
-				case <-req.Context().Done():
-					return
-				case <-ticker.C:
-					h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
-
-					return
-				}
-			}
-		}
-
-		log.Info().
-			Caller().
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Str("node_key", registerRequest.NodeKey.ShortString()).
-			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-			Str("follow_up", registerRequest.Followup).
-			Msg("New machine not yet in the database")
-
-		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "RegistrationHandler").
-				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
-				Err(err)
-
-			return
-		}
-
-		// The machine did not have a key to authenticate, which means
-		// that we rely on a method that calls back some how (OpenID or CLI)
-		// We create the machine and then keep it around until a callback
-		// happens
-		newMachine := Machine{
-			MachineKey: machineKeyStr,
-			Hostname:   registerRequest.Hostinfo.Hostname,
-			GivenName:  givenName,
-			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
-			LastSeen:   &now,
-			Expiry:     &time.Time{},
-		}
-
-		if !registerRequest.Expiry.IsZero() {
-			log.Trace().
-				Caller().
-				Str("machine", registerRequest.Hostinfo.Hostname).
-				Time("expiry", registerRequest.Expiry).
-				Msg("Non-zero expiry time requested")
-			newMachine.Expiry = &registerRequest.Expiry
-		}
-
-		h.registrationCache.Set(
-			newMachine.NodeKey,
-			newMachine,
-			registerCacheExpiration,
-		)
-
-		h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest)
-
-		return
-	}
-
-	// The machine is already registered, so we need to pass through reauth or key update.
-	if machine != nil {
-		// If the NodeKey stored in headscale is the same as the key presented in a registration
-		// request, then we have a node that is either:
-		// - Trying to log out (sending a expiry in the past)
-		// - A valid, registered machine, looking for the node map
-		// - Expired machine wanting to reauthenticate
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
-			// 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 !registerRequest.Expiry.IsZero() &&
-				registerRequest.Expiry.UTC().Before(now) {
-				h.handleMachineLogOut(writer, req, machineKey, *machine)
-
-				return
-			}
-
-			// If machine is not expired, and is register, we have a already accepted this machine,
-			// let it proceed with a valid registration
-			if !machine.isExpired() {
-				h.handleMachineValidRegistration(writer, req, machineKey, *machine)
-
-				return
-			}
-		}
-
-		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
-			!machine.isExpired() {
-			h.handleMachineRefreshKey(
-				writer,
-				req,
-				machineKey,
-				registerRequest,
-				*machine,
-			)
-
-			return
-		}
-
-		// The machine has expired
-		h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine)
-
-		return
-	}
 }

From 43ad0d4416dcfbc0499829379ebf0897514a7d1b Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 14:24:43 +0200
Subject: [PATCH 53/63] Removed unused method

---
 utils.go | 13 -------------
 1 file changed, 13 deletions(-)

diff --git a/utils.go b/utils.go
index 7727a24569..e7fb13aa42 100644
--- a/utils.go
+++ b/utils.go
@@ -135,19 +135,6 @@ func decode(
 	return nil
 }
 
-func encode(
-	v interface{},
-	pubKey *key.MachinePublic,
-	privKey *key.MachinePrivate,
-) ([]byte, error) {
-	b, err := json.Marshal(v)
-	if err != nil {
-		return nil, err
-	}
-
-	return privKey.SealTo(*pubKey, b), nil
-}
-
 func (h *Headscale) getAvailableIPs() (MachineAddresses, error) {
 	var ips MachineAddresses
 	var err error

From b6e3cd81c6dc318c8b1bfef05d386d98a2f73638 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 14:27:40 +0200
Subject: [PATCH 54/63] Fixed minor linting things

---
 protocol_common.go | 4 ----
 protocol_legacy.go | 1 -
 2 files changed, 5 deletions(-)

diff --git a/protocol_common.go b/protocol_common.go
index 998b202638..49c9138a86 100644
--- a/protocol_common.go
+++ b/protocol_common.go
@@ -100,11 +100,9 @@ func (h *Headscale) handleRegisterCommon(
 	registerRequest tailcfg.RegisterRequest,
 	machineKey key.MachinePublic,
 ) {
-
 	now := time.Now().UTC()
 	machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
 	if errors.Is(err, gorm.ErrRecordNotFound) {
-
 		// If the machine has AuthKey set, handle registration via PreAuthKeys
 		if registerRequest.Auth.AuthKey != "" {
 			h.handleAuthKeyCommon(writer, req, registerRequest, machineKey)
@@ -242,7 +240,6 @@ func (h *Headscale) handleRegisterCommon(
 
 		return
 	}
-
 }
 
 // handleAuthKeyCommon contains the logic to manage auth key client registration
@@ -676,7 +673,6 @@ func (h *Headscale) handleMachineRefreshKeyCommon(
 		Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
 		Str("machine", machine.Hostname).
 		Msg("Machine successfully refreshed")
-
 }
 
 func (h *Headscale) handleMachineExpiredCommon(
diff --git a/protocol_legacy.go b/protocol_legacy.go
index b943dc9f22..4e75d12730 100644
--- a/protocol_legacy.go
+++ b/protocol_legacy.go
@@ -55,5 +55,4 @@ func (h *Headscale) RegistrationHandler(
 	}
 
 	h.handleRegisterCommon(writer, req, registerRequest, machineKey)
-
 }

From c894db3dd41c97c5fb989973d0351010c98fb31a Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 16:29:04 +0200
Subject: [PATCH 55/63] Use common core for noise registration

---
 protocol_noise.go | 466 +---------------------------------------------
 1 file changed, 2 insertions(+), 464 deletions(-)

diff --git a/protocol_noise.go b/protocol_noise.go
index 98a66d226c..46f7a03baa 100644
--- a/protocol_noise.go
+++ b/protocol_noise.go
@@ -2,16 +2,12 @@ package headscale
 
 import (
 	"encoding/json"
-	"errors"
-	"fmt"
 	"io"
 	"net/http"
-	"strings"
-	"time"
 
 	"github.com/rs/zerolog/log"
-	"gorm.io/gorm"
 	"tailscale.com/tailcfg"
+	"tailscale.com/types/key"
 )
 
 // // NoiseRegistrationHandler handles the actual registration process of a machine.
@@ -38,463 +34,5 @@ func (h *Headscale) NoiseRegistrationHandler(
 		return
 	}
 
-	log.Trace().Caller().
-		Str("node_key", registerRequest.NodeKey.ShortString()).
-		Str("old_node_key", registerRequest.OldNodeKey.ShortString()).
-		Msg("New node is registering")
-
-	now := time.Now().UTC()
-	machine, err := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
-	if errors.Is(err, gorm.ErrRecordNotFound) {
-		// If the machine has AuthKey set, handle registration via PreAuthKeys
-		if registerRequest.Auth.AuthKey != "" {
-			h.handleNoiseAuthKey(writer, req, registerRequest)
-
-			return
-		}
-
-		// Check if the node is waiting for interactive login.
-		//
-		// TODO(juan): We could use this field to improve our protocol implementation,
-		// and hold the request until the client closes it, or the interactive
-		// login is completed (i.e., the user registers the machine).
-		// This is not implemented yet, as it is no strictly required. The only side-effect
-		// is that the client will hammer headscale with requests until it gets a
-		// successful RegisterResponse.
-		if registerRequest.Followup != "" {
-			if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok {
-				log.Debug().
-					Caller().
-					Str("machine", registerRequest.Hostinfo.Hostname).
-					Str("node_key", registerRequest.NodeKey.ShortString()).
-					Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-					Str("follow_up", registerRequest.Followup).
-					Msg("Machine is waiting for interactive login")
-
-				ticker := time.NewTicker(registrationHoldoff)
-				select {
-				case <-req.Context().Done():
-					return
-				case <-ticker.C:
-					h.handleNoiseMachineRegistrationNew(writer, req, registerRequest)
-
-					return
-				}
-			}
-		}
-
-		log.Info().
-			Caller().
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Str("node_key", registerRequest.NodeKey.ShortString()).
-			Str("node_key_old", registerRequest.OldNodeKey.ShortString()).
-			Str("follow_up", registerRequest.Followup).
-			Msg("New Noise machine not yet in the database")
-
-		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "RegistrationHandler").
-				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
-				Err(err)
-
-			return
-		}
-
-		// The machine did not have a key to authenticate, which means
-		// that we rely on a method that calls back some how (OpenID or CLI)
-		// We create the machine and then keep it around until a callback
-		// happens
-		newMachine := Machine{
-			MachineKey: "",
-			Hostname:   registerRequest.Hostinfo.Hostname,
-			GivenName:  givenName,
-			NodeKey:    NodePublicKeyStripPrefix(registerRequest.NodeKey),
-			LastSeen:   &now,
-			Expiry:     &time.Time{},
-		}
-
-		if !registerRequest.Expiry.IsZero() {
-			log.Trace().
-				Caller().
-				Str("machine", registerRequest.Hostinfo.Hostname).
-				Time("expiry", registerRequest.Expiry).
-				Msg("Non-zero expiry time requested")
-			newMachine.Expiry = &registerRequest.Expiry
-		}
-
-		h.registrationCache.Set(
-			NodePublicKeyStripPrefix(registerRequest.NodeKey),
-			newMachine,
-			registerCacheExpiration,
-		)
-
-		h.handleNoiseMachineRegistrationNew(writer, req, registerRequest)
-
-		return
-	}
-
-	// The machine is already registered, so we need to pass through reauth or key update.
-	if machine != nil {
-		// If the NodeKey stored in headscale is the same as the key presented in a registration
-		// request, then we have a node that is either:
-		// - Trying to log out (sending a expiry in the past)
-		// - A valid, registered machine, looking for the node map
-		// - Expired machine wanting to reauthenticate
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.NodeKey) {
-			// 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 !registerRequest.Expiry.IsZero() && registerRequest.Expiry.UTC().Before(now) {
-				h.handleNoiseNodeLogOut(writer, req, *machine)
-
-				return
-			}
-
-			// If machine is not expired, and is register, we have a already accepted this machine,
-			// let it proceed with a valid registration
-			if !machine.isExpired() {
-				h.handleNoiseNodeValidRegistration(writer, req, *machine)
-
-				return
-			}
-		}
-
-		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
-		if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) &&
-			!machine.isExpired() {
-			h.handleNoiseNodeRefreshKey(writer, req, registerRequest, *machine)
-
-			return
-		}
-
-		// The node has expired
-		h.handleNoiseNodeExpired(writer, req, registerRequest, *machine)
-
-		return
-	}
-}
-
-func (h *Headscale) handleNoiseAuthKey(
-	writer http.ResponseWriter,
-	req *http.Request,
-	registerRequest tailcfg.RegisterRequest,
-) {
-	log.Debug().
-		Caller().
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msgf("Processing auth key for %s over Noise", registerRequest.Hostinfo.Hostname)
-	resp := tailcfg.RegisterResponse{}
-
-	pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Err(err).
-			Msg("Failed authentication via AuthKey")
-		resp.MachineAuthorized = false
-
-		writer.Header().Set("Content-Type", "application/json")
-		writer.WriteHeader(http.StatusUnauthorized)
-		err = json.NewEncoder(writer).Encode(resp)
-		if err != nil {
-			log.Error().
-				Caller().
-				Err(err).
-				Msg("Failed to encode response")
-		}
-
-		log.Error().
-			Caller().
-			Str("machine", registerRequest.Hostinfo.Hostname).
-			Msg("Failed authentication via AuthKey over Noise")
-
-		if pak != nil {
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-				Inc()
-		} else {
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
-		}
-
-		return
-	}
-
-	log.Debug().
-		Caller().
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msg("Authentication key was valid, proceeding to acquire IP addresses")
-
-	nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
-
-	// retrieve 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.
-	machine, _ := h.GetMachineByAnyNodeKey(registerRequest.NodeKey, registerRequest.OldNodeKey)
-	if machine != nil {
-		log.Trace().
-			Caller().
-			Str("machine", machine.Hostname).
-			Msg("machine already registered, refreshing with new auth key")
-
-		machine.NodeKey = nodeKey
-		machine.AuthKeyID = uint(pak.ID)
-		err = h.RefreshMachine(machine, registerRequest.Expiry)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("machine", machine.Hostname).
-				Err(err).
-				Msg("Failed to refresh machine")
-
-			return
-		}
-	} else {
-		now := time.Now().UTC()
-
-		givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname)
-		if err != nil {
-			log.Error().
-				Caller().
-				Str("func", "RegistrationHandler").
-				Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
-				Err(err)
-
-			return
-		}
-
-		machineToRegister := Machine{
-			Hostname:       registerRequest.Hostinfo.Hostname,
-			GivenName:      givenName,
-			NamespaceID:    pak.Namespace.ID,
-			MachineKey:     "",
-			RegisterMethod: RegisterMethodAuthKey,
-			Expiry:         &registerRequest.Expiry,
-			NodeKey:        nodeKey,
-			LastSeen:       &now,
-			AuthKeyID:      uint(pak.ID),
-		}
-
-		machine, err = h.RegisterMachine(
-			machineToRegister,
-		)
-		if err != nil {
-			log.Error().
-				Caller().
-				Err(err).
-				Msg("could not register machine")
-			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-				Inc()
-			http.Error(writer, "Internal error", http.StatusInternalServerError)
-
-			return
-		}
-	}
-
-	err = h.UsePreAuthKey(pak)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to use pre-auth key")
-		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
-			Inc()
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	resp.MachineAuthorized = true
-	resp.User = *pak.Namespace.toUser()
-
-	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
-		Inc()
-
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err = json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to encode response")
-
-		return
-	}
-
-	log.Info().
-		Caller().
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
-		Msg("Successfully authenticated via AuthKey on Noise")
-}
-
-func (h *Headscale) handleNoiseNodeValidRegistration(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The machine registration is valid, respond with redirect to /map
-	log.Debug().
-		Str("machine", machine.Hostname).
-		Msg("Client is registered and we have the current NodeKey. All clear to /map")
-
-	resp.AuthURL = ""
-	resp.MachineAuthorized = true
-	resp.User = *machine.Namespace.toUser()
-	resp.Login = *machine.Namespace.toLogin()
-
-	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
-		Inc()
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err := json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to encode response")
-	}
-}
-
-func (h *Headscale) handleNoiseMachineRegistrationNew(
-	writer http.ResponseWriter,
-	req *http.Request,
-	registerRequest tailcfg.RegisterRequest,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The machine registration is new, redirect the client to the registration URL
-	log.Debug().
-		Caller().
-		Str("machine", registerRequest.Hostinfo.Hostname).
-		Msg("The node seems to be new, sending auth url")
-	if h.cfg.OIDC.Issuer != "" {
-		resp.AuthURL = fmt.Sprintf(
-			"%s/oidc/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"),
-			NodePublicKeyStripPrefix(registerRequest.NodeKey),
-		)
-	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	}
-
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err := json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to encode response")
-	}
-}
-
-func (h *Headscale) handleNoiseNodeLogOut(
-	writer http.ResponseWriter,
-	req *http.Request,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	log.Info().
-		Str("machine", machine.Hostname).
-		Msg("Client requested logout")
-
-	err := h.ExpireMachine(&machine)
-	if err != nil {
-		log.Error().
-			Caller().
-			Str("func", "handleMachineLogOut").
-			Err(err).
-			Msg("Failed to expire machine")
-		http.Error(writer, "Internal server error", http.StatusInternalServerError)
-
-		return
-	}
-
-	resp.AuthURL = ""
-	resp.MachineAuthorized = false
-	resp.User = *machine.Namespace.toUser()
-
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err = json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("could not encode response")
-	}
-}
-
-func (h *Headscale) handleNoiseNodeRefreshKey(
-	writer http.ResponseWriter,
-	req *http.Request,
-	registerRequest tailcfg.RegisterRequest,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	log.Debug().
-		Str("machine", machine.Hostname).
-		Msg("We have the OldNodeKey in the database. This is a key refresh")
-	machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
-	h.db.Save(&machine)
-
-	resp.AuthURL = ""
-	resp.User = *machine.Namespace.toUser()
-
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err := json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().
-			Caller().
-			Err(err).
-			Msg("Failed to encode response")
-	}
-}
-
-func (h *Headscale) handleNoiseNodeExpired(
-	writer http.ResponseWriter,
-	req *http.Request,
-	registerRequest tailcfg.RegisterRequest,
-	machine Machine,
-) {
-	resp := tailcfg.RegisterResponse{}
-
-	// The client has registered before, but has expired
-	log.Debug().
-		Caller().
-		Str("machine", machine.Hostname).
-		Msg("Machine registration has expired. Sending a authurl to register")
-
-	if registerRequest.Auth.AuthKey != "" {
-		h.handleNoiseAuthKey(writer, req, registerRequest)
-
-		return
-	}
-
-	if h.cfg.OIDC.Issuer != "" {
-		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	} else {
-		resp.AuthURL = fmt.Sprintf("%s/register/%s",
-			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
-	}
-
-	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
-		Inc()
-
-	writer.Header().Set("Content-Type", "application/json")
-	writer.WriteHeader(http.StatusOK)
-	err := json.NewEncoder(writer).Encode(resp)
-	if err != nil {
-		log.Error().Caller().Err(err).Msg("Failed to encode response")
-	}
+	h.handleRegisterCommon(writer, req, registerRequest, key.MachinePublic{})
 }

From 2f554133c5d849d2a4ca92cd8020dd1dd3a589b7 Mon Sep 17 00:00:00 2001
From: Juan Font <juanfontalonso@gmail.com>
Date: Fri, 19 Aug 2022 23:49:06 +0200
Subject: [PATCH 56/63] Move comment up

Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
---
 machine.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/machine.go b/machine.go
index 89f0c7e77b..960683ebf0 100644
--- a/machine.go
+++ b/machine.go
@@ -600,8 +600,8 @@ func (machine Machine) toNode(
 	}
 
 	var machineKey key.MachinePublic
+	// MachineKey is only used in the legacy protocol
 	if machine.MachineKey != "" {
-		// MachineKey is only used in the legacy protocol
 		err = machineKey.UnmarshalText(
 			[]byte(MachinePublicKeyEnsurePrefix(machine.MachineKey)),
 		)

From e9906b522fcab56e826a1234b0e7d3c24e21c0cc Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 20 Aug 2022 00:06:26 +0200
Subject: [PATCH 57/63] Use upstream AcceptHTTP for the Noise upgrade

---
 noise.go | 102 ++-----------------------------------------------------
 1 file changed, 2 insertions(+), 100 deletions(-)

diff --git a/noise.go b/noise.go
index cf18fbfbc1..c8e6674de2 100644
--- a/noise.go
+++ b/noise.go
@@ -1,34 +1,18 @@
 package headscale
 
 import (
-	"encoding/base64"
 	"net/http"
 
 	"github.com/rs/zerolog/log"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2/h2c"
-	"tailscale.com/control/controlbase"
+	"tailscale.com/control/controlhttp"
 	"tailscale.com/net/netutil"
 )
 
-const (
-	errWrongConnectionUpgrade = Error("wrong connection upgrade")
-	errCannotHijack           = Error("cannot hijack connection")
-	errNoiseHandshakeFailed   = Error("noise handshake failed")
-)
-
 const (
 	// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
 	ts2021UpgradePath = "/ts2021"
-
-	// upgradeHeader is the value of the Upgrade HTTP header used to
-	// indicate the Tailscale control protocol.
-	upgradeHeaderValue = "tailscale-control-protocol"
-
-	// handshakeHeaderName is the HTTP request header that can
-	// optionally contain base64-encoded initial handshake
-	// payload, to save an RTT.
-	handshakeHeaderName = "X-Tailscale-Handshake"
 )
 
 // NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
@@ -39,14 +23,7 @@ func (h *Headscale) NoiseUpgradeHandler(
 ) {
 	log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr)
 
-	// Under normal circumstances, we should be able to use the controlhttp.AcceptHTTP()
-	// function to do this - kindly left there by the Tailscale authors for us to use.
-	// (https://github.com/tailscale/tailscale/blob/main/control/controlhttp/server.go)
-	//
-	// When we used to use Gin, we had troubles here as Gin seems to do some
-	// fun stuff, and not flusing the writer properly.
-	// So have getNoiseConnection() that is essentially an AcceptHTTP, but in our side.
-	noiseConn, err := h.getNoiseConnection(writer, req)
+	noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey)
 	if err != nil {
 		log.Error().Err(err).Msg("noise upgrade failed")
 		http.Error(writer, err.Error(), http.StatusInternalServerError)
@@ -61,78 +38,3 @@ func (h *Headscale) NoiseUpgradeHandler(
 		log.Info().Err(err).Msg("The HTTP2 server was closed")
 	}
 }
-
-// getNoiseConnection is basically AcceptHTTP from tailscale
-// TODO(juan): Figure out why we need to do this at all.
-func (h *Headscale) getNoiseConnection(
-	writer http.ResponseWriter,
-	req *http.Request,
-) (*controlbase.Conn, error) {
-	next := req.Header.Get("Upgrade")
-	if next == "" {
-		http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest)
-
-		return nil, errWrongConnectionUpgrade
-	}
-	if next != upgradeHeaderValue {
-		http.Error(writer, errWrongConnectionUpgrade.Error(), http.StatusBadRequest)
-
-		return nil, errWrongConnectionUpgrade
-	}
-
-	initB64 := req.Header.Get(handshakeHeaderName)
-	if initB64 == "" {
-		log.Warn().
-			Caller().
-			Msg("no handshake header")
-		http.Error(writer, "missing Tailscale handshake header", http.StatusBadRequest)
-
-		return nil, errWrongConnectionUpgrade
-	}
-
-	init, err := base64.StdEncoding.DecodeString(initB64)
-	if err != nil {
-		log.Warn().Err(err).Msg("invalid handshake header")
-		http.Error(writer, "invalid tailscale handshake header", http.StatusBadRequest)
-
-		return nil, errWrongConnectionUpgrade
-	}
-
-	hijacker, ok := writer.(http.Hijacker)
-	if !ok {
-		log.Error().Caller().Err(err).Msgf("Hijack failed")
-		http.Error(writer, errCannotHijack.Error(), http.StatusInternalServerError)
-
-		return nil, errCannotHijack
-	}
-
-	// This is what changes from the original AcceptHTTP() function.
-	writer.Header().Set("Upgrade", upgradeHeaderValue)
-	writer.Header().Set("Connection", "upgrade")
-	writer.WriteHeader(http.StatusSwitchingProtocols)
-	// end
-
-	netConn, conn, err := hijacker.Hijack()
-	if err != nil {
-		log.Error().Caller().Err(err).Msgf("Hijack failed")
-		http.Error(writer, "HTTP does not support general TCP support", http.StatusInternalServerError)
-
-		return nil, errCannotHijack
-	}
-	if err := conn.Flush(); err != nil {
-		netConn.Close()
-
-		return nil, errCannotHijack
-	}
-
-	netConn = netutil.NewDrainBufConn(netConn, conn.Reader)
-
-	noiseConn, err := controlbase.Server(req.Context(), netConn, *h.noisePrivateKey, init)
-	if err != nil {
-		netConn.Close()
-
-		return nil, errNoiseHandshakeFailed
-	}
-
-	return noiseConn, nil
-}

From 04e4fa785b70b7cecbc595e6004c7fc19fbabb91 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 20 Aug 2022 00:11:07 +0200
Subject: [PATCH 58/63] Updated dependencies

---
 go.mod |  4 +++-
 go.sum | 42 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index c01a9ff72b..c934a46164 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
 	github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
 	github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
+	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
 	google.golang.org/genproto v0.0.0-20220808204814-fd01256a5276
@@ -63,6 +64,7 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/glebarez/go-sqlite v1.17.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/google/go-github v17.0.0+incompatible // indirect
@@ -126,7 +128,6 @@ require (
 	go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
 	go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
 	go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
-	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
 	golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d // indirect
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
 	golang.org/x/text v0.3.7 // indirect
@@ -139,4 +140,5 @@ require (
 	modernc.org/mathutil v1.4.1 // indirect
 	modernc.org/memory v1.1.1 // indirect
 	modernc.org/sqlite v1.17.3 // indirect
+	nhooyr.io/websocket v1.8.7 // indirect
 )
diff --git a/go.sum b/go.sum
index 7b245ca861..6870e458a2 100644
--- a/go.sum
+++ b/go.sum
@@ -56,6 +56,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o=
 github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
 github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
 github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo=
@@ -64,6 +65,7 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
 github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
@@ -236,6 +238,10 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV
 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.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/glebarez/go-sqlite v1.17.3 h1:Rji9ROVSTTfjuWD6j5B+8DtkNvPILoUC3xRhkQzGxvk=
 github.com/glebarez/go-sqlite v1.17.3/go.mod h1:Hg+PQuhUy98XCxWEJEaWob8x7lhJzhNYF1nZbUiRGIY=
 github.com/glebarez/sqlite v1.4.6 h1:D5uxD2f6UJ82cHnVtO2TZ9pqsLyto3fpDKHIk2OsR8A=
@@ -254,6 +260,13 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
 github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 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/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.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 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=
@@ -273,6 +286,12 @@ github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2
 github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
 github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -291,6 +310,8 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -398,6 +419,7 @@ github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b0
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
 github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
@@ -538,8 +560,10 @@ 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=
@@ -556,6 +580,7 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
@@ -585,6 +610,8 @@ 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 v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -675,9 +702,11 @@ github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdx
 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=
@@ -918,7 +947,11 @@ 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 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+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=
@@ -1023,7 +1056,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1050,6 +1085,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1374,6 +1410,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1582,6 +1619,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
+honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83 h1:lZ9GIYaU+o5+X6ST702I/Ntyq9Y2oIMZ42rBQpem64A=
+howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
 inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
 inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
 lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
@@ -1614,9 +1653,12 @@ mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
 mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=
 tailscale.com v1.28.0 h1:eW5bJMqw6eu7YUjBcgJY94uIcm5Zv+xpyTxxa7ztZOM=
 tailscale.com v1.28.0/go.mod h1:T9uKhlkxVPdSu1Qvp882evcS/hQ1+TAyZ7sJ/VACGRI=

From 175dfa1ede2b9be7e9bd13d9b5e3d3b42698921c Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 20 Aug 2022 00:15:46 +0200
Subject: [PATCH 59/63] Update flake.nix sum

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

diff --git a/flake.nix b/flake.nix
index 0c257ff53e..62631bfa28 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-1VYegqEearzbqEX8ZLbsvHrRKbM/HIm/XIqQjMbvxkA=";
+              vendorSha256 = "sha256-paDdPsi5OfxsmgX7c5NSDSLYDipFqxxcxV3K4Tc77nQ=";
 
               ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
             };

From f0a8a2857bce64e0585263b4edd8beff60df80ff Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 20 Aug 2022 00:23:33 +0200
Subject: [PATCH 60/63] Clarified why we have a different key

---
 app.go             | 1 +
 protocol_common.go | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app.go b/app.go
index 851805dcce..6e37fcd419 100644
--- a/app.go
+++ b/app.go
@@ -132,6 +132,7 @@ func NewHeadscale(cfg *Config) (*Headscale, error) {
 		return nil, ErrFailedPrivateKey
 	}
 
+	// TS2021 requires to have a different key from the legacy protocol.
 	noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
 	if err != nil {
 		return nil, ErrFailedNoisePrivateKey
diff --git a/protocol_common.go b/protocol_common.go
index 49c9138a86..3cce7602fe 100644
--- a/protocol_common.go
+++ b/protocol_common.go
@@ -56,8 +56,8 @@ func (h *Headscale) KeyHandler(
 			return
 		}
 
+		// TS2021 (Tailscale v2 protocol) requires to have a different key
 		if clientCapabilityVersion >= NoiseCapabilityVersion {
-			// Tailscale has a different key for the TS2021 protocol
 			resp := tailcfg.OverTLSPublicKeyResponse{
 				LegacyPublicKey: h.privateKey.Public(),
 				PublicKey:       h.noisePrivateKey.Public(),

From 4424a9abc0e1c81aae6709a70c635fc71b5e308f Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 21 Aug 2022 10:42:23 +0200
Subject: [PATCH 61/63] Noise private key now a nested field in config

---
 cmd/headscale/headscale_test.go               | 20 ++++++++++++-------
 config-example.yaml                           | 14 +++++++------
 config.go                                     |  6 +++---
 docs/running-headscale-container.md           |  3 ++-
 .../etc/alt-config.dump.gold.yaml             |  3 ++-
 integration_test/etc/alt-config.yaml          |  3 ++-
 integration_test/etc/config.dump.gold.yaml    |  3 ++-
 7 files changed, 32 insertions(+), 20 deletions(-)

diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go
index b0667f7813..007d280155 100644
--- a/cmd/headscale/headscale_test.go
+++ b/cmd/headscale/headscale_test.go
@@ -163,10 +163,12 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
 		c.Fatal(err)
 	}
 	// defer os.RemoveAll(tmpDir)
-
-	configYaml := []byte(
-		"---\nnoise_private_key_path: \"noise_private.key\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"\n",
-	)
+	configYaml := []byte(`---
+tls_letsencrypt_hostname: example.com
+tls_letsencrypt_challenge_type: ""
+tls_cert_path: abc.pem
+noise:
+  private_key_path: noise_private.key`)
 	writeConfig(c, tmpDir, configYaml)
 
 	// Check configuration validation errors (1)
@@ -191,9 +193,13 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
 	)
 
 	// Check configuration validation errors (2)
-	configYaml = []byte(
-		"---\nnoise_private_key_path: \"noise_private.key\"\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
-	)
+	configYaml = []byte(`---
+noise:
+  private_key_path: noise_private.key
+server_url: http://127.0.0.1:8080
+tls_letsencrypt_hostname: example.com
+tls_letsencrypt_challenge_type: TLS-ALPN-01
+`)
 	writeConfig(c, tmpDir, configYaml)
 	err = headscale.LoadConfig(tmpDir, false)
 	c.Assert(err, check.IsNil)
diff --git a/config-example.yaml b/config-example.yaml
index 5ebc1300da..2019a13364 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -41,12 +41,14 @@ grpc_allow_insecure: false
 # autogenerated if it's missing
 private_key_path: /var/lib/headscale/private.key
 
-# The Noise private key is used to encrypt the
-# traffic between headscale and Tailscale clients when
-# using the new Noise-based TS2021 protocol.
-# The noise private key file which will be
-# autogenerated if it's missing
-noise_private_key_path: /var/lib/headscale/noise_private.key
+# The Noise section includes specific configuration for the
+# TS2021 Noise procotol
+noise:
+  # The Noise private key is used to encrypt the
+  # traffic between headscale and Tailscale clients when
+  # using the new Noise-based protocol. It must be different
+  # from the legacy private key.
+  private_key_path: /var/lib/headscale/noise_private.key
 
 # List of IP prefixes to allocate tailaddresses from.
 # Each prefix consists of either an IPv4 or IPv6 address,
diff --git a/config.go b/config.go
index e503b61563..0024731889 100644
--- a/config.go
+++ b/config.go
@@ -185,8 +185,8 @@ func LoadConfig(path string, isFile bool) error {
 		errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n"
 	}
 
-	if !viper.IsSet("noise_private_key_path") {
-		errorText += "Fatal config error: headscale now requires a new `noise_private_key_path` field in the config file for the Tailscale v2 protocol\n"
+	if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
+		errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
 	}
 
 	if (viper.GetString("tls_letsencrypt_hostname") != "") &&
@@ -494,7 +494,7 @@ func GetHeadscaleConfig() (*Config, error) {
 			viper.GetString("private_key_path"),
 		),
 		NoisePrivateKeyPath: AbsolutePathFromConfigPath(
-			viper.GetString("noise_private_key_path"),
+			viper.GetString("noise.private_key_path"),
 		),
 		BaseDomain: baseDomain,
 
diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md
index 4a9f15188c..d341bb7cf3 100644
--- a/docs/running-headscale-container.md
+++ b/docs/running-headscale-container.md
@@ -54,7 +54,8 @@ metrics_listen_addr: 0.0.0.0:9090
 # The default /var/lib/headscale path is not writable in the container
 private_key_path: /etc/headscale/private.key
 # The default /var/lib/headscale path is not writable in the container
-noise_private_key_path: /var/lib/headscale/noise_private.key
+noise:
+  private_key_path: /var/lib/headscale/noise_private.key
 # The default /var/lib/headscale path is not writable  in the container
 db_path: /etc/headscale/db.sqlite
 ```
diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml
index e71c957658..3d38b12828 100644
--- a/integration_test/etc/alt-config.dump.gold.yaml
+++ b/integration_test/etc/alt-config.dump.gold.yaml
@@ -39,7 +39,8 @@ oidc:
     - email
   strip_email_domain: true
 private_key_path: private.key
-noise_private_key_path: noise_private.key
+noise:
+  private_key_path: noise_private.key
 server_url: http://headscale:18080
 tls_client_auth_mode: relaxed
 tls_letsencrypt_cache_dir: /var/www/.cache
diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml
index 35dd9e4802..179fdcd527 100644
--- a/integration_test/etc/alt-config.yaml
+++ b/integration_test/etc/alt-config.yaml
@@ -15,7 +15,8 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
-noise_private_key_path: noise_private.key
+noise:
+  private_key_path: noise_private.key
 listen_addr: 0.0.0.0:18080
 metrics_listen_addr: 127.0.0.1:19090
 server_url: http://headscale:18080
diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml
index 715396566b..91ca5b93fd 100644
--- a/integration_test/etc/config.dump.gold.yaml
+++ b/integration_test/etc/config.dump.gold.yaml
@@ -39,7 +39,8 @@ oidc:
     - email
   strip_email_domain: true
 private_key_path: private.key
-noise_private_key_path: noise_private.key
+noise:
+  private_key_path: noise_private.key
 server_url: http://headscale:8080
 tls_client_auth_mode: relaxed
 tls_letsencrypt_cache_dir: /var/www/.cache

From 71d22dc994c716508de9499983ca3a6751a58776 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 21 Aug 2022 10:47:45 +0200
Subject: [PATCH 62/63] Added missing files

---
 integration_test/etc/config.yaml               | 3 ++-
 integration_test/etc_embedded_derp/config.yaml | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml
index 9866d554ec..da842cc417 100644
--- a/integration_test/etc/config.yaml
+++ b/integration_test/etc/config.yaml
@@ -15,7 +15,8 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
-noise_private_key_path: noise_private.key
+noise:
+  private_key_path: noise_private.key
 listen_addr: 0.0.0.0:8080
 metrics_listen_addr: 127.0.0.1:9090
 server_url: http://headscale:8080
diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index 86a59848b9..ed4d51a075 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -14,7 +14,8 @@ dns_config:
     - 1.1.1.1
 db_path: /tmp/integration_test_db.sqlite3
 private_key_path: private.key
-noise_private_key_path: noise_private.key
+noise:
+  private_key_path: noise_private.key
 listen_addr: 0.0.0.0:443
 server_url: https://headscale:443
 tls_cert_path: "/etc/headscale/tls/server.crt"

From 4aafe6c9d1bba90266fcc5cdd1ba7c79fcfe6907 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 21 Aug 2022 12:32:01 +0200
Subject: [PATCH 63/63] Added line in CHANGELOG

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e72bcc771..2f2fd51771 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ## 0.17.0 (2022-XX-XX)
 
+- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
+
 ## 0.16.4 (2022-08-21)
 
 ### Changes