From 897d480f4dd723ad0c197e9c9a782da8a4503c7f Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 4 Mar 2022 00:01:31 +0100
Subject: [PATCH 01/28] Add an embedded DERP server to Headscale

This series of commit will be adding an embedded DERP server (and STUN) to Headscale,
thus making it completely self-contained and not dependant in other infrastructure.
---
 app.go                     |  58 ++++++++++--
 cmd/headscale/cli/utils.go |   6 ++
 derp_embedded.go           | 178 +++++++++++++++++++++++++++++++++++++
 3 files changed, 235 insertions(+), 7 deletions(-)
 create mode 100644 derp_embedded.go

diff --git a/app.go b/app.go
index 5818509..ee38ed9 100644
--- a/app.go
+++ b/app.go
@@ -119,6 +119,7 @@ type OIDCConfig struct {
 }
 
 type DERPConfig struct {
+	EmbeddedDERP    bool
 	URLs            []url.URL
 	Paths           []string
 	AutoUpdate      bool
@@ -141,7 +142,8 @@ type Headscale struct {
 	dbDebug    bool
 	privateKey *key.MachinePrivate
 
-	DERPMap *tailcfg.DERPMap
+	DERPMap            *tailcfg.DERPMap
+	EmbeddedDerpServer *EmbeddedDerpServer
 
 	aclPolicy *ACLPolicy
 	aclRules  []tailcfg.FilterRule
@@ -238,6 +240,38 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
 		}
 	}
 
+	if cfg.DERP.EmbeddedDERP {
+		embeddedDerpServer, err := app.NewEmbeddedDerpServer()
+		if err != nil {
+			return nil, err
+		}
+		app.EmbeddedDerpServer = embeddedDerpServer
+
+		// If we are using the embedded DERP, there is no reason to use Tailscale's DERP infrastructure
+		serverURL, err := url.Parse(app.cfg.ServerURL)
+		if err != nil {
+			return nil, err
+		}
+		app.DERPMap = &tailcfg.DERPMap{
+			Regions: map[int]*tailcfg.DERPRegion{
+				1: {
+					RegionID:   1,
+					RegionCode: "headscale",
+					RegionName: "Headscale Embedded DERP",
+					Avoid:      false,
+					Nodes: []*tailcfg.DERPNode{
+						{
+							Name:     "1a",
+							RegionID: 1,
+							HostName: serverURL.Host,
+						},
+					},
+				},
+			},
+			OmitDefaultRegions: false,
+		}
+	}
+
 	return &app, nil
 }
 
@@ -454,6 +488,12 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 	router.GET("/swagger", SwaggerUI)
 	router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
 
