package hscontrol import ( "bytes" "encoding/json" "errors" "html/template" "net/http" "strconv" "time" "github.com/gorilla/mux" "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "tailscale.com/tailcfg" "tailscale.com/types/key" ) const ( // The CapabilityVersion is used by Tailscale clients to indicate // their codebase version. Tailscale clients can communicate over TS2021 // from CapabilityVersion 28, but we only have good support for it // since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port). // // Related to this change, there is https://github.com/tailscale/tailscale/pull/5379, // where CapabilityVersion 39 is introduced to indicate #4323 was merged. // // See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go NoiseCapabilityVersion = 39 // TODO(juan): remove this once https://github.com/juanfont/headscale/issues/727 is fixed. registrationHoldoff = time.Second * 5 reservedResponseHeaderSize = 4 ) var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New( "machines registered with CLI does not support expire", ) // KeyHandler provides the Headscale pub key // Listens in /key. func (h *Headscale) KeyHandler( writer http.ResponseWriter, req *http.Request, ) { // New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion clientCapabilityStr := req.URL.Query().Get("v") if clientCapabilityStr != "" { log.Debug(). Str("handler", "/key"). Str("v", clientCapabilityStr). Msg("New noise client") clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr) if err != nil { writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusBadRequest) _, err := writer.Write([]byte("Wrong params")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } // TS2021 (Tailscale v2 protocol) requires to have a different key if clientCapabilityVersion >= NoiseCapabilityVersion { resp := tailcfg.OverTLSPublicKeyResponse{ LegacyPublicKey: h.privateKey2019.Public(), PublicKey: h.noisePrivateKey.Public(), } writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) err = json.NewEncoder(writer).Encode(resp) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } } log.Debug(). Str("handler", "/key"). Msg("New legacy client") // Old clients don't send a 'v' parameter, so we send the legacy public key writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err := writer.Write([]byte(util.MachinePublicKeyStripPrefix(h.privateKey2019.Public()))) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } } func (h *Headscale) HealthHandler( writer http.ResponseWriter, req *http.Request, ) { respond := func(err error) { writer.Header().Set("Content-Type", "application/health+json; charset=utf-8") res := struct { Status string `json:"status"` }{ Status: "pass", } if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Error().Caller().Err(err).Msg("health check failed") res.Status = "fail" } buf, err := json.Marshal(res) if err != nil { log.Error().Caller().Err(err).Msg("marshal failed") } _, err = writer.Write(buf) if err != nil { log.Error().Caller().Err(err).Msg("write failed") } } if err := h.db.PingDB(req.Context()); err != nil { respond(err) return } respond(nil) } type registerWebAPITemplateConfig struct { Key string } var registerWebAPITemplate = template.Must( template.New("registerweb").Parse(`
Run the command below in the headscale server to add this machine to your network:
headscale nodes register --user USERNAME --key {{.Key}}
`))
// RegisterWebAPI shows a simple message in the browser to point to the CLI
// Listens in /register/:nkey.
//
// This is not part of the Tailscale control API, as we could send whatever URL
// in the RegisterResponse.AuthURL field.
func (h *Headscale) RegisterWebAPI(
writer http.ResponseWriter,
req *http.Request,
) {
vars := mux.Vars(req)
nodeKeyStr, ok := vars["nkey"]
if !util.NodePublicKeyRegex.Match([]byte(nodeKeyStr)) {
log.Warn().Str("node_key", nodeKeyStr).Msg("Invalid node key passed to registration url")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusUnauthorized)
_, err := writer.Write([]byte("Unauthorized"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
// We need to make sure we dont open for XSS style injections, if the parameter that
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
// the template and log an error.
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(util.NodePublicKeyEnsurePrefix(nodeKeyStr)),
)
if !ok || nodeKeyStr == "" || err != nil {
log.Warn().Err(err).Msg("Failed to parse incoming nodekey")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
_, err := writer.Write([]byte("Wrong params"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
var content bytes.Buffer
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
Key: nodeKeyStr,
}); err != nil {
log.Error().
Str("func", "RegisterWebAPI").
Err(err).
Msg("Could not render register web API template")
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, err = writer.Write([]byte("Could not render register web API template"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
return
}
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, err = writer.Write(content.Bytes())
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}