mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 12:58:37 +00:00

Observed on some airlines (British Airways, WestJet), Squid is configured to cache and transform these results, which is disruptive. The server and client should both actively request that this is not done by setting Cache-Control headers. Send a timestamp parameter to further work against caches that do not respect the cache-control headers. Updates #14856 Signed-off-by: James Tucker <james@tailscale.com>
116 lines
3.4 KiB
Go
116 lines
3.4 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package derphttp
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"tailscale.com/derp"
|
|
)
|
|
|
|
// 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"
|
|
|
|
// Handler returns an http.Handler to be mounted at /derp, serving s.
|
|
func Handler(s *derp.Server) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// These are installed both here and in cmd/derper. The check here
|
|
// catches both cmd/derper run with DERP disabled (STUN only mode) as
|
|
// well as DERP being run in tests with derphttp.Handler directly,
|
|
// as netcheck still assumes this replies.
|
|
switch r.URL.Path {
|
|
case "/derp/probe", "/derp/latency-check":
|
|
ProbeHandler(w, r)
|
|
return
|
|
}
|
|
|
|
up := strings.ToLower(r.Header.Get("Upgrade"))
|
|
if up != "websocket" && up != "derp" {
|
|
if up != "" {
|
|
log.Printf("Weird upgrade: %q", up)
|
|
}
|
|
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
|
return
|
|
}
|
|
|
|
fastStart := r.Header.Get(fastStartHeader) == "1"
|
|
|
|
h, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
http.Error(w, "HTTP does not support general TCP support", 500)
|
|
return
|
|
}
|
|
|
|
netConn, conn, err := h.Hijack()
|
|
if err != nil {
|
|
log.Printf("Hijack failed: %v", err)
|
|
http.Error(w, "HTTP does not support general TCP support", 500)
|
|
return
|
|
}
|
|
|
|
if !fastStart {
|
|
pubKey := s.PublicKey()
|
|
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())
|
|
}
|
|
|
|
if v := r.Header.Get(derp.IdealNodeHeader); v != "" {
|
|
ctx = derp.IdealNodeContextKey.WithValue(ctx, v)
|
|
}
|
|
|
|
s.Accept(ctx, netConn, conn, netConn.RemoteAddr().String())
|
|
})
|
|
}
|
|
|
|
// ProbeHandler is the endpoint that clients without UDP access (including js/wasm) hit to measure
|
|
// DERP latency, as a replacement for UDP STUN queries.
|
|
func ProbeHandler(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case "HEAD", "GET":
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
default:
|
|
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// ServeNoContent generates the /generate_204 response used by Tailscale's
|
|
// captive portal detection.
|
|
func ServeNoContent(w http.ResponseWriter, r *http.Request) {
|
|
if challenge := r.Header.Get(NoContentChallengeHeader); challenge != "" {
|
|
badChar := strings.IndexFunc(challenge, func(r rune) bool {
|
|
return !isChallengeChar(r)
|
|
}) != -1
|
|
if len(challenge) <= 64 && !badChar {
|
|
w.Header().Set(NoContentResponseHeader, "response "+challenge)
|
|
}
|
|
}
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func isChallengeChar(c rune) bool {
|
|
// Semi-randomly chosen as a limited set of valid characters
|
|
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
|
('0' <= c && c <= '9') ||
|
|
c == '.' || c == '-' || c == '_' || c == ':'
|
|
}
|
|
|
|
const (
|
|
NoContentChallengeHeader = "X-Tailscale-Challenge"
|
|
NoContentResponseHeader = "X-Tailscale-Response"
|
|
)
|