mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-27 12:05:26 +00:00
228 lines
5.5 KiB
Go
228 lines
5.5 KiB
Go
package headscale
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rs/zerolog/log"
|
|
"tailscale.com/derp"
|
|
"tailscale.com/net/stun"
|
|
"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"
|
|
|
|
var (
|
|
dnsCache atomic.Value // of []byte
|
|
bootstrapDNS = "derp.tailscale.com"
|
|
)
|
|
|
|
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)
|
|
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) {
|
|
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 != "" {
|
|
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.DERPServer.tailscaleDERP.Accept(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 (h *Headscale) DERPProbeHandler(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) 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)
|
|
}
|
|
|
|
// 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.Trace().Msgf("STUN server started at %s", 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.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
|
|
}
|
|
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.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)
|
|
}
|