-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add an embedded DERP server to Headscale #388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
897d480
Add an embedded DERP server to Headscale
juanfont 9d43f58
Added missing deps
juanfont 23cde84
Merge branch 'main' into embedded-derp
juanfont 607c1eb
Be consistent with uppercase DERP
juanfont 22d2443
Move more stuff to common
juanfont 09d78c7
Even more stuff moved to common
juanfont 758b1ba
Renamed configuration items of the DERP server
juanfont df37d1a
Do not offer the option to be DERP insecure
juanfont b742379
Do not use the term embedded
juanfont 88378c2
Rename the file to derp_server.go for coherence
juanfont e9eb90f
Added integration tests for the embedded DERP server
juanfont 992efbd
Added missing private TLS key
juanfont 237f7f1
Merge branch 'main' into embedded-derp
juanfont e78c002
Fix minor issue
juanfont 54c3e00
Merge local DERP server region with other configured DERP sources
juanfont 70910c4
Working /bootstrap-dns DERP helper
juanfont dc909ba
Improved logging on startup
juanfont eb50015
Make STUN server configurable
juanfont eb06054
Make DERP Region configurable
juanfont de2ea83
Linting here and there
juanfont e1fcf0d
Added more version
juanfont b47de07
Update Dockerfile.tailscale
juanfont 580db9b
Mention that STUN is UDP
juanfont a27b386
Clarified expiration dates
juanfont b3fa66d
Check for DERP in test
juanfont 05df8e9
Added missing file
juanfont 15ed713
Merge branch 'embedded-derp' of https://github.com/juanfont/headscale…
juanfont 03452a8
Prettied
juanfont dd26cbd
Merge branch 'main' into embedded-derp
kradalby cc0c88a
Added small integration test for stun
juanfont b41d899
Merge branch 'embedded-derp' of https://github.com/juanfont/headscale…
juanfont 05c5e22
Updated CHANGELOG and README
juanfont e5d22b8
Merge branch 'main' into embedded-derp
juanfont bdbf620
Merge branch 'embedded-derp' of https://github.com/juanfont/headscale…
juanfont b803240
Added new line for prettier
juanfont File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"+ | ||
juanfont marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "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") | ||
juanfont marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.