Files
headscale/hscontrol/derp/server/derp_server.go
T
Kristoffer Dalby 8f314797ce all: fix full-tree golangci-lint findings
The new full-tree golangci-lint check reports issues the --new-from-rev
diff lint hid: nine wsl_v5 whitespace gaps, a prealloc, and an unparam
(setCSRFCookie never errored, so drop the return and update callers).
gocyclo on the central UpdateNodeFromMapRequest and an SA1019 NetMap
deprecation in an integration helper are suppressed with reasons.
2026-06-20 21:08:01 +02:00

446 lines
12 KiB
Go

package server
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/coder/websocket"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/derp/derpserver"
"tailscale.com/envknob"
"tailscale.com/net/stun"
"tailscale.com/net/wsconn"
"tailscale.com/tailcfg"
"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"
DerpVerifyScheme = "headscale-derp-verify"
)
// debugUseDERPIP is a debug-only flag that causes the DERP server to resolve
// hostnames to IP addresses when generating the DERP region configuration.
// This is useful for integration testing where DNS resolution may be unreliable.
var debugUseDERPIP = envknob.Bool("HEADSCALE_DEBUG_DERP_USE_IP")
type DERPServer struct {
serverURL string
key key.NodePrivate
cfg *types.DERPConfig
tailscaleDERP *derpserver.Server
}
func NewDERPServer(
serverURL string,
derpKey key.NodePrivate,
cfg *types.DERPConfig,
) (*DERPServer, error) {
log.Trace().Caller().Msg("creating new embedded DERP server")
server := derpserver.New(derpKey, util.TSLogfWrapper()) // nolint // zerolinter complains
if cfg.ServerVerifyClients {
server.SetVerifyClientURL(DerpVerifyScheme + "://verify")
server.SetVerifyClientURLFailOpen(false)
}
return &DERPServer{
serverURL: serverURL,
key: derpKey,
cfg: cfg,
tailscaleDERP: server,
}, nil
}
func (d *DERPServer) GenerateRegion() (tailcfg.DERPRegion, error) {
serverURL, err := url.Parse(d.serverURL)
if err != nil {
return tailcfg.DERPRegion{}, err
}
// Extract hostname and port from URL
host, portStr, err := net.SplitHostPort(serverURL.Host)
var port int
if err != nil {
host = serverURL.Host
if serverURL.Scheme == "https" {
port = 443
} else {
port = 80
}
} else {
port, err = strconv.Atoi(portStr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
}
// If debug flag is set, resolve hostname to IP address
if debugUseDERPIP {
ips, err := new(net.Resolver).LookupIPAddr(context.Background(), host)
if err != nil {
log.Error().Caller().Err(err).Msgf("failed to resolve DERP hostname %s to IP, using hostname", host)
} else if len(ips) > 0 {
// Use the first IP address
ipStr := ips[0].IP.String()
log.Info().Caller().Msgf("HEADSCALE_DEBUG_DERP_USE_IP: resolved %s to %s", host, ipStr)
host = ipStr
}
}
localDERPregion := tailcfg.DERPRegion{
RegionID: d.cfg.ServerRegionID,
RegionCode: d.cfg.ServerRegionCode,
RegionName: d.cfg.ServerRegionName,
Avoid: false,
Nodes: []*tailcfg.DERPNode{
{
Name: strconv.Itoa(d.cfg.ServerRegionID),
RegionID: d.cfg.ServerRegionID,
HostName: host,
DERPPort: port,
IPv4: d.cfg.IPv4,
IPv6: d.cfg.IPv6,
},
},
}
_, portSTUNStr, err := net.SplitHostPort(d.cfg.STUNAddr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
portSTUN, err := strconv.Atoi(portSTUNStr)
if err != nil {
return tailcfg.DERPRegion{}, err
}
localDERPregion.Nodes[0].STUNPort = portSTUN
log.Info().Caller().Msgf("derp region: %+v", localDERPregion)
log.Info().Caller().Msgf("derp nodes[0]: %+v", localDERPregion.Nodes[0])
return localDERPregion, nil
}
func (d *DERPServer) DERPHandler(
writer http.ResponseWriter,
req *http.Request,
) {
log.Trace().Caller().Msgf("/derp request from %v", req.RemoteAddr)
upgrade := strings.ToLower(req.Header.Get("Upgrade"))
if upgrade != "websocket" && upgrade != "derp" {
if upgrade != "" {
log.Warn().
Caller().
Msg("No Upgrade header in DERP server request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.")
}
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusUpgradeRequired)
_, err := writer.Write([]byte("DERP requires connection upgrade"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
return
}
if strings.Contains(req.Header.Get("Sec-Websocket-Protocol"), "derp") {
d.serveWebsocket(writer, req)
} else {
d.servePlain(writer, req)
}
}
func (d *DERPServer) serveWebsocket(writer http.ResponseWriter, req *http.Request) {
websocketConn, err := websocket.Accept(writer, req, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because DERP transmits WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to upgrade websocket request")
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)
_, err = writer.Write([]byte("Failed to upgrade websocket request"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
return
}
defer websocketConn.Close(websocket.StatusInternalError, "closing")
if websocketConn.Subprotocol() != "derp" {
websocketConn.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
return
}
wc := wsconn.NetConn(req.Context(), websocketConn, websocket.MessageBinary, req.RemoteAddr)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
d.tailscaleDERP.Accept(req.Context(), wc, brw, req.RemoteAddr)
}
func (d *DERPServer) servePlain(writer http.ResponseWriter, req *http.Request) {
fastStart := req.Header.Get(fastStartHeader) == "1"
hijacker, ok := writer.(http.Hijacker)
if !ok {
log.Error().Caller().Msg("derp requires Hijacker interface from Gin")
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)
_, err := writer.Write([]byte("HTTP does not support general TCP support"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
return
}
netConn, conn, err := hijacker.Hijack()
if err != nil {
log.Error().Caller().Err(err).Msgf("hijack failed")
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)
_, err = writer.Write([]byte("HTTP does not support general TCP support"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
return
}
log.Trace().Caller().Msgf("hijacked connection from %v", req.RemoteAddr)
if !fastStart {
pubKey := d.key.Public()
pubKeyStr, _ := pubKey.MarshalText() //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,
string(pubKeyStr))
}
d.tailscaleDERP.Accept(req.Context(), netConn, conn, netConn.RemoteAddr().String())
}
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func DERPProbeHandler(
writer http.ResponseWriter,
req *http.Request,
) {
switch req.Method {
case http.MethodHead, http.MethodGet:
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.WriteHeader(http.StatusOK)
default:
writer.WriteHeader(http.StatusMethodNotAllowed)
_, err := writer.Write([]byte("bogus probe method"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
}
}
// DERPBootstrapDNSHandler implements the /bootstrap-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.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
func DERPBootstrapDNSHandler(
derpMap tailcfg.DERPMapView,
) func(http.ResponseWriter, *http.Request) {
return func(
writer http.ResponseWriter,
req *http.Request,
) {
dnsEntries := make(map[string][]net.IP)
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
defer cancel()
var resolver net.Resolver
for _, region := range derpMap.Regions().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
for _, node := range region.Nodes().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
addrs, err := resolver.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
}
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err := json.NewEncoder(writer).Encode(dnsEntries)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write HTTP response")
}
}
}
// ServeSTUN starts a STUN server on the configured addr.
func (d *DERPServer) ServeSTUN() {
packetConn, err := new(net.ListenConfig).ListenPacket(context.Background(), "udp", d.cfg.STUNAddr)
if err != nil {
log.Fatal().Msgf("failed to open STUN listener: %v", err)
}
log.Info().Msgf("stun server started at %s", packetConn.LocalAddr())
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, packetConn *net.UDPConn) {
var buf [64 << 10]byte
for {
bytesRead, udpAddr, err := packetConn.ReadFromUDP(buf[:])
if err != nil {
if ctx.Err() != nil {
return
}
log.Error().Caller().Err(err).Msgf("stun ReadFrom")
// Rate limit error logging - wait before retrying, but respect context cancellation
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
}
continue
}
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
}
addr, _ := netip.AddrFromSlice(udpAddr.IP)
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(udpAddr.Port))) //nolint:gosec // port is always <=65535
_, err = packetConn.WriteTo(res, udpAddr)
if err != nil {
log.Trace().Caller().Err(err).Msgf("issue writing to UDP")
continue
}
}
}
func NewDERPVerifyTransport(handleVerifyRequest func(*http.Request, io.Writer) error) *DERPVerifyTransport {
return &DERPVerifyTransport{
handleVerifyRequest: handleVerifyRequest,
}
}
type DERPVerifyTransport struct {
handleVerifyRequest func(*http.Request, io.Writer) error
}
func (t *DERPVerifyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
buf := new(bytes.Buffer)
err := t.handleVerifyRequest(req, buf)
if err != nil {
log.Error().Caller().Err(err).Msg("failed to handle client verify request")
return nil, err
}
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(buf),
}
return resp, nil
}