mirror of
https://github.com/juanfont/headscale.git
synced 2025-10-19 11:15:48 +02:00
Initial work on a nodestore which stores all of the nodes and their relations in memory with relationship for peers precalculated. It is a copy-on-write structure, replacing the "snapshot" when a change to the structure occurs. It is optimised for reads, and while batches are not fast, they are grouped together to do less of the expensive peer calculation if there are many changes rapidly. Writes will block until commited, while reads are never blocked. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
253 lines
7.1 KiB
Go
253 lines
7.1 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",
|
|
)
|
|
|
|
func parseCapabilityVersion(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) handleVerifyRequest(
|
|
req *http.Request,
|
|
writer io.Writer,
|
|
) error {
|
|
body, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot read request body: %w", err)
|
|
}
|
|
|
|
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
|
|
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
|
|
return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err))
|
|
}
|
|
|
|
nodes := h.state.ListNodes()
|
|
|
|
// Check if any node has the requested NodeKey
|
|
var nodeKeyFound bool
|
|
for _, node := range nodes.All() {
|
|
if node.NodeKey() == derpAdmitClientRequest.NodePublic {
|
|
nodeKeyFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
resp := &tailcfg.DERPAdmitClientResponse{
|
|
Allow: nodeKeyFound,
|
|
}
|
|
|
|
return json.NewEncoder(writer).Encode(resp)
|
|
}
|
|
|
|
// VerifyHandler 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
|
|
}
|
|
|
|
err := h.handleVerifyRequest(req, writer)
|
|
if err != nil {
|
|
httpError(writer, err)
|
|
return
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
}
|
|
|
|
// 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 := parseCapabilityVersion(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.state.PingDB(req.Context()); err != nil {
|
|
respond(err)
|
|
|
|
return
|
|
}
|
|
|
|
respond(nil)
|
|
}
|
|
|
|
func (h *Headscale) RobotsHandler(
|
|
writer http.ResponseWriter,
|
|
req *http.Request,
|
|
) {
|
|
writer.Header().Set("Content-Type", "text/plain")
|
|
writer.WriteHeader(http.StatusOK)
|
|
_, err := writer.Write([]byte("User-agent: *\nDisallow: /"))
|
|
if err != nil {
|
|
log.Error().
|
|
Caller().
|
|
Err(err).
|
|
Msg("Failed to write response")
|
|
}
|
|
}
|
|
|
|
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()))
|
|
}
|