mirror of
https://github.com/juanfont/headscale.git
synced 2025-02-08 10:18:01 +09:00
45752db0f6
Some checks are pending
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=386 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=5) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=6) (push) Waiting to run
Build / build-cross (GOARCH=arm GOOS=linux GOARM=7) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Tests / test (push) Waiting to run
* add dedicated http error to propagate to user Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * classify user errors in http handlers Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * move validation of pre auth key out of db This move separates the logic a bit and allow us to write specific errors for the caller, in this case the web layer so we can present the user with the correct error codes without bleeding web stuff into a generic validate. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update changelog Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
232 lines
6.6 KiB
Go
232 lines
6.6 KiB
Go
package hscontrol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/chasefleming/elem-go/styles"
|
|
"github.com/gorilla/mux"
|
|
"github.com/juanfont/headscale/hscontrol/templates"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/rs/zerolog/log"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
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
|
|
|
|
reservedResponseHeaderSize = 4
|
|
)
|
|
|
|
// httpError logs an error and sends an HTTP error response with the given
|
|
func httpError(w http.ResponseWriter, err error) {
|
|
var herr HTTPError
|
|
if errors.As(err, &herr) {
|
|
http.Error(w, herr.Msg, herr.Code)
|
|
log.Error().Err(herr.Err).Int("code", herr.Code).Msgf("user msg: %s", herr.Msg)
|
|
} else {
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
log.Error().Err(err).Int("code", http.StatusInternalServerError).Msg("http internal server error")
|
|
}
|
|
}
|
|
|
|
// HTTPError represents an error that is surfaced to the user via web.
|
|
type HTTPError struct {
|
|
Code int // HTTP response code to send to client; 0 means 500
|
|
Msg string // Response body to send to client
|
|
Err error // Detailed error to log on the server
|
|
}
|
|
|
|
func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]: %s, %s", e.Code, e.Msg, e.Err) }
|
|
func (e HTTPError) Unwrap() error { return e.Err }
|
|
|
|
// Error returns an HTTPError containing the given information.
|
|
func NewHTTPError(code int, msg string, err error) HTTPError {
|
|
return HTTPError{Code: code, Msg: msg, Err: err}
|
|
}
|
|
|
|
var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)
|
|
|
|
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
|
|
"machines registered with CLI does not support expire",
|
|
)
|
|
var ErrNoCapabilityVersion = errors.New("no capability version set")
|
|
|
|
func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
|
|
clientCapabilityStr := req.URL.Query().Get("v")
|
|
|
|
if clientCapabilityStr == "" {
|
|
return 0, NewHTTPError(http.StatusBadRequest, "capability version must be set", nil)
|
|
}
|
|
|
|
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
|
|
if err != nil {
|
|
return 0, NewHTTPError(http.StatusBadRequest, "invalid capability version", fmt.Errorf("failed to parse capability version: %w", err))
|
|
}
|
|
|
|
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
|
}
|
|
|
|
func (h *Headscale) derpRequestIsAllowed(
|
|
req *http.Request,
|
|
) (bool, error) {
|
|
body, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return false, fmt.Errorf("cannot read request body: %w", err)
|
|
}
|
|
|
|
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
|
|
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
|
|
return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
|
|
}
|
|
|
|
nodes, err := h.db.ListNodes()
|
|
if err != nil {
|
|
return false, fmt.Errorf("cannot list nodes: %w", err)
|
|
}
|
|
|
|
return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil
|
|
}
|
|
|
|
// see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159, Derp use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
|
|
func (h *Headscale) VerifyHandler(
|
|
writer http.ResponseWriter,
|
|
req *http.Request,
|
|
) {
|
|
if req.Method != http.MethodPost {
|
|
httpError(writer, errMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
allow, err := h.derpRequestIsAllowed(req)
|
|
if err != nil {
|
|
httpError(writer, err)
|
|
return
|
|
}
|
|
|
|
resp := tailcfg.DERPAdmitClientResponse{
|
|
Allow: allow,
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(writer).Encode(resp)
|
|
}
|
|
|
|
// 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
|
|
capVer, err := parseCabailityVersion(req)
|
|
if err != nil {
|
|
httpError(writer, err)
|
|
return
|
|
}
|
|
|
|
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
|
if capVer >= NoiseCapabilityVersion {
|
|
resp := tailcfg.OverTLSPublicKeyResponse{
|
|
PublicKey: h.noisePrivateKey.Public(),
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(writer).Encode(resp)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
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)
|
|
res.Status = "fail"
|
|
}
|
|
|
|
json.NewEncoder(writer).Encode(res)
|
|
}
|
|
|
|
if err := h.db.PingDB(req.Context()); err != nil {
|
|
respond(err)
|
|
|
|
return
|
|
}
|
|
|
|
respond(nil)
|
|
}
|
|
|
|
var codeStyleRegisterWebAPI = styles.Props{
|
|
styles.Display: "block",
|
|
styles.Padding: "20px",
|
|
styles.Border: "1px solid #bbb",
|
|
styles.BackgroundColor: "#eee",
|
|
}
|
|
|
|
type AuthProviderWeb struct {
|
|
serverURL string
|
|
}
|
|
|
|
func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
|
|
return &AuthProviderWeb{
|
|
serverURL: serverURL,
|
|
}
|
|
}
|
|
|
|
func (a *AuthProviderWeb) AuthURL(registrationId types.RegistrationID) string {
|
|
return fmt.Sprintf(
|
|
"%s/register/%s",
|
|
strings.TrimSuffix(a.serverURL, "/"),
|
|
registrationId.String())
|
|
}
|
|
|
|
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
|
// Listens in /register/:registration_id.
|
|
//
|
|
// This is not part of the Tailscale control API, as we could send whatever URL
|
|
// in the RegisterResponse.AuthURL field.
|
|
func (a *AuthProviderWeb) RegisterHandler(
|
|
writer http.ResponseWriter,
|
|
req *http.Request,
|
|
) {
|
|
vars := mux.Vars(req)
|
|
registrationIdStr := vars["registration_id"]
|
|
|
|
// 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.
|
|
registrationId, err := types.RegistrationIDFromString(registrationIdStr)
|
|
if err != nil {
|
|
httpError(writer, NewHTTPError(http.StatusBadRequest, "invalid registration id", err))
|
|
return
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
writer.WriteHeader(http.StatusOK)
|
|
writer.Write([]byte(templates.RegisterWeb(registrationId).Render()))
|
|
}
|