From 897d480f4dd723ad0c197e9c9a782da8a4503c7f Mon Sep 17 00:00:00 2001 From: Juan Font Alonso 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 58185093..ee38ed93 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 97c2440e..cff31f32 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 00000000..0631fff8 --- /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 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 a121826d..d6754f98 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 558d3d7c..c23db380 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 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 3115b994..eb3b8ade 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 0631fff8..1d4fb0b5 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 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 de304d0d..de9fdd99 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 03d6d2f2..523a9a99 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 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 de9fdd99..a3417121 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 523a9a99..1649f322 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 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 eb3b8ade..b739a052 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 98ffe2ec..06b9ca98 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 c28b6089..84b1c906 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 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 b739a052..34602d60 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 06b9ca98..7277723f 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 84b1c906..08cc6c12 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 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 1d4fb0b5..d8abbc81 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 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 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 057a20e7..e3acf996 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 f96c6b9c..fded8375 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 266dadb8..73630d3f 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 00000000..d95c460b --- /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 00000000..48953883 --- /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 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 00000000..8a2df34b --- /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 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 d8abbc81..e9009860 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 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 4568558d..8cb987a9 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 63e448db..7a9b2367 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 e9009860..81cb1e29 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 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 81cb1e29..dcda63e9 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 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 6d9ad194..b741f295 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 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 8cb987a9..82e87cf0 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 7277723f..e6dce3a1 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 6f4060ac..57b43fd4 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 dcda63e9..aeb4877a 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 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 82e87cf0..f1426bbb 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 e6dce3a1..dc7a4e9f 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 57b43fd4..1ab92dca 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 aeb4877a..8995ca8b 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 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 24aadf5b..95484806 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 b741f295..c19580b9 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 8995ca8b..9e1b7e54 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 Date: Sun, 6 Mar 2022 20:40:55 +0100 Subject: [PATCH 19/28] Added more version Co-authored-by: Kristoffer Dalby --- 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 a3417121..70285fc4 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 Date: Sun, 6 Mar 2022 20:42:27 +0100 Subject: [PATCH 20/28] Update Dockerfile.tailscale Co-authored-by: Kristoffer Dalby --- Dockerfile.tailscale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale index fded8375..32a8ce7b 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 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 1ab92dca..2075e69a 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 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 48953883..95556495 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 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 d95c460b..e68da013 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 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 00000000..00f63325 --- /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 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 00f63325..6e5291fa 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 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 9e1b7e54..11e3eb14 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 d6754f98..1ec291c3 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 c23db380..6d254a25 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 e68da013..a1737173 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 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 8535b45f..6ce0c35b 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 1b97b1aa..ec66b086 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 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 6e5291fa..1531d347 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"