Skip to content

Commit

Permalink
Add DERP generate_204 endpoint for captive portal detection.
Browse files Browse the repository at this point in the history
  • Loading branch information
tomtom5152 committed Nov 14, 2023
1 parent 2af71c9 commit dad1315
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 0 deletions.
1 change: 1 addition & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
router.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.DERPMap))
router.HandleFunc("/generate_204", derpServer.DERPNoContextHandler)
}

apiRouter := router.PathPrefix("/api").Subrouter()
Expand Down
41 changes: 41 additions & 0 deletions hscontrol/derp/server/derp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import (
// headers and it will begin writing & reading the DERP protocol immediately
// following its HTTP request.
const fastStartHeader = "Derp-Fast-Start"
const (
noContentChallengeHeader = "X-Tailscale-Challenge"
noContentResponseHeader = "X-Tailscale-Response"
)

type DERPServer struct {
serverURL string
Expand Down Expand Up @@ -200,6 +204,43 @@ func DERPProbeHandler(
}
}

// DERPNoContextHandler is the endpoint clients use to determine if they are behind a captive portal
// Clients challenge this with the X-Tailscale-Challenge header and expect the challenge value within X-Tailscale-Response
// https://github.com/tailscale/tailscale/blob/955e2fcbfb4fe7ff9b8dbd665ba24ef2008c676e/cmd/derper/derper.go#L324
func DERPNoContextHandler(
writer http.ResponseWriter,
req *http.Request,
) {
switch req.Method {
case http.MethodHead, http.MethodGet:
if challenge := req.Header.Get(noContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
writer.Header().Set(noContentResponseHeader, "response "+challenge)
}
}
writer.WriteHeader(http.StatusNoContent)
default:
writer.WriteHeader(http.StatusMethodNotAllowed)
_, err := writer.Write([]byte("bogus captive portal method"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}
}

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 == '_'
}

// DERPBootstrapDNSHandler implements the /bootsrap-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
Expand Down

0 comments on commit dad1315

Please sign in to comment.