mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-13 11:01:07 -04:00
this commit moves all of the read and write logic, and all different parts of headscale that manages some sort of persistent and in memory state into a separate package. The goal of this is to clearly define the boundry between parts of the app which accesses and modifies data, and where it happens. Previously, different state (routes, policy, db and so on) was used directly, and sometime passed to functions as pointers. Now all access has to go through state. In the initial implementation, most of the same functions exists and have just been moved. In the future centralising this will allow us to optimise bottle necks with the database (in memory state) and make the different parts talking to eachother do so in the same way across headscale components. 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) 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 fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
|
|
}
|
|
|
|
nodes, err := h.state.ListNodes()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot list nodes: %w", err)
|
|
}
|
|
|
|
resp := &tailcfg.DERPAdmitClientResponse{
|
|
Allow: nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic),
|
|
}
|
|
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 := 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.state.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()))
|
|
}
|