mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	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.
This commit is contained in:
		
							parent
							
								
									ccec534e19
								
							
						
					
					
						commit
						897d480f4d
					
				
							
								
								
									
										58
									
								
								app.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								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)
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										178
									
								
								derp_embedded.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								derp_embedded.go
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user