+	if h.cfg.DERP.EmbeddedDERP {
+		router.Any("/derp", h.EmbeddedDerpHandler)
+		router.Any("/derp/probe", h.EmbeddedDerpProbeHandler)
+		router.Any("/bootstrap-dns", h.EmbeddedDerpBootstrapDNSHandler)
+	}
+
 	api := router.Group("/api")
 	api.Use(h.httpAuthenticationMiddleware)
 	{
@@ -469,13 +509,17 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 func (h *Headscale) Serve() error {
 	var err error
 
-	// Fetch an initial DERP Map before we start serving
-	h.DERPMap = GetDERPMap(h.cfg.DERP)
+	if h.cfg.DERP.EmbeddedDERP {
+		go h.ServeSTUN()
+	} else {
+		// Fetch an initial DERP Map before we start serving
+		h.DERPMap = GetDERPMap(h.cfg.DERP)
 
-	if h.cfg.DERP.AutoUpdate {
-		derpMapCancelChannel := make(chan struct{})
-		defer func() { derpMapCancelChannel <- struct{}{} }()
-		go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
+		if h.cfg.DERP.AutoUpdate {
+			derpMapCancelChannel := make(chan struct{})
+			defer func() { derpMapCancelChannel <- struct{}{} }()
+			go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
+		}
 	}
 
 	go h.expireEphemeralNodes(updateInterval)
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index 97c2440..cff31f3 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -117,6 +117,12 @@ func LoadConfig(path string) error {
 }
 
 func GetDERPConfig() headscale.DERPConfig {
+	if viper.GetBool("derp.embedded_derp") {
+		return headscale.DERPConfig{
+			EmbeddedDERP: true,
+		}
+	}
+
 	urlStrs := viper.GetStringSlice("derp.urls")
 
 	urls := make([]url.URL, len(urlStrs))
diff --git a/derp_embedded.go b/derp_embedded.go
new file mode 100644
index 0000000..0631fff
--- /dev/null
+++ b/derp_embedded.go
@@ -0,0 +1,178 @@
+package headscale
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/rs/zerolog/log"
+	"tailscale.com/derp"
+	"tailscale.com/net/stun"
+	"tailscale.com/types/key"
+)
+
+// fastStartHeader is the header (with value "1") that signals to the HTTP
+// server that the DERP HTTP client does not want the HTTP 101 response
+// headers and it will begin writing & reading the DERP protocol immediately
+// following its HTTP request.
+const fastStartHeader = "Derp-Fast-Start"
+
+var (
+	dnsCache     atomic.Value // of []byte
+	bootstrapDNS = "derp.tailscale.com"
+)
+
+type EmbeddedDerpServer struct {
+	tailscaleDerp *derp.Server
+}
+
+func (h *Headscale) NewEmbeddedDerpServer() (*EmbeddedDerpServer, error) {
+	s := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
+	return &EmbeddedDerpServer{s}, nil
+
+}
+
+func (h *Headscale) EmbeddedDerpHandler(ctx *gin.Context) {
+	up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
+	if up != "websocket" && up != "derp" {
+		if up != "" {
+			log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
+		}
+		ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
+		return
+	}
+
+	fastStart := ctx.Request.Header.Get(fastStartHeader) == "1"
+
+	hijacker, ok := ctx.Writer.(http.Hijacker)
+	if !ok {
+		log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
+		ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
+		return
+	}
+
+	netConn, conn, err := hijacker.Hijack()
+	if err != nil {
+		log.Error().Caller().Err(err).Msgf("Hijack failed")
+		ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
+		return
+	}
+
+	if !fastStart {
+		pubKey := h.privateKey.Public()
+		fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
+			"Upgrade: DERP\r\n"+
+			"Connection: Upgrade\r\n"+
+			"Derp-Version: %v\r\n"+
+			"Derp-Public-Key: %s\r\n\r\n",
+			derp.ProtocolVersion,
+			pubKey.UntypedHexString())
+	}
+
+	h.EmbeddedDerpServer.tailscaleDerp.Accept(netConn, conn, netConn.RemoteAddr().String())
+}
+
+// EmbeddedDerpProbeHandler is the endpoint that js/wasm clients hit to measure
+// DERP latency, since they can't do UDP STUN queries.
+func (h *Headscale) EmbeddedDerpProbeHandler(ctx *gin.Context) {
+	switch ctx.Request.Method {
+	case "HEAD", "GET":
+		ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+	default:
+		ctx.String(http.StatusMethodNotAllowed, "bogus probe method")
+	}
+}
+
+func (h *Headscale) EmbeddedDerpBootstrapDNSHandler(ctx *gin.Context) {
+	ctx.Header("Content-Type", "application/json")
+	j, _ := dnsCache.Load().([]byte)
+	// Bootstrap DNS requests occur cross-regions,
+	// and are randomized per request,
+	// so keeping a connection open is pointlessly expensive.
+	ctx.Header("Connection", "close")
+	ctx.Writer.Write(j)
+}
+
+// ServeSTUN starts a STUN server on udp/3478
+func (h *Headscale) ServeSTUN() {
+	pc, err := net.ListenPacket("udp", "0.0.0.0:3478")
+	if err != nil {
+		log.Fatal().Msgf("failed to open STUN listener: %v", err)
+	}
+	log.Printf("running STUN server on %v", pc.LocalAddr())
+	serverSTUNListener(context.Background(), pc.(*net.UDPConn))
+}
+
+func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
+	var buf [64 << 10]byte
+	var (
+		n   int
+		ua  *net.UDPAddr
+		err error
+	)
+	for {
+		n, ua, err = pc.ReadFromUDP(buf[:])
+		if err != nil {
+			if ctx.Err() != nil {
+				return
+			}
+			log.Printf("STUN ReadFrom: %v", err)
+			time.Sleep(time.Second)
+			continue
+		}
+		pkt := buf[:n]
+		if !stun.Is(pkt) {
+			continue
+		}
+		txid, err := stun.ParseBindingRequest(pkt)
+		if err != nil {
+			continue
+		}
+
+		res := stun.Response(txid, ua.IP, uint16(ua.Port))
+		pc.WriteTo(res, ua)
+	}
+}
+
+// Shamelessly taken from
+// https://github.com/tailscale/tailscale/blob/main/cmd/derper/bootstrap_dns.go
+func refreshBootstrapDNSLoop() {
+	if bootstrapDNS == "" {
+		return
+	}
+	for {
+		refreshBootstrapDNS()
+		time.Sleep(10 * time.Minute)
+	}
+}
+
+func refreshBootstrapDNS() {
+	if bootstrapDNS == "" {
+		return
+	}
+	dnsEntries := make(map[string][]net.IP)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+	defer cancel()
+	names := strings.Split(bootstrapDNS, ",")
+	var r net.Resolver
+	for _, name := range names {
+		addrs, err := r.LookupIP(ctx, "ip", name)
+		if err != nil {
+			log.Printf("bootstrap DNS lookup %q: %v", name, err)
+			continue
+		}
+		dnsEntries[name] = addrs
+	}
+	j, err := json.MarshalIndent(dnsEntries, "", "\t")
+	if err != nil {
+		// leave the old values in place
+		return
+	}
+	dnsCache.Store(j)
+}

From 9d43f589ae5ac9b902247b5d00ec28cc632b9168 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 4 Mar 2022 00:04:28 +0100
Subject: [PATCH 02/28] Added missing deps

---
 go.mod | 3 +++
 go.sum | 7 +++++++
 2 files changed, 10 insertions(+)

diff --git a/go.mod b/go.mod
index a121826..d6754f9 100644
--- a/go.mod
+++ b/go.mod
@@ -46,6 +46,7 @@ require (
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Microsoft/go-winio v0.5.2 // indirect
 	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
+	github.com/akutz/memconn v0.1.0 // indirect
 	github.com/atomicgo/cursor v0.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.2 // indirect
@@ -100,6 +101,7 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.11 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
+	github.com/mitchellh/go-ps v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.4.3 // indirect
 	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -134,6 +136,7 @@ require (
 	golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
 	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
diff --git a/go.sum b/go.sum
index 558d3d7..c23db38 100644
--- a/go.sum
+++ b/go.sum
@@ -82,6 +82,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
+github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -205,6 +207,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
 github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
 github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
 github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -570,6 +573,8 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@@ -1083,6 +1088,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
+golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

From 607c1eb3163999fb310170a5ee67a83c634e8196 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 4 Mar 2022 11:31:41 +0100
Subject: [PATCH 03/28] Be consistent with uppercase DERP

---
 app.go           | 12 ++++++------
 derp_embedded.go | 18 +++++++++---------
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/app.go b/app.go
index 3115b99..eb3b8ad 100644
--- a/app.go
+++ b/app.go
@@ -144,7 +144,7 @@ type Headscale struct {
 	privateKey *key.MachinePrivate
 
 	DERPMap            *tailcfg.DERPMap
-	EmbeddedDerpServer *EmbeddedDerpServer
+	EmbeddedDERPServer *EmbeddedDERPServer
 
 	aclPolicy *ACLPolicy
 	aclRules  []tailcfg.FilterRule
@@ -242,11 +242,11 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
 	}
 
 	if cfg.DERP.EmbeddedDERP {
-		embeddedDerpServer, err := app.NewEmbeddedDerpServer()
+		embeddedDERPServer, err := app.NewEmbeddedDERPServer()
 		if err != nil {
 			return nil, err
 		}
-		app.EmbeddedDerpServer = embeddedDerpServer
+		app.EmbeddedDERPServer = embeddedDERPServer
 
 		// If we are using the embedded DERP, there is no reason to use Tailscale's DERP infrastructure
 		serverURL, err := url.Parse(app.cfg.ServerURL)
@@ -496,9 +496,9 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 	router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
 
 	if h.cfg.DERP.EmbeddedDERP {
-		router.Any("/derp", h.EmbeddedDerpHandler)
-		router.Any("/derp/probe", h.EmbeddedDerpProbeHandler)
-		router.Any("/bootstrap-dns", h.EmbeddedDerpBootstrapDNSHandler)
+		router.Any("/derp", h.EmbeddedDERPHandler)
+		router.Any("/derp/probe", h.EmbeddedDERPProbeHandler)
+		router.Any("/bootstrap-dns", h.EmbeddedDERPBootstrapDNSHandler)
 	}
 
 	api := router.Group("/api")
diff --git a/derp_embedded.go b/derp_embedded.go
index 0631fff..1d4fb0b 100644
--- a/derp_embedded.go
+++ b/derp_embedded.go
@@ -28,17 +28,17 @@ var (
 	bootstrapDNS = "derp.tailscale.com"
 )
 
-type EmbeddedDerpServer struct {
-	tailscaleDerp *derp.Server
+type EmbeddedDERPServer struct {
+	tailscaleDERP *derp.Server
 }
 
-func (h *Headscale) NewEmbeddedDerpServer() (*EmbeddedDerpServer, error) {
+func (h *Headscale) NewEmbeddedDERPServer() (*EmbeddedDERPServer, error) {
 	s := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
-	return &EmbeddedDerpServer{s}, nil
+	return &EmbeddedDERPServer{s}, nil
 
 }
 
-func (h *Headscale) EmbeddedDerpHandler(ctx *gin.Context) {
+func (h *Headscale) EmbeddedDERPHandler(ctx *gin.Context) {
 	up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
 	if up != "websocket" && up != "derp" {
 		if up != "" {
@@ -75,12 +75,12 @@ func (h *Headscale) EmbeddedDerpHandler(ctx *gin.Context) {
 			pubKey.UntypedHexString())
 	}
 
-	h.EmbeddedDerpServer.tailscaleDerp.Accept(netConn, conn, netConn.RemoteAddr().String())
+	h.EmbeddedDERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
 }
 
-// EmbeddedDerpProbeHandler is the endpoint that js/wasm clients hit to measure
+// EmbeddedDERPProbeHandler is the endpoint that js/wasm clients hit to measure
 // DERP latency, since they can't do UDP STUN queries.
-func (h *Headscale) EmbeddedDerpProbeHandler(ctx *gin.Context) {
+func (h *Headscale) EmbeddedDERPProbeHandler(ctx *gin.Context) {
 	switch ctx.Request.Method {
 	case "HEAD", "GET":
 		ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
@@ -89,7 +89,7 @@ func (h *Headscale) EmbeddedDerpProbeHandler(ctx *gin.Context) {
 	}
 }
 
-func (h *Headscale) EmbeddedDerpBootstrapDNSHandler(ctx *gin.Context) {
+func (h *Headscale) EmbeddedDERPBootstrapDNSHandler(ctx *gin.Context) {
 	ctx.Header("Content-Type", "application/json")
 	j, _ := dnsCache.Load().([]byte)
 	// Bootstrap DNS requests occur cross-regions,

From 22d2443281466006500f3a0ceb2e1f87b21d174d Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 4 Mar 2022 13:26:45 +0100
Subject: [PATCH 04/28] Move more stuff to common

---
 integration_common_test.go | 7 +++++++
 integration_test.go        | 7 -------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/integration_common_test.go b/integration_common_test.go
index de304d0..de9fdd9 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -18,8 +18,15 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
 var (
 	IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
 	IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
+
+	tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
 )
 
+type TestNamespace struct {
+	count      int
+	tailscales map[string]dockertest.Resource
+}
+
 type ExecuteCommandConfig struct {
 	timeout time.Duration
 }
diff --git a/integration_test.go b/integration_test.go
index 03d6d2f..523a9a9 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -29,13 +29,6 @@ import (
 	"tailscale.com/ipn/ipnstate"
 )
 
-var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
-
-type TestNamespace struct {
-	count      int
-	tailscales map[string]dockertest.Resource
-}
-
 type IntegrationTestSuite struct {
 	suite.Suite
 	stats *suite.SuiteInformation

From 09d78c7a05b6a9dc90b1a365d0a98ee34f62b112 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Fri, 4 Mar 2022 13:54:59 +0100
Subject: [PATCH 05/28] Even more stuff moved to common

---
 integration_common_test.go | 33 +++++++++++++++++++++++++++++++++
 integration_test.go        | 32 --------------------------------
 2 files changed, 33 insertions(+), 32 deletions(-)

diff --git a/integration_common_test.go b/integration_common_test.go
index de9fdd9..a341712 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -6,6 +6,7 @@ package headscale
 import (
 	"bytes"
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/ory/dockertest/v3"
@@ -126,3 +127,35 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
 		Target: "/dev/net/tun",
 	})
 }
+
+func getIPs(
+	tailscales map[string]dockertest.Resource,
+) (map[string][]netaddr.IP, error) {
+	ips := make(map[string][]netaddr.IP)
+	for hostname, tailscale := range tailscales {
+		command := []string{"tailscale", "ip"}
+
+		result, err := ExecuteCommand(
+			&tailscale,
+			command,
+			[]string{},
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, address := range strings.Split(result, "\n") {
+			address = strings.TrimSuffix(address, "\n")
+			if len(address) < 1 {
+				continue
+			}
+			ip, err := netaddr.ParseIP(address)
+			if err != nil {
+				return nil, err
+			}
+			ips[hostname] = append(ips[hostname], ip)
+		}
+	}
+
+	return ips, nil
+}
diff --git a/integration_test.go b/integration_test.go
index 523a9a9..1649f32 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -680,38 +680,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
 	}
 }
 
-func getIPs(
-	tailscales map[string]dockertest.Resource,
-) (map[string][]netaddr.IP, error) {
-	ips := make(map[string][]netaddr.IP)
-	for hostname, tailscale := range tailscales {
-		command := []string{"tailscale", "ip"}
-
-		result, err := ExecuteCommand(
-			&tailscale,
-			command,
-			[]string{},
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		for _, address := range strings.Split(result, "\n") {
-			address = strings.TrimSuffix(address, "\n")
-			if len(address) < 1 {
-				continue
-			}
-			ip, err := netaddr.ParseIP(address)
-			if err != nil {
-				return nil, err
-			}
-			ips[hostname] = append(ips[hostname], ip)
-		}
-	}
-
-	return ips, nil
-}
-
 func getAPIURLs(
 	tailscales map[string]dockertest.Resource,
 ) (map[netaddr.IP]string, error) {

From 758b1ba1cbcd13fcff10954a301b7773f5921f20 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 16:22:02 +0100
Subject: [PATCH 06/28] Renamed configuration items of the DERP server

---
 app.go                     | 50 ++++++++++++++++++++++++++------------
 cmd/headscale/cli/utils.go |  9 +++----
 config-example.yaml        |  8 ++++++
 3 files changed, 47 insertions(+), 20 deletions(-)

diff --git a/app.go b/app.go
index eb3b8ad..b739a05 100644
--- a/app.go
+++ b/app.go
@@ -13,6 +13,7 @@ import (
 	"os"
 	"os/signal"
 	"sort"
+	"strconv"
 	"strings"
 	"sync"
 	"syscall"
@@ -120,7 +121,8 @@ type OIDCConfig struct {
 }
 
 type DERPConfig struct {
-	EmbeddedDERP    bool
+	ServerEnabled   bool
+	ServerInsecure  bool
 	URLs            []url.URL
 	Paths           []string
 	AutoUpdate      bool
@@ -143,8 +145,8 @@ type Headscale struct {
 	dbDebug    bool
 	privateKey *key.MachinePrivate
 
-	DERPMap            *tailcfg.DERPMap
-	EmbeddedDERPServer *EmbeddedDERPServer
+	DERPMap    *tailcfg.DERPMap
+	DERPServer *DERPServer
 
 	aclPolicy *ACLPolicy
 	aclRules  []tailcfg.FilterRule
@@ -180,7 +182,6 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
 	}
 }
 
-// NewHeadscale returns the Headscale app.
 func NewHeadscale(cfg Config) (*Headscale, error) {
 	privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
 	if err != nil {
@@ -241,30 +242,49 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
 		}
 	}
 
-	if cfg.DERP.EmbeddedDERP {
-		embeddedDERPServer, err := app.NewEmbeddedDERPServer()
+	if cfg.DERP.ServerEnabled {
+		embeddedDERPServer, err := app.NewDERPServer()
 		if err != nil {
 			return nil, err
 		}
-		app.EmbeddedDERPServer = embeddedDERPServer
+		app.DERPServer = embeddedDERPServer
 
-		// If we are using the embedded DERP, there is no reason to use Tailscale's DERP infrastructure
 		serverURL, err := url.Parse(app.cfg.ServerURL)
 		if err != nil {
 			return nil, err
 		}
+		var host string
+		var port int
+		host, portStr, err := net.SplitHostPort(serverURL.Host)
+		if err != nil {
+			if serverURL.Scheme == "https" {
+				host = serverURL.Host
+				port = 443
+			} else {
+				host = serverURL.Host
+				port = 80
+			}
+		} else {
+			port, err = strconv.Atoi(portStr)
+			if err != nil {
+				return nil, err
+			}
+		}
+
 		app.DERPMap = &tailcfg.DERPMap{
 			Regions: map[int]*tailcfg.DERPRegion{
-				1: {
-					RegionID:   1,
+				999: {
+					RegionID:   999,
 					RegionCode: "headscale",
 					RegionName: "Headscale Embedded DERP",
 					Avoid:      false,
 					Nodes: []*tailcfg.DERPNode{
 						{
-							Name:     "1a",
-							RegionID: 1,
-							HostName: serverURL.Host,
+							Name:             "999a",
+							RegionID:         999,
+							HostName:         host,
+							DERPPort:         port,
+							InsecureForTests: cfg.DERP.ServerInsecure,
 						},
 					},
 				},
@@ -495,7 +515,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 	router.GET("/swagger", SwaggerUI)
 	router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
 
-	if h.cfg.DERP.EmbeddedDERP {
+	if h.cfg.DERP.ServerEnabled {
 		router.Any("/derp", h.EmbeddedDERPHandler)
 		router.Any("/derp/probe", h.EmbeddedDERPProbeHandler)
 		router.Any("/bootstrap-dns", h.EmbeddedDERPBootstrapDNSHandler)
@@ -516,7 +536,7 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 func (h *Headscale) Serve() error {
 	var err error
 
-	if h.cfg.DERP.EmbeddedDERP {
+	if h.cfg.DERP.ServerEnabled {
 		go h.ServeSTUN()
 	} else {
 		// Fetch an initial DERP Map before we start serving
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index 98ffe2e..06b9ca9 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -117,11 +117,8 @@ func LoadConfig(path string) error {
 }
 
 func GetDERPConfig() headscale.DERPConfig {
-	if viper.GetBool("derp.embedded_derp") {
-		return headscale.DERPConfig{
-			EmbeddedDERP: true,
-		}
-	}
+	enabled := viper.GetBool("derp.server.enabled")
+	insecure := viper.GetBool("derp.server.insecure")
 
 	urlStrs := viper.GetStringSlice("derp.urls")
 
@@ -144,6 +141,8 @@ func GetDERPConfig() headscale.DERPConfig {
 	updateFrequency := viper.GetDuration("derp.update_frequency")
 
 	return headscale.DERPConfig{
+		ServerEnabled:   enabled,
+		ServerInsecure:  insecure,
 		URLs:            urls,
 		Paths:           paths,
 		AutoUpdate:      autoUpdate,
diff --git a/config-example.yaml b/config-example.yaml
index c28b608..84b1c90 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -55,6 +55,14 @@ ip_prefixes:
 # headscale needs a list of DERP servers that can be presented
 # to the clients.
 derp:
+  server:
+    # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
+    enabled: false
+
+    # Insecure mode is recommended only for tests. It indicates the tailscale clients
+    # to use insecure connections to this server.
+    insecure: false
+
   # List of externally available DERP maps encoded in JSON
   urls:
     - https://controlplane.tailscale.com/derpmap/default

From df37d1a639b81e7b9305bba3ef941d38a64604a1 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:19:21 +0100
Subject: [PATCH 07/28] Do not offer the option to be DERP insecure

Websockets, in which DERP is based, requires a TLS certificate. At the same time,
if we use a certificate it must be valid... otherwise Tailscale wont connect (does not
have an Insecure option). So there is no option to expose insecure here
---
 app.go                     | 16 +++++++---------
 cmd/headscale/cli/utils.go |  2 --
 config-example.yaml        |  5 +----
 3 files changed, 8 insertions(+), 15 deletions(-)

diff --git a/app.go b/app.go
index b739a05..34602d6 100644
--- a/app.go
+++ b/app.go
@@ -122,7 +122,6 @@ type OIDCConfig struct {
 
 type DERPConfig struct {
 	ServerEnabled   bool
-	ServerInsecure  bool
 	URLs            []url.URL
 	Paths           []string
 	AutoUpdate      bool
@@ -280,11 +279,10 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
 					Avoid:      false,
 					Nodes: []*tailcfg.DERPNode{
 						{
-							Name:             "999a",
-							RegionID:         999,
-							HostName:         host,
-							DERPPort:         port,
-							InsecureForTests: cfg.DERP.ServerInsecure,
+							Name:     "999a",
+							RegionID: 999,
+							HostName: host,
+							DERPPort: port,
 						},
 					},
 				},
@@ -516,9 +514,9 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 	router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
 
 	if h.cfg.DERP.ServerEnabled {
-		router.Any("/derp", h.EmbeddedDERPHandler)
-		router.Any("/derp/probe", h.EmbeddedDERPProbeHandler)
-		router.Any("/bootstrap-dns", h.EmbeddedDERPBootstrapDNSHandler)
+		router.Any("/derp", h.DERPHandler)
+		router.Any("/derp/probe", h.DERPProbeHandler)
+		router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
 	}
 
 	api := router.Group("/api")
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index 06b9ca9..7277723 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -118,7 +118,6 @@ func LoadConfig(path string) error {
 
 func GetDERPConfig() headscale.DERPConfig {
 	enabled := viper.GetBool("derp.server.enabled")
-	insecure := viper.GetBool("derp.server.insecure")
 
 	urlStrs := viper.GetStringSlice("derp.urls")
 
@@ -142,7 +141,6 @@ func GetDERPConfig() headscale.DERPConfig {
 
 	return headscale.DERPConfig{
 		ServerEnabled:   enabled,
-		ServerInsecure:  insecure,
 		URLs:            urls,
 		Paths:           paths,
 		AutoUpdate:      autoUpdate,
diff --git a/config-example.yaml b/config-example.yaml
index 84b1c90..08cc6c1 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -57,12 +57,9 @@ ip_prefixes:
 derp:
   server:
     # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
+    # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
     enabled: false
 
-    # Insecure mode is recommended only for tests. It indicates the tailscale clients
-    # to use insecure connections to this server.
-    insecure: false
-
   # List of externally available DERP maps encoded in JSON
   urls:
     - https://controlplane.tailscale.com/derpmap/default

From b7423796278aa4036bca3e64d587801d8a1fd0eb Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:30:30 +0100
Subject: [PATCH 08/28] Do not use the term embedded

---
 derp_embedded.go | 24 +++++++++++++-----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/derp_embedded.go b/derp_embedded.go
index 1d4fb0b..d8abbc8 100644
--- a/derp_embedded.go
+++ b/derp_embedded.go
@@ -28,17 +28,18 @@ var (
 	bootstrapDNS = "derp.tailscale.com"
 )
 
-type EmbeddedDERPServer struct {
+type DERPServer struct {
 	tailscaleDERP *derp.Server
 }
 
-func (h *Headscale) NewEmbeddedDERPServer() (*EmbeddedDERPServer, error) {
+func (h *Headscale) NewDERPServer() (*DERPServer, error) {
 	s := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
-	return &EmbeddedDERPServer{s}, nil
+	return &DERPServer{s}, nil
 
 }
 
-func (h *Headscale) EmbeddedDERPHandler(ctx *gin.Context) {
+func (h *Headscale) DERPHandler(ctx *gin.Context) {
+	log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
 	up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
 	if up != "websocket" && up != "derp" {
 		if up != "" {
@@ -75,12 +76,12 @@ func (h *Headscale) EmbeddedDERPHandler(ctx *gin.Context) {
 			pubKey.UntypedHexString())
 	}
 
-	h.EmbeddedDERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
+	h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
 }
 
-// EmbeddedDERPProbeHandler is the endpoint that js/wasm clients hit to measure
+// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
 // DERP latency, since they can't do UDP STUN queries.
-func (h *Headscale) EmbeddedDERPProbeHandler(ctx *gin.Context) {
+func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
 	switch ctx.Request.Method {
 	case "HEAD", "GET":
 		ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
@@ -89,7 +90,7 @@ func (h *Headscale) EmbeddedDERPProbeHandler(ctx *gin.Context) {
 	}
 }
 
-func (h *Headscale) EmbeddedDERPBootstrapDNSHandler(ctx *gin.Context) {
+func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
 	ctx.Header("Content-Type", "application/json")
 	j, _ := dnsCache.Load().([]byte)
 	// Bootstrap DNS requests occur cross-regions,
@@ -105,7 +106,7 @@ func (h *Headscale) ServeSTUN() {
 	if err != nil {
 		log.Fatal().Msgf("failed to open STUN listener: %v", err)
 	}
-	log.Printf("running STUN server on %v", pc.LocalAddr())
+	log.Trace().Msgf("STUN server started at %s", pc.LocalAddr())
 	serverSTUNListener(context.Background(), pc.(*net.UDPConn))
 }
 
@@ -122,10 +123,11 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
 			if ctx.Err() != nil {
 				return
 			}
-			log.Printf("STUN ReadFrom: %v", err)
+			log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
 			time.Sleep(time.Second)
 			continue
 		}
+		log.Trace().Caller().Msgf("STUN request from %v", ua)
 		pkt := buf[:n]
 		if !stun.Is(pkt) {
 			continue
@@ -164,7 +166,7 @@ func refreshBootstrapDNS() {
 	for _, name := range names {
 		addrs, err := r.LookupIP(ctx, "ip", name)
 		if err != nil {
-			log.Printf("bootstrap DNS lookup %q: %v", name, err)
+			log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup %q: %v", name)
 			continue
 		}
 		dnsEntries[name] = addrs

From 88378c22fb41383273f43c9d84aba1e44271b1f6 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:31:50 +0100
Subject: [PATCH 09/28] Rename the file to derp_server.go for coherence

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

diff --git a/derp_embedded.go b/derp_server.go
similarity index 100%
rename from derp_embedded.go
rename to derp_server.go

From e9eb90fa7691f1f45b0ad60180dd87ae59374ae7 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:34:06 +0100
Subject: [PATCH 10/28] Added integration tests for the embedded DERP server

---
 .dockerignore                                 |   1 +
 Dockerfile.tailscale                          |   7 +-
 Makefile                                      |   3 +
 integration_embedded_derp_test.go             | 384 ++++++++++++++++++
 .../etc_embedded_derp/tls/server.crt          |  18 +
 5 files changed, 412 insertions(+), 1 deletion(-)
 create mode 100644 integration_embedded_derp_test.go
 create mode 100644 integration_test/etc_embedded_derp/tls/server.crt

diff --git a/.dockerignore b/.dockerignore
index 057a20e..e3acf99 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,6 +3,7 @@
 // development
 integration_test.go
 integration_test/
+!integration_test/etc_embedded_derp/tls/server.crt
 
 Dockerfile*
 docker-compose*
diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale
index f96c6b9..fded837 100644
--- a/Dockerfile.tailscale
+++ b/Dockerfile.tailscale
@@ -7,5 +7,10 @@ RUN apt-get update \
     && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
     && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
     && apt-get update \
-    && apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
+    && apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
     && rm -rf /var/lib/apt/lists/*
+
+ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
+RUN chmod 644 /usr/local/share/ca-certificates/server.crt 
+
+RUN update-ca-certificates
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 266dadb..73630d3 100644
--- a/Makefile
+++ b/Makefile
@@ -23,6 +23,9 @@ test_integration:
 test_integration_cli:
 	go test -tags integration -v integration_cli_test.go integration_common_test.go
 
+test_integration_derp:
+	go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
+
 coverprofile_func:
 	go tool cover -func=coverage.out
 
diff --git a/integration_embedded_derp_test.go b/integration_embedded_derp_test.go
new file mode 100644
index 0000000..d95c460
--- /dev/null
+++ b/integration_embedded_derp_test.go
@@ -0,0 +1,384 @@
+//go:build integration
+
+package headscale
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
+	"github.com/ory/dockertest/v3"
+	"github.com/ory/dockertest/v3/docker"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+)
+
+const (
+	headscaleHostname = "headscale-derp"
+	namespaceName     = "derpnamespace"
+	totalContainers   = 3
+)
+
+type IntegrationDERPTestSuite struct {
+	suite.Suite
+	stats *suite.SuiteInformation
+
+	pool      dockertest.Pool
+	networks  map[int]dockertest.Network // so we keep the containers isolated
+	headscale dockertest.Resource
+
+	tailscales    map[string]dockertest.Resource
+	joinWaitGroup sync.WaitGroup
+}
+
+func TestDERPIntegrationTestSuite(t *testing.T) {
+	s := new(IntegrationDERPTestSuite)
+
+	s.tailscales = make(map[string]dockertest.Resource)
+	s.networks = make(map[int]dockertest.Network)
+
+	suite.Run(t, s)
+
+	// HandleStats, which allows us to check if we passed and save logs
+	// is called after TearDown, so we cannot tear down containers before
+	// we have potentially saved the logs.
+	for _, tailscale := range s.tailscales {
+		if err := s.pool.Purge(&tailscale); err != nil {
+			log.Printf("Could not purge resource: %s\n", err)
+		}
+	}
+
+	if !s.stats.Passed() {
+		err := s.saveLog(&s.headscale, "test_output")
+		if err != nil {
+			log.Printf("Could not save log: %s\n", err)
+		}
+	}
+	if err := s.pool.Purge(&s.headscale); err != nil {
+		log.Printf("Could not purge resource: %s\n", err)
+	}
+
+	for _, network := range s.networks {
+		if err := network.Close(); err != nil {
+			log.Printf("Could not close network: %s\n", err)
+		}
+	}
+}
+
+func (s *IntegrationDERPTestSuite) SetupSuite() {
+	if ppool, err := dockertest.NewPool(""); err == nil {
+		s.pool = *ppool
+	} else {
+		log.Fatalf("Could not connect to docker: %s", err)
+	}
+
+	for i := 0; i < totalContainers; i++ {
+		if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
+			s.networks[i] = *pnetwork
+		} else {
+			log.Fatalf("Could not create network: %s", err)
+		}
+	}
+
+	headscaleBuildOptions := &dockertest.BuildOptions{
+		Dockerfile: "Dockerfile",
+		ContextDir: ".",
+	}
+
+	currentPath, err := os.Getwd()
+	if err != nil {
+		log.Fatalf("Could not determine current path: %s", err)
+	}
+
+	headscaleOptions := &dockertest.RunOptions{
+		Name: headscaleHostname,
+		Mounts: []string{
+			fmt.Sprintf("%s/integration_test/etc_embedded_derp:/etc/headscale", currentPath),
+		},
+		Cmd:          []string{"headscale", "serve"},
+		ExposedPorts: []string{"8443/tcp", "3478/udp"},
+		PortBindings: map[docker.Port][]docker.PortBinding{
+			"8443/tcp": {{HostPort: "8443"}},
+			"3478/udp": {{HostPort: "3478"}},
+		},
+	}
+
+	log.Println("Creating headscale container")
+	if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
+		s.headscale = *pheadscale
+	} else {
+		log.Fatalf("Could not start resource: %s", err)
+	}
+	log.Println("Created headscale container to test DERP")
+
+	log.Println("Creating tailscale containers")
+
+	for i := 0; i < totalContainers; i++ {
+		version := tailscaleVersions[i%len(tailscaleVersions)]
+		hostname, container := s.tailscaleContainer(
+			fmt.Sprint(i),
+			version,
+			s.networks[i],
+		)
+		s.tailscales[hostname] = *container
+	}
+
+	log.Println("Waiting for headscale to be ready")
+	hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
+
+	if err := s.pool.Retry(func() error {
+		url := fmt.Sprintf("https://%s/health", hostEndpoint)
+		insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
+		insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+		client := &http.Client{Transport: insecureTransport}
+		resp, err := client.Get(url)
+		if err != nil {
+			return err
+		}
+
+		if resp.StatusCode != http.StatusOK {
+			return fmt.Errorf("status code not OK")
+		}
+
+		return nil
+	}); err != nil {
+		// TODO(kradalby): If we cannot access headscale, or any other fatal error during
+		// test setup, we need to abort and tear down. However, testify does not seem to
+		// support that at the moment:
+		// https://github.com/stretchr/testify/issues/849
+		return // fmt.Errorf("Could not connect to headscale: %s", err)
+	}
+	log.Println("headscale container is ready")
+
+	log.Printf("Creating headscale namespace: %s\n", namespaceName)
+	result, err := ExecuteCommand(
+		&s.headscale,
+		[]string{"headscale", "namespaces", "create", namespaceName},
+		[]string{},
+	)
+	log.Println("headscale create namespace result: ", result)
+	assert.Nil(s.T(), err)
+
+	log.Printf("Creating pre auth key for %s\n", namespaceName)
+	preAuthResult, err := ExecuteCommand(
+		&s.headscale,
+		[]string{
+			"headscale",
+			"--namespace",
+			namespaceName,
+			"preauthkeys",
+			"create",
+			"--reusable",
+			"--expiration",
+			"24h",
+			"--output",
+			"json",
+		},
+		[]string{"LOG_LEVEL=error"},
+	)
+	assert.Nil(s.T(), err)
+
+	var preAuthKey v1.PreAuthKey
+	err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
+	assert.Nil(s.T(), err)
+	assert.True(s.T(), preAuthKey.Reusable)
+
+	headscaleEndpoint := fmt.Sprintf("https://headscale:%s", s.headscale.GetPort("8443/tcp"))
+
+	log.Printf(
+		"Joining tailscale containers to headscale at %s\n",
+		headscaleEndpoint,
+	)
+	for hostname, tailscale := range s.tailscales {
+		s.joinWaitGroup.Add(1)
+		go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
+	}
+
+	s.joinWaitGroup.Wait()
+
+	// The nodes need a bit of time to get their updated maps from headscale
+	// TODO: See if we can have a more deterministic wait here.
+	time.Sleep(60 * time.Second)
+}
+
+func (s *IntegrationDERPTestSuite) Join(
+	endpoint, key, hostname string,
+	tailscale dockertest.Resource,
+) {
+	defer s.joinWaitGroup.Done()
+
+	command := []string{
+		"tailscale",
+		"up",
+		"-login-server",
+		endpoint,
+		"--authkey",
+		key,
+		"--hostname",
+		hostname,
+	}
+
+	log.Println("Join command:", command)
+	log.Printf("Running join command for %s\n", hostname)
+	_, err := ExecuteCommand(
+		&tailscale,
+		command,
+		[]string{},
+	)
+	assert.Nil(s.T(), err)
+	log.Printf("%s joined\n", hostname)
+}
+
+func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
+) (string, *dockertest.Resource) {
+	tailscaleBuildOptions := &dockertest.BuildOptions{
+		Dockerfile: "Dockerfile.tailscale",
+		ContextDir: ".",
+		BuildArgs: []docker.BuildArg{
+			{
+				Name:  "TAILSCALE_VERSION",
+				Value: version,
+			},
+		},
+	}
+	hostname := fmt.Sprintf(
+		"tailscale-%s-%s",
+		strings.Replace(version, ".", "-", -1),
+		identifier,
+	)
+	tailscaleOptions := &dockertest.RunOptions{
+		Name:     hostname,
+		Networks: []*dockertest.Network{&network},
+		Cmd: []string{
+			"tailscaled", "--tun=tsdev",
+		},
+
+		// expose the host IP address, so we can access it from inside the container
+		ExtraHosts: []string{"host.docker.internal:host-gateway", "headscale:host-gateway"},
+	}
+
+	pts, err := s.pool.BuildAndRunWithBuildOptions(
+		tailscaleBuildOptions,
+		tailscaleOptions,
+		DockerRestartPolicy,
+		DockerAllowLocalIPv6,
+		DockerAllowNetworkAdministration,
+	)
+	if err != nil {
+		log.Fatalf("Could not start resource: %s", err)
+	}
+	log.Printf("Created %s container\n", hostname)
+
+	return hostname, pts
+}
+
+func (s *IntegrationDERPTestSuite) TearDownSuite() {
+}
+
+func (s *IntegrationDERPTestSuite) HandleStats(
+	suiteName string,
+	stats *suite.SuiteInformation,
+) {
+	s.stats = stats
+}
+
+func (s *IntegrationDERPTestSuite) saveLog(
+	resource *dockertest.Resource,
+	basePath string,
+) error {
+	err := os.MkdirAll(basePath, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	err = s.pool.Client.Logs(
+		docker.LogsOptions{
+			Context:      context.TODO(),
+			Container:    resource.Container.ID,
+			OutputStream: &stdout,
+			ErrorStream:  &stderr,
+			Tail:         "all",
+			RawTerminal:  false,
+			Stdout:       true,
+			Stderr:       true,
+			Follow:       false,
+			Timestamps:   false,
+		},
+	)
+	if err != nil {
+		return err
+	}
+
+	log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
+
+	err = ioutil.WriteFile(
+		path.Join(basePath, resource.Container.Name+".stdout.log"),
+		[]byte(stdout.String()),
+		0o644,
+	)
+	if err != nil {
+		return err
+	}
+
+	err = ioutil.WriteFile(
+		path.Join(basePath, resource.Container.Name+".stderr.log"),
+		[]byte(stdout.String()),
+		0o644,
+	)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
+	ips, err := getIPs(s.tailscales)
+	assert.Nil(s.T(), err)
+	for hostname, tailscale := range s.tailscales {
+		for peername := range ips {
+			if peername == hostname {
+				continue
+			}
+			s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
+				command := []string{
+					"tailscale", "ping",
+					"--timeout=10s",
+					"--c=5",
+					"--until-direct=false",
+					peername,
+				}
+
+				log.Printf(
+					"Pinging using hostname from %s to %s\n",
+					hostname,
+					peername,
+				)
+				log.Println(command)
+				result, err := ExecuteCommand(
+					&tailscale,
+					command,
+					[]string{},
+				)
+				assert.Nil(t, err)
+				log.Printf("Result for %s: %s\n", hostname, result)
+				assert.Contains(t, result, "via DERP")
+			})
+		}
+	}
+}
diff --git a/integration_test/etc_embedded_derp/tls/server.crt b/integration_test/etc_embedded_derp/tls/server.crt
new file mode 100644
index 0000000..4895388
--- /dev/null
+++ b/integration_test/etc_embedded_derp/tls/server.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
+MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
+U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
+5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
+NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
+TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
+9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
+EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
+AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
+Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
+L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
+guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
+B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
+w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
+-----END CERTIFICATE-----

From 992efbd84adc8adbb3259476983876f87b552cc8 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:35:15 +0100
Subject: [PATCH 11/28] Added missing private TLS key

---
 .../etc_embedded_derp/tls/server.key          | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 integration_test/etc_embedded_derp/tls/server.key

diff --git a/integration_test/etc_embedded_derp/tls/server.key b/integration_test/etc_embedded_derp/tls/server.key
new file mode 100644
index 0000000..8a2df34
--- /dev/null
+++ b/integration_test/etc_embedded_derp/tls/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
+NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
+WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
+XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
+4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
+uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
+RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
+CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
+FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
+cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
+12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
+d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
+KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
+IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
+xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
+7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
+mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
+PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
+8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
+kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
+o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
+ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
+ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
+O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
+j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
+TDALZPOBg8VlV+HEFDP43sp9Bf0=
+-----END PRIVATE KEY-----

From e78c002f5a0c4e941d6a7b822b3b926f5c6922b4 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 19:48:30 +0100
Subject: [PATCH 12/28] Fix minor issue

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

diff --git a/derp_server.go b/derp_server.go
index d8abbc8..e900986 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -166,7 +166,7 @@ func refreshBootstrapDNS() {
 	for _, name := range names {
 		addrs, err := r.LookupIP(ctx, "ip", name)
 		if err != nil {
-			log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup %q: %v", name)
+			log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup %q", name)
 			continue
 		}
 		dnsEntries[name] = addrs

From 54c3e00a1ffb89d2b96d988f8fe21b3280a577f6 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sat, 5 Mar 2022 20:04:31 +0100
Subject: [PATCH 13/28] Merge local DERP server region with other configured
 DERP sources

---
 app.go         | 60 ++++++++------------------------------------------
 derp.go        |  1 +
 derp_server.go | 49 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 58 insertions(+), 52 deletions(-)

diff --git a/app.go b/app.go
index 4568558..8cb987a 100644
--- a/app.go
+++ b/app.go
@@ -13,7 +13,6 @@ import (
 	"os"
 	"os/signal"
 	"sort"
-	"strconv"
 	"strings"
 	"sync"
 	"syscall"
@@ -247,48 +246,6 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
 			return nil, err
 		}
 		app.DERPServer = embeddedDERPServer
-
-		serverURL, err := url.Parse(app.cfg.ServerURL)
-		if err != nil {
-			return nil, err
-		}
-		var host string
-		var port int
-		host, portStr, err := net.SplitHostPort(serverURL.Host)
-		if err != nil {
-			if serverURL.Scheme == "https" {
-				host = serverURL.Host
-				port = 443
-			} else {
-				host = serverURL.Host
-				port = 80
-			}
-		} else {
-			port, err = strconv.Atoi(portStr)
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		app.DERPMap = &tailcfg.DERPMap{
-			Regions: map[int]*tailcfg.DERPRegion{
-				999: {
-					RegionID:   999,
-					RegionCode: "headscale",
-					RegionName: "Headscale Embedded DERP",
-					Avoid:      false,
-					Nodes: []*tailcfg.DERPNode{
-						{
-							Name:     "999a",
-							RegionID: 999,
-							HostName: host,
-							DERPPort: port,
-						},
-					},
-				},
-			},
-			OmitDefaultRegions: false,
-		}
 	}
 
 	return &app, nil
@@ -536,17 +493,18 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
 func (h *Headscale) Serve() error {
 	var err error
 
+	// Fetch an initial DERP Map before we start serving
+	h.DERPMap = GetDERPMap(h.cfg.DERP)
+
 	if h.cfg.DERP.ServerEnabled {
 		go h.ServeSTUN()
-	} else {
-		// Fetch an initial DERP Map before we start serving
-		h.DERPMap = GetDERPMap(h.cfg.DERP)
+		h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
+	}
 
-		if h.cfg.DERP.AutoUpdate {
-			derpMapCancelChannel := make(chan struct{})
-			defer func() { derpMapCancelChannel <- struct{}{} }()
-			go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
-		}
+	if h.cfg.DERP.AutoUpdate {
+		derpMapCancelChannel := make(chan struct{})
+		defer func() { derpMapCancelChannel <- struct{}{} }()
+		go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel)
 	}
 
 	go h.expireEphemeralNodes(updateInterval)
diff --git a/derp.go b/derp.go
index 63e448d..7a9b236 100644
--- a/derp.go
+++ b/derp.go
@@ -148,6 +148,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
 		case <-ticker.C:
 			log.Info().Msg("Fetching DERPMap updates")
 			h.DERPMap = GetDERPMap(h.cfg.DERP)
+			h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
 
 			namespaces, err := h.ListNamespaces()
 			if err != nil {
diff --git a/derp_server.go b/derp_server.go
index e900986..81cb1e2 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"net/url"
+	"strconv"
 	"strings"
 	"sync/atomic"
 	"time"
@@ -14,6 +16,7 @@ import (
 	"github.com/rs/zerolog/log"
 	"tailscale.com/derp"
 	"tailscale.com/net/stun"
+	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
 )
 
@@ -30,12 +33,56 @@ var (
 
 type DERPServer struct {
 	tailscaleDERP *derp.Server
+	region        tailcfg.DERPRegion
 }
 
 func (h *Headscale) NewDERPServer() (*DERPServer, error) {
 	s := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
-	return &DERPServer{s}, nil
+	region, err := h.generateRegionLocalDERP()
+	if err != nil {
+		return nil, err
+	}
+	return &DERPServer{s, region}, nil
+}
 
+func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
+	serverURL, err := url.Parse(h.cfg.ServerURL)
+	if err != nil {
+		return tailcfg.DERPRegion{}, err
+	}
+	var host string
+	var port int
+	host, portStr, err := net.SplitHostPort(serverURL.Host)
+	if err != nil {
+		if serverURL.Scheme == "https" {
+			host = serverURL.Host
+			port = 443
+		} else {
+			host = serverURL.Host
+			port = 80
+		}
+	} else {
+		port, err = strconv.Atoi(portStr)
+		if err != nil {
+			return tailcfg.DERPRegion{}, err
+		}
+	}
+
+	localDERPregion := tailcfg.DERPRegion{
+		RegionID:   999,
+		RegionCode: "headscale",
+		RegionName: "Headscale Embedded DERP",
+		Avoid:      false,
+		Nodes: []*tailcfg.DERPNode{
+			{
+				Name:     "999a",
+				RegionID: 999,
+				HostName: host,
+				DERPPort: port,
+			},
+		},
+	}
+	return localDERPregion, nil
 }
 
 func (h *Headscale) DERPHandler(ctx *gin.Context) {

From 70910c459548304d00b5520525455910920dd353 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 01:23:35 +0100
Subject: [PATCH 14/28] Working /bootstrap-dns DERP helper

---
 derp_server.go | 73 +++++++++++++++-----------------------------------
 1 file changed, 22 insertions(+), 51 deletions(-)

diff --git a/derp_server.go b/derp_server.go
index 81cb1e2..dcda63e 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -2,14 +2,12 @@ package headscale
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net"
 	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
-	"sync/atomic"
 	"time"
 
 	"github.com/gin-gonic/gin"
@@ -26,11 +24,6 @@ import (
 // following its HTTP request.
 const fastStartHeader = "Derp-Fast-Start"
 
-var (
-	dnsCache     atomic.Value // of []byte
-	bootstrapDNS = "derp.tailscale.com"
-)
-
 type DERPServer struct {
 	tailscaleDERP *derp.Server
 	region        tailcfg.DERPRegion
@@ -137,14 +130,29 @@ func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
 	}
 }
 
+// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint
+// Described in https://github.com/tailscale/tailscale/issues/1405,
+// this endpoint provides a way to help a client when it fails to start up
+// because its DNS are broken.
+// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
+// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
 func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
-	ctx.Header("Content-Type", "application/json")
-	j, _ := dnsCache.Load().([]byte)
-	// Bootstrap DNS requests occur cross-regions,
-	// and are randomized per request,
-	// so keeping a connection open is pointlessly expensive.
-	ctx.Header("Connection", "close")
-	ctx.Writer.Write(j)
+	dnsEntries := make(map[string][]net.IP)
+
+	resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
+	defer cancel()
+	var r net.Resolver
+	for _, region := range h.DERPMap.Regions {
+		for _, node := range region.Nodes { // we don't care if we override some nodes
+			addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
+			if err != nil {
+				log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup failed %q", node.HostName)
+				continue
+			}
+			dnsEntries[node.HostName] = addrs
+		}
+	}
+	ctx.JSON(http.StatusOK, dnsEntries)
 }
 
 // ServeSTUN starts a STUN server on udp/3478
@@ -188,40 +196,3 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
 		pc.WriteTo(res, ua)
 	}
 }
-
-// Shamelessly taken from
-// https://github.com/tailscale/tailscale/blob/main/cmd/derper/bootstrap_dns.go
-func refreshBootstrapDNSLoop() {
-	if bootstrapDNS == "" {
-		return
-	}
-	for {
-		refreshBootstrapDNS()
-		time.Sleep(10 * time.Minute)
-	}
-}
-
-func refreshBootstrapDNS() {
-	if bootstrapDNS == "" {
-		return
-	}
-	dnsEntries := make(map[string][]net.IP)
-	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
-	defer cancel()
-	names := strings.Split(bootstrapDNS, ",")
-	var r net.Resolver
-	for _, name := range names {
-		addrs, err := r.LookupIP(ctx, "ip", name)
-		if err != nil {
-			log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup %q", name)
-			continue
-		}
-		dnsEntries[name] = addrs
-	}
-	j, err := json.MarshalIndent(dnsEntries, "", "\t")
-	if err != nil {
-		// leave the old values in place
-		return
-	}
-	dnsCache.Store(j)
-}

From dc909ba6d7ce48e0ed3d6f6415f301ef2653a903 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 16:54:19 +0100
Subject: [PATCH 15/28] Improved logging on startup

---
 cmd/headscale/cli/server.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/cmd/headscale/cli/server.go b/cmd/headscale/cli/server.go
index 6d9ad19..b741f29 100644
--- a/cmd/headscale/cli/server.go
+++ b/cmd/headscale/cli/server.go
@@ -1,7 +1,7 @@
 package cli
 
 import (
-	"log"
+	"github.com/rs/zerolog/log"
 
 	"github.com/spf13/cobra"
 )
@@ -19,12 +19,12 @@ var serveCmd = &cobra.Command{
 	Run: func(cmd *cobra.Command, args []string) {
 		h, err := getHeadscaleApp()
 		if err != nil {
-			log.Fatalf("Error initializing: %s", err)
+			log.Fatal().Caller().Err(err).Msg("Error initializing")
 		}
 
 		err = h.Serve()
 		if err != nil {
-			log.Fatalf("Error initializing: %s", err)
+			log.Fatal().Caller().Err(err).Msg("Error starting server")
 		}
 	},
 }

From eb500155e84847774d79b8454c0327e37c23c0ff Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 17:00:56 +0100
Subject: [PATCH 16/28] Make STUN server configurable

---
 app.go                     |  6 +++++-
 cmd/headscale/cli/utils.go |  4 ++++
 config-example.yaml        |  6 ++++++
 derp_server.go             | 22 ++++++++++++++++++----
 4 files changed, 33 insertions(+), 5 deletions(-)

diff --git a/app.go b/app.go
index 8cb987a..82e87cf 100644
--- a/app.go
+++ b/app.go
@@ -121,6 +121,8 @@ type OIDCConfig struct {
 
 type DERPConfig struct {
 	ServerEnabled   bool
+	STUNEnabled     bool
+	STUNAddr        string
 	URLs            []url.URL
 	Paths           []string
 	AutoUpdate      bool
@@ -497,8 +499,10 @@ func (h *Headscale) Serve() error {
 	h.DERPMap = GetDERPMap(h.cfg.DERP)
 
 	if h.cfg.DERP.ServerEnabled {
-		go h.ServeSTUN()
 		h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
+		if h.cfg.DERP.STUNEnabled {
+			go h.ServeSTUN()
+		}
 	}
 
 	if h.cfg.DERP.AutoUpdate {
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index 7277723..e6dce3a 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -118,6 +118,8 @@ func LoadConfig(path string) error {
 
 func GetDERPConfig() headscale.DERPConfig {
 	enabled := viper.GetBool("derp.server.enabled")
+	stunEnabled := viper.GetBool("derp.server.stun.enabled")
+	stunAddr := viper.GetString("derp.server.stun.listen_addr")
 
 	urlStrs := viper.GetStringSlice("derp.urls")
 
@@ -141,6 +143,8 @@ func GetDERPConfig() headscale.DERPConfig {
 
 	return headscale.DERPConfig{
 		ServerEnabled:   enabled,
+		STUNEnabled:     stunEnabled,
+		STUNAddr:        stunAddr,
 		URLs:            urls,
 		Paths:           paths,
 		AutoUpdate:      autoUpdate,
diff --git a/config-example.yaml b/config-example.yaml
index 6f4060a..57b43fd 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -60,6 +60,12 @@ derp:
     # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
     enabled: false
 
+    # If enabled, also listens in the configured address for STUN connections to help on NAT traversal
+    # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
+    stun:
+      enabled: false
+      listen_addr: "0.0.0.0:3478"
+
   # List of externally available DERP maps encoded in JSON
   urls:
     - https://controlplane.tailscale.com/derpmap/default
diff --git a/derp_server.go b/derp_server.go
index dcda63e..aeb4877 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -75,6 +75,19 @@ func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
 			},
 		},
 	}
+
+	if h.cfg.DERP.STUNEnabled {
+		_, portStr, err := net.SplitHostPort(h.cfg.DERP.STUNAddr)
+		if err != nil {
+			return tailcfg.DERPRegion{}, err
+		}
+		port, err := strconv.Atoi(portStr)
+		if err != nil {
+			return tailcfg.DERPRegion{}, err
+		}
+		localDERPregion.Nodes[0].STUNPort = port
+	}
+
 	return localDERPregion, nil
 }
 
@@ -136,6 +149,7 @@ func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
 // because its DNS are broken.
 // The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
 // They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
+// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
 func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
 	dnsEntries := make(map[string][]net.IP)
 
@@ -155,14 +169,14 @@ func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
 	ctx.JSON(http.StatusOK, dnsEntries)
 }
 
-// ServeSTUN starts a STUN server on udp/3478
+// ServeSTUN starts a STUN server on the configured addr
 func (h *Headscale) ServeSTUN() {
-	pc, err := net.ListenPacket("udp", "0.0.0.0:3478")
+	packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
 	if err != nil {
 		log.Fatal().Msgf("failed to open STUN listener: %v", err)
 	}
-	log.Trace().Msgf("STUN server started at %s", pc.LocalAddr())
-	serverSTUNListener(context.Background(), pc.(*net.UDPConn))
+	log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
+	serverSTUNListener(context.Background(), packetConn.(*net.UDPConn))
 }
 
 func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {

From eb06054a7b1e9d429b69d1c205ea81058468f60d Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 17:25:21 +0100
Subject: [PATCH 17/28] Make DERP Region configurable

---
 app.go                     | 17 ++++++++++-------
 cmd/headscale/cli/utils.go | 22 ++++++++++++++--------
 config-example.yaml        |  9 +++++++++
 derp_server.go             | 14 ++++++++------
 4 files changed, 41 insertions(+), 21 deletions(-)

diff --git a/app.go b/app.go
index 82e87cf..f1426bb 100644
--- a/app.go
+++ b/app.go
@@ -120,13 +120,16 @@ type OIDCConfig struct {
 }
 
 type DERPConfig struct {
-	ServerEnabled   bool
-	STUNEnabled     bool
-	STUNAddr        string
-	URLs            []url.URL
-	Paths           []string
-	AutoUpdate      bool
-	UpdateFrequency time.Duration
+	ServerEnabled    bool
+	ServerRegionID   int
+	ServerRegionCode string
+	ServerRegionName string
+	STUNEnabled      bool
+	STUNAddr         string
+	URLs             []url.URL
+	Paths            []string
+	AutoUpdate       bool
+	UpdateFrequency  time.Duration
 }
 
 type CLIConfig struct {
diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go
index e6dce3a..dc7a4e9 100644
--- a/cmd/headscale/cli/utils.go
+++ b/cmd/headscale/cli/utils.go
@@ -117,7 +117,10 @@ func LoadConfig(path string) error {
 }
 
 func GetDERPConfig() headscale.DERPConfig {
-	enabled := viper.GetBool("derp.server.enabled")
+	serverEnabled := viper.GetBool("derp.server.enabled")
+	serverRegionID := viper.GetInt("derp.server.region_id")
+	serverRegionCode := viper.GetString("derp.server.region_code")
+	serverRegionName := viper.GetString("derp.server.region_name")
 	stunEnabled := viper.GetBool("derp.server.stun.enabled")
 	stunAddr := viper.GetString("derp.server.stun.listen_addr")
 
@@ -142,13 +145,16 @@ func GetDERPConfig() headscale.DERPConfig {
 	updateFrequency := viper.GetDuration("derp.update_frequency")
 
 	return headscale.DERPConfig{
-		ServerEnabled:   enabled,
-		STUNEnabled:     stunEnabled,
-		STUNAddr:        stunAddr,
-		URLs:            urls,
-		Paths:           paths,
-		AutoUpdate:      autoUpdate,
-		UpdateFrequency: updateFrequency,
+		ServerEnabled:    serverEnabled,
+		ServerRegionID:   serverRegionID,
+		ServerRegionCode: serverRegionCode,
+		ServerRegionName: serverRegionName,
+		STUNEnabled:      stunEnabled,
+		STUNAddr:         stunAddr,
+		URLs:             urls,
+		Paths:            paths,
+		AutoUpdate:       autoUpdate,
+		UpdateFrequency:  updateFrequency,
 	}
 }
 
diff --git a/config-example.yaml b/config-example.yaml
index 57b43fd..1ab92dc 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -60,6 +60,15 @@ derp:
     # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
     enabled: false
 
+    # Region ID to use for the embedded DERP server.
+    # The local DERP prevails if the region ID collides with other region ID coming from
+    # the regular DERP config.
+    region_id: 999
+
+    # Region code and name are displayed in the Tailscale UI to identify a DERP region
+    region_code: "headscale"
+    region_name: "Headscale Embedded DERP"
+
     # If enabled, also listens in the configured address for STUN connections to help on NAT traversal
     # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
     stun:
diff --git a/derp_server.go b/derp_server.go
index aeb4877..8995ca8 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -62,14 +62,14 @@ func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
 	}
 
 	localDERPregion := tailcfg.DERPRegion{
-		RegionID:   999,
-		RegionCode: "headscale",
-		RegionName: "Headscale Embedded DERP",
+		RegionID:   h.cfg.DERP.ServerRegionID,
+		RegionCode: h.cfg.DERP.ServerRegionCode,
+		RegionName: h.cfg.DERP.ServerRegionName,
 		Avoid:      false,
 		Nodes: []*tailcfg.DERPNode{
 			{
-				Name:     "999a",
-				RegionID: 999,
+				Name:     fmt.Sprintf("%d", h.cfg.DERP.ServerRegionID),
+				RegionID: h.cfg.DERP.ServerRegionID,
 				HostName: host,
 				DERPPort: port,
 			},
@@ -108,6 +108,7 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
 	if !ok {
 		log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
 		ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
+
 		return
 	}
 
@@ -115,6 +116,7 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
 	if err != nil {
 		log.Error().Caller().Err(err).Msgf("Hijack failed")
 		ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
+
 		return
 	}
 
@@ -169,7 +171,7 @@ func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
 	ctx.JSON(http.StatusOK, dnsEntries)
 }
 
-// ServeSTUN starts a STUN server on the configured addr
+// ServeSTUN starts a STUN server on the configured addr.
 func (h *Headscale) ServeSTUN() {
 	packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
 	if err != nil {

From de2ea83b3b87c0d043f8dec45d788159722198f2 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 17:35:54 +0100
Subject: [PATCH 18/28] Linting here and there

---
 acls.go                     | 11 +++++------
 cmd/headscale/cli/server.go |  1 -
 derp_server.go              | 39 ++++++++++++++++++++++++-------------
 3 files changed, 31 insertions(+), 20 deletions(-)

diff --git a/acls.go b/acls.go
index 24aadf5..9548480 100644
--- a/acls.go
+++ b/acls.go
@@ -17,12 +17,11 @@ import (
 )
 
 const (
-	errEmptyPolicy        = Error("empty policy")
-	errInvalidAction      = Error("invalid action")
-	errInvalidUserSection = Error("invalid user section")
-	errInvalidGroup       = Error("invalid group")
-	errInvalidTag         = Error("invalid tag")
-	errInvalidPortFormat  = Error("invalid port format")
+	errEmptyPolicy       = Error("empty policy")
+	errInvalidAction     = Error("invalid action")
+	errInvalidGroup      = Error("invalid group")
+	errInvalidTag        = Error("invalid tag")
+	errInvalidPortFormat = Error("invalid port format")
 )
 
 const (
diff --git a/cmd/headscale/cli/server.go b/cmd/headscale/cli/server.go
index b741f29..c19580b 100644
--- a/cmd/headscale/cli/server.go
+++ b/cmd/headscale/cli/server.go
@@ -2,7 +2,6 @@ package cli
 
 import (
 	"github.com/rs/zerolog/log"
-
 	"github.com/spf13/cobra"
 )
 
diff --git a/derp_server.go b/derp_server.go
index 8995ca8..9e1b7e5 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -30,12 +30,13 @@ type DERPServer struct {
 }
 
 func (h *Headscale) NewDERPServer() (*DERPServer, error) {
-	s := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
+	server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
 	region, err := h.generateRegionLocalDERP()
 	if err != nil {
 		return nil, err
 	}
-	return &DERPServer{s, region}, nil
+
+	return &DERPServer{server, region}, nil
 }
 
 func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
@@ -99,6 +100,7 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
 			log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
 		}
 		ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
+
 		return
 	}
 
@@ -122,13 +124,14 @@ func (h *Headscale) DERPHandler(ctx *gin.Context) {
 
 	if !fastStart {
 		pubKey := h.privateKey.Public()
+		pubKeyStr := pubKey.UntypedHexString() // nolint
 		fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
 			"Upgrade: DERP\r\n"+
 			"Connection: Upgrade\r\n"+
 			"Derp-Version: %v\r\n"+
 			"Derp-Public-Key: %s\r\n\r\n",
 			derp.ProtocolVersion,
-			pubKey.UntypedHexString())
+			pubKeyStr)
 	}
 
 	h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
@@ -163,6 +166,7 @@ func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
 			addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
 			if err != nil {
 				log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup failed %q", node.HostName)
+
 				continue
 			}
 			dnsEntries[node.HostName] = addrs
@@ -178,28 +182,34 @@ func (h *Headscale) ServeSTUN() {
 		log.Fatal().Msgf("failed to open STUN listener: %v", err)
 	}
 	log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
-	serverSTUNListener(context.Background(), packetConn.(*net.UDPConn))
+
+	udpConn, ok := packetConn.(*net.UDPConn)
+	if !ok {
+		log.Fatal().Msg("STUN listener is not a UDP listener")
+	}
+	serverSTUNListener(context.Background(), udpConn)
 }
 
-func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
+func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
 	var buf [64 << 10]byte
 	var (
-		n   int
-		ua  *net.UDPAddr
-		err error
+		bytesRead int
+		udpAddr   *net.UDPAddr
+		err       error
 	)
 	for {
-		n, ua, err = pc.ReadFromUDP(buf[:])
+		bytesRead, udpAddr, err = packetConn.ReadFromUDP(buf[:])
 		if err != nil {
 			if ctx.Err() != nil {
 				return
 			}
 			log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
 			time.Sleep(time.Second)
+
 			continue
 		}
-		log.Trace().Caller().Msgf("STUN request from %v", ua)
-		pkt := buf[:n]
+		log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
+		pkt := buf[:bytesRead]
 		if !stun.Is(pkt) {
 			continue
 		}
@@ -208,7 +218,10 @@ func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
 			continue
 		}
 
-		res := stun.Response(txid, ua.IP, uint16(ua.Port))
-		pc.WriteTo(res, ua)
+		res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
+		_, err = packetConn.WriteTo(res, udpAddr)
+		if err != nil {
+			continue
+		}
 	}
 }

From e1fcf0da262244ccde3f6c9117acf50bd326c532 Mon Sep 17 00:00:00 2001
From: Juan Font <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 20:40:55 +0100
Subject: [PATCH 19/28] Added more version

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

diff --git a/integration_common_test.go b/integration_common_test.go
index a341712..70285fc 100644
--- a/integration_common_test.go
+++ b/integration_common_test.go
@@ -20,7 +20,7 @@ var (
 	IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
 	IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
 
-	tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
+	tailscaleVersions = []string{"1.22.0", "1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
 )
 
 type TestNamespace struct {

From b47de07eea7bf0ec637f24bb5ad0c1aad9ab2961 Mon Sep 17 00:00:00 2001
From: Juan Font <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 20:42:27 +0100
Subject: [PATCH 20/28] Update Dockerfile.tailscale

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

diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale
index fded837..32a8ce7 100644
--- a/Dockerfile.tailscale
+++ b/Dockerfile.tailscale
@@ -13,4 +13,4 @@ RUN apt-get update \
 ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
 RUN chmod 644 /usr/local/share/ca-certificates/server.crt 
 
-RUN update-ca-certificates
\ No newline at end of file
+RUN update-ca-certificates

From 580db9b58fe786fc23d1506ab3907dc8d4f5235d Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 23:19:21 +0100
Subject: [PATCH 21/28] Mention that STUN is UDP

---
 config-example.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config-example.yaml b/config-example.yaml
index 1ab92dc..2075e69 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -69,7 +69,7 @@ derp:
     region_code: "headscale"
     region_name: "Headscale Embedded DERP"
 
-    # If enabled, also listens in the configured address for STUN connections to help on NAT traversal
+    # If enabled, also listens in UDP at the configured address for STUN connections to help on NAT traversal
     # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
     stun:
       enabled: false

From a27b386123a815f9841b7109e01349e55f91c723 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 23:45:01 +0100
Subject: [PATCH 22/28] Clarified expiration dates

---
 integration_test/etc_embedded_derp/tls/server.crt | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/integration_test/etc_embedded_derp/tls/server.crt b/integration_test/etc_embedded_derp/tls/server.crt
index 4895388..9555649 100644
--- a/integration_test/etc_embedded_derp/tls/server.crt
+++ b/integration_test/etc_embedded_derp/tls/server.crt
@@ -1,3 +1,4 @@
+
 -----BEGIN CERTIFICATE-----
 MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
 BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
@@ -16,3 +17,6 @@ guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
 B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
 w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
 -----END CERTIFICATE-----
+
+(Expires on Nov  4 16:48:03 2521 GMT)
+

From b3fa66dbd2aebf4faf048bab563e1a5dad952ad7 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 23:46:16 +0100
Subject: [PATCH 23/28] Check for DERP in test

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

diff --git a/integration_embedded_derp_test.go b/integration_embedded_derp_test.go
index d95c460..e68da01 100644
--- a/integration_embedded_derp_test.go
+++ b/integration_embedded_derp_test.go
@@ -377,7 +377,7 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
 				)
 				assert.Nil(t, err)
 				log.Printf("Result for %s: %s\n", hostname, result)
-				assert.Contains(t, result, "via DERP")
+				assert.Contains(t, result, "via DERP(headscale)")
 			})
 		}
 	}

From 05df8e947aa61a87d9e49086a0b2d44fb47d3ccd Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Sun, 6 Mar 2022 23:47:14 +0100
Subject: [PATCH 24/28] Added missing file

---
 .../etc_embedded_derp/config.yaml             | 30 +++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 integration_test/etc_embedded_derp/config.yaml

diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
new file mode 100644
index 0000000..00f6332
--- /dev/null
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -0,0 +1,30 @@
+log_level: trace
+acl_policy_path: ""
+db_type: sqlite3
+ephemeral_node_inactivity_timeout: 30m
+ip_prefixes:
+  - fd7a:115c:a1e0::/48
+  - 100.64.0.0/10
+dns_config:
+  base_domain: headscale.net
+  magic_dns: true
+  domains: []
+  nameservers:
+    - 1.1.1.1
+db_path: /tmp/integration_test_db.sqlite3
+private_key_path: private.key
+listen_addr: 0.0.0.0:8443
+server_url: https://headscale:8443
+tls_cert_path: "/etc/headscale/tls/server.crt"
+tls_key_path: "/etc/headscale/tls/server.key"
+tls_client_auth_mode: disabled
+
+derp:
+  server:
+    enabled: true
+    region_id: 999
+    region_code: "headscale"
+    region_name: "Headscale Embedded DERP"
+    stun:
+      enabled: true
+      listen_addr: "0.0.0.0:3478"
\ No newline at end of file

From 03452a8dca9a4b6d956cd319b8d07c216bcb64db Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Mon, 7 Mar 2022 00:29:40 +0100
Subject: [PATCH 25/28] Prettied

---
 integration_test/etc_embedded_derp/config.yaml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index 00f6332..6e5291f 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -18,7 +18,6 @@ server_url: https://headscale:8443
 tls_cert_path: "/etc/headscale/tls/server.crt"
 tls_key_path: "/etc/headscale/tls/server.key"
 tls_client_auth_mode: disabled
-
 derp:
   server:
     enabled: true

From cc0c88a63ab193dc075bf0cac4962025bfccab04 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 8 Mar 2022 12:11:51 +0100
Subject: [PATCH 26/28] Added small integration test for stun

---
 derp_server.go                    |  6 ++++++
 go.mod                            |  1 +
 go.sum                            |  2 ++
 integration_embedded_derp_test.go | 12 ++++++++++++
 4 files changed, 21 insertions(+)

diff --git a/derp_server.go b/derp_server.go
index 9e1b7e5..11e3eb1 100644
--- a/derp_server.go
+++ b/derp_server.go
@@ -211,16 +211,22 @@ func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
 		log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
 		pkt := buf[:bytesRead]
 		if !stun.Is(pkt) {
+			log.Trace().Caller().Msgf("UDP packet is not STUN")
+
 			continue
 		}
 		txid, err := stun.ParseBindingRequest(pkt)
 		if err != nil {
+			log.Trace().Caller().Err(err).Msgf("STUN parse error")
+
 			continue
 		}
 
 		res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
 		_, err = packetConn.WriteTo(res, udpAddr)
 		if err != nil {
+			log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
+
 			continue
 		}
 	}
diff --git a/go.mod b/go.mod
index d6754f9..1ec291c 100644
--- a/go.mod
+++ b/go.mod
@@ -49,6 +49,7 @@ require (
 	github.com/akutz/memconn v0.1.0 // indirect
 	github.com/atomicgo/cursor v0.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.2 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/containerd/continuity v0.2.2 // indirect
diff --git a/go.sum b/go.sum
index c23db38..6d254a2 100644
--- a/go.sum
+++ b/go.sum
@@ -108,6 +108,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/bufbuild/buf v0.37.0/go.mod h1:lQ1m2HkIaGOFba6w/aC3KYBHhKEOESP3gaAEpS3dAFM=
+github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 h1:POmUHfxXdeyM8Aomg4tKDcwATCFuW+cYLkj6pwsw9pc=
+github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029/go.mod h1:Rpr5n9cGHYdM3S3IK8ROSUUUYjQOu+MSUCZDcJbYWi8=
 github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
 github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/integration_embedded_derp_test.go b/integration_embedded_derp_test.go
index e68da01..a173717 100644
--- a/integration_embedded_derp_test.go
+++ b/integration_embedded_derp_test.go
@@ -23,6 +23,8 @@ import (
 	"github.com/ory/dockertest/v3/docker"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
+
+	"github.com/ccding/go-stun/stun"
 )
 
 const (
@@ -382,3 +384,13 @@ func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
 		}
 	}
 }
+
+func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
+	headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
+	client := stun.NewClient()
+	client.SetVerbose(true)
+	client.SetVVerbose(true)
+	client.SetServerAddr(headscaleSTUNAddr)
+	_, _, err := client.Discover()
+	assert.Nil(s.T(), err)
+}

From 05c5e2280b5e6fbe801195c3d1f1575d9f63b434 Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 8 Mar 2022 12:15:05 +0100
Subject: [PATCH 27/28] Updated CHANGELOG and README

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8535b45..6ce0c35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
 - Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
 - Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
 - Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
+- Added embedded DERP server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
 
 ### Changes
 
diff --git a/README.md b/README.md
index 1b97b1a..ec66b08 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ one of the maintainers.
 - Dual stack (IPv4 and IPv6)
 - Routing advertising (including exit nodes)
 - Ephemeral nodes
+- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
 
 ## Client OS support
 

From b803240dc1045e16da2bc79337c1bd33f8941bbb Mon Sep 17 00:00:00 2001
From: Juan Font Alonso <juanfontalonso@gmail.com>
Date: Tue, 8 Mar 2022 12:21:08 +0100
Subject: [PATCH 28/28] Added new line for prettier

---
 integration_test/etc_embedded_derp/config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml
index 6e5291f..1531d34 100644
--- a/integration_test/etc_embedded_derp/config.yaml
+++ b/integration_test/etc_embedded_derp/config.yaml
@@ -26,4 +26,4 @@ derp:
     region_name: "Headscale Embedded DERP"
     stun:
       enabled: true
-      listen_addr: "0.0.0.0:3478"
\ No newline at end of file
+      listen_addr: "0.0.0.0:3478"