mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			737 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			737 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package headscale
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/binary"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"html/template"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/gin-gonic/gin"
 | |
| 	"github.com/klauspost/compress/zstd"
 | |
| 	"github.com/rs/zerolog/log"
 | |
| 	"gorm.io/gorm"
 | |
| 	"tailscale.com/tailcfg"
 | |
| 	"tailscale.com/types/key"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	ErrRegisterMethodCLIDoesNotSupportExpire = Error(
 | |
| 		"machines registered with CLI does not support expire",
 | |
| 	)
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	reservedResponseHeaderSize = 4
 | |
| 	RegisterMethodAuthKey      = "authkey"
 | |
| 	RegisterMethodOIDC         = "oidc"
 | |
| 	RegisterMethodCLI          = "cli"
 | |
| 
 | |
| 	// The CapabilityVersion is used by Tailscale clients to indicate
 | |
| 	// their codebase version. Tailscale clients can communicate over TS2021
 | |
| 	// from CapabilityVersion 28.
 | |
| 	// See https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
 | |
| 	NoiseCapabilityVersion = 28
 | |
| )
 | |
| 
 | |
| // KeyHandler provides the Headscale pub key
 | |
| // Listens in /key.
 | |
| func (h *Headscale) KeyHandler(ctx *gin.Context) {
 | |
| 	// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
 | |
| 	v := ctx.Query("v")
 | |
| 	if v != "" {
 | |
| 		clientCapabilityVersion, err := strconv.Atoi(v)
 | |
| 		if err != nil {
 | |
| 			ctx.String(http.StatusBadRequest, "Invalid version")
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if clientCapabilityVersion >= NoiseCapabilityVersion {
 | |
| 			// Tailscale has a different key for the TS2021 protocol. Not sure why.
 | |
| 			resp := tailcfg.OverTLSPublicKeyResponse{
 | |
| 				LegacyPublicKey: h.privateKey.Public(),
 | |
| 				PublicKey:       h.noisePrivateKey.Public(),
 | |
| 			}
 | |
| 			ctx.JSON(http.StatusOK, resp)
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Old clients don't send a 'v' parameter, so we send the legacy public key
 | |
| 	ctx.Data(
 | |
| 		http.StatusOK,
 | |
| 		"text/plain; charset=utf-8",
 | |
| 		[]byte(MachinePublicKeyStripPrefix(h.privateKey.Public())),
 | |
| 	)
 | |
| }
 | |
| 
 | |
| type registerWebAPITemplateConfig struct {
 | |
| 	Key string
 | |
| }
 | |
| 
 | |
| var registerWebAPITemplate = template.Must(
 | |
| 	template.New("registerweb").Parse(`
 | |
| <html>
 | |
| 	<head>
 | |
| 		<title>Registration - Headscale</title>
 | |
| 	</head>
 | |
| 	<body>
 | |
| 		<h1>headscale</h1>
 | |
| 		<h2>Machine registration</h2>
 | |
| 		<p>
 | |
| 			Run the command below in the headscale server to add this machine to your network:
 | |
| 		</p>
 | |
| 		<pre><code>headscale -n NAMESPACE nodes register --key {{.Key}}</code></pre>
 | |
| 	</body>
 | |
| </html>
 | |
| `))
 | |
| 
 | |
| // RegisterWebAPI shows a simple message in the browser to point to the CLI
 | |
| // Listens in /register.
 | |
| func (h *Headscale) RegisterWebAPI(ctx *gin.Context) {
 | |
| 	machineKeyStr := ctx.Query("key")
 | |
| 	if machineKeyStr == "" {
 | |
| 		ctx.String(http.StatusBadRequest, "Wrong params")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var content bytes.Buffer
 | |
| 	if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
 | |
| 		Key: machineKeyStr,
 | |
| 	}); err != nil {
 | |
| 		log.Error().
 | |
| 			Str("func", "RegisterWebAPI").
 | |
| 			Err(err).
 | |
| 			Msg("Could not render register web API template")
 | |
| 		ctx.Data(
 | |
| 			http.StatusInternalServerError,
 | |
| 			"text/html; charset=utf-8",
 | |
| 			[]byte("Could not render register web API template"),
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	ctx.Data(http.StatusOK, "text/html; charset=utf-8", content.Bytes())
 | |
| }
 | |
| 
 | |
| // RegistrationHandler handles the actual registration process of a machine
 | |
| // Endpoint /machine/:id.
 | |
| func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
 | |
| 	body, _ := io.ReadAll(ctx.Request.Body)
 | |
| 	machineKeyStr := ctx.Param("id")
 | |
| 
 | |
| 	var machineKey key.MachinePublic
 | |
| 	err := machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr)))
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot parse machine key")
 | |
| 		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
 | |
| 		ctx.String(http.StatusInternalServerError, "Sad!")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 	req := tailcfg.RegisterRequest{}
 | |
| 	err = decode(body, &req, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot decode message")
 | |
| 		machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc()
 | |
| 		ctx.String(http.StatusInternalServerError, "Very sad!")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	now := time.Now().UTC()
 | |
| 	machine, err := h.GetMachineByMachineKey(machineKey)
 | |
| 	if errors.Is(err, gorm.ErrRecordNotFound) {
 | |
| 		log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
 | |
| 
 | |
| 		machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
 | |
| 
 | |
| 		// If the machine has AuthKey set, handle registration via PreAuthKeys
 | |
| 		if req.Auth.AuthKey != "" {
 | |
| 			h.handleAuthKey(ctx, machineKey, req)
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 		hname, err := NormalizeToFQDNRules(
 | |
| 			req.Hostinfo.Hostname,
 | |
| 			h.cfg.OIDC.StripEmaildomain,
 | |
| 		)
 | |
| 		if err != nil {
 | |
| 			log.Error().
 | |
| 				Caller().
 | |
| 				Str("func", "RegistrationHandler").
 | |
| 				Str("hostinfo.name", req.Hostinfo.Hostname).
 | |
| 				Err(err)
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// The machine did not have a key to authenticate, which means
 | |
| 		// that we rely on a method that calls back some how (OpenID or CLI)
 | |
| 		// We create the machine and then keep it around until a callback
 | |
| 		// happens
 | |
| 		newMachine := Machine{
 | |
| 			MachineKey: machineKeyStr,
 | |
| 			Name:       hname,
 | |
| 			NodeKey:    NodePublicKeyStripPrefix(req.NodeKey),
 | |
| 			LastSeen:   &now,
 | |
| 			Expiry:     &time.Time{},
 | |
| 		}
 | |
| 
 | |
| 		if !req.Expiry.IsZero() {
 | |
| 			log.Trace().
 | |
| 				Caller().
 | |
| 				Str("machine", req.Hostinfo.Hostname).
 | |
| 				Time("expiry", req.Expiry).
 | |
| 				Msg("Non-zero expiry time requested")
 | |
| 			newMachine.Expiry = &req.Expiry
 | |
| 		}
 | |
| 
 | |
| 		h.registrationCache.Set(
 | |
| 			NodePublicKeyStripPrefix(req.NodeKey),
 | |
| 			newMachine,
 | |
| 			registerCacheExpiration,
 | |
| 		)
 | |
| 
 | |
| 		h.handleMachineRegistrationNew(ctx, machineKey, req)
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// The machine is already registered, so we need to pass through reauth or key update.
 | |
| 	if machine != nil {
 | |
| 		// If the NodeKey stored in headscale is the same as the key presented in a registration
 | |
| 		// request, then we have a node that is either:
 | |
| 		// - Trying to log out (sending a expiry in the past)
 | |
| 		// - A valid, registered machine, looking for the node map
 | |
| 		// - Expired machine wanting to reauthenticate
 | |
| 		if machine.NodeKey == NodePublicKeyStripPrefix(req.NodeKey) {
 | |
| 			// The client sends an Expiry in the past if the client is requesting to expire the key (aka logout)
 | |
| 			//   https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
 | |
| 			if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) {
 | |
| 				h.handleMachineLogOut(ctx, machineKey, *machine)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			// If machine is not expired, and is register, we have a already accepted this machine,
 | |
| 			// let it proceed with a valid registration
 | |
| 			if !machine.isExpired() {
 | |
| 				h.handleMachineValidRegistration(ctx, machineKey, *machine)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration
 | |
| 		if machine.NodeKey == NodePublicKeyStripPrefix(req.OldNodeKey) &&
 | |
| 			!machine.isExpired() {
 | |
| 			h.handleMachineRefreshKey(ctx, machineKey, req, *machine)
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// The machine has expired
 | |
| 		h.handleMachineExpired(ctx, machineKey, req, *machine)
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (h *Headscale) getMapResponse(
 | |
| 	machineKey key.MachinePublic,
 | |
| 	req tailcfg.MapRequest,
 | |
| 	machine *Machine,
 | |
| ) ([]byte, error) {
 | |
| 	log.Trace().
 | |
| 		Str("func", "getMapResponse").
 | |
| 		Str("machine", req.Hostinfo.Hostname).
 | |
| 		Msg("Creating Map response")
 | |
| 	node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "getMapResponse").
 | |
| 			Err(err).
 | |
| 			Msg("Cannot convert to node")
 | |
| 
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	peers, err := h.getValidPeers(machine)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "getMapResponse").
 | |
| 			Err(err).
 | |
| 			Msg("Cannot fetch peers")
 | |
| 
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	profiles := getMapResponseUserProfiles(*machine, peers)
 | |
| 
 | |
| 	nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "getMapResponse").
 | |
| 			Err(err).
 | |
| 			Msg("Failed to convert peers to Tailscale nodes")
 | |
| 
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	dnsConfig := getMapResponseDNSConfig(
 | |
| 		h.cfg.DNSConfig,
 | |
| 		h.cfg.BaseDomain,
 | |
| 		*machine,
 | |
| 		peers,
 | |
| 	)
 | |
| 
 | |
| 	resp := tailcfg.MapResponse{
 | |
| 		KeepAlive:    false,
 | |
| 		Node:         node,
 | |
| 		Peers:        nodePeers,
 | |
| 		DNSConfig:    dnsConfig,
 | |
| 		Domain:       h.cfg.BaseDomain,
 | |
| 		PacketFilter: h.aclRules,
 | |
| 		DERPMap:      h.DERPMap,
 | |
| 		UserProfiles: profiles,
 | |
| 	}
 | |
| 
 | |
| 	log.Trace().
 | |
| 		Str("func", "getMapResponse").
 | |
| 		Str("machine", req.Hostinfo.Hostname).
 | |
| 		// Interface("payload", resp).
 | |
| 		Msgf("Generated map response: %s", tailMapResponseToString(resp))
 | |
| 
 | |
| 	var respBody []byte
 | |
| 	if machineKey.IsZero() {
 | |
| 		// The TS2021 protocol does not rely anymore on the machine key to
 | |
| 		// encrypt in a NaCl box the map response. We just send it back
 | |
| 		// unencrypted via the encrypted Noise channel.
 | |
| 		// declare the incoming size on the first 4 bytes
 | |
| 		respBody, err := json.Marshal(resp)
 | |
| 		if err != nil {
 | |
| 			log.Error().
 | |
| 				Caller().
 | |
| 				Err(err).
 | |
| 				Msg("Cannot marshal map response")
 | |
| 		}
 | |
| 
 | |
| 		var srcCompressed []byte
 | |
| 		if req.Compress == "zstd" {
 | |
| 			encoder, _ := zstd.NewWriter(nil)
 | |
| 			srcCompressed = encoder.EncodeAll(respBody, nil)
 | |
| 		} else {
 | |
| 			srcCompressed = respBody
 | |
| 		}
 | |
| 
 | |
| 		data := make([]byte, reservedResponseHeaderSize)
 | |
| 		binary.LittleEndian.PutUint32(data, uint32(len(srcCompressed)))
 | |
| 		data = append(data, srcCompressed...)
 | |
| 
 | |
| 		return data, nil
 | |
| 	} else {
 | |
| 		if req.Compress == "zstd" {
 | |
| 			src, err := json.Marshal(resp)
 | |
| 			if err != nil {
 | |
| 				log.Error().
 | |
| 					Caller().
 | |
| 					Str("func", "getMapResponse").
 | |
| 					Err(err).
 | |
| 					Msg("Failed to marshal response for the client")
 | |
| 
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			encoder, _ := zstd.NewWriter(nil)
 | |
| 			srcCompressed := encoder.EncodeAll(src, nil)
 | |
| 			respBody = h.privateKey.SealTo(machineKey, srcCompressed)
 | |
| 		} else {
 | |
| 			respBody, err = encode(resp, &machineKey, h.privateKey)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 		// declare the incoming size on the first 4 bytes
 | |
| 		data := make([]byte, reservedResponseHeaderSize)
 | |
| 		binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
 | |
| 		data = append(data, respBody...)
 | |
| 
 | |
| 		return data, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (h *Headscale) getMapKeepAliveResponse(
 | |
| 	machineKey key.MachinePublic,
 | |
| 	mapRequest tailcfg.MapRequest,
 | |
| ) ([]byte, error) {
 | |
| 	mapResponse := tailcfg.MapResponse{
 | |
| 		KeepAlive: true,
 | |
| 	}
 | |
| 	var respBody []byte
 | |
| 	var err error
 | |
| 	if machineKey.IsZero() {
 | |
| 		// The TS2021 protocol does not rely anymore on the machine key.
 | |
| 		return json.Marshal(mapResponse)
 | |
| 	} else {
 | |
| 		if mapRequest.Compress == "zstd" {
 | |
| 			src, err := json.Marshal(mapResponse)
 | |
| 			if err != nil {
 | |
| 				log.Error().
 | |
| 					Caller().
 | |
| 					Str("func", "getMapKeepAliveResponse").
 | |
| 					Err(err).
 | |
| 					Msg("Failed to marshal keepalive response for the client")
 | |
| 
 | |
| 				return nil, err
 | |
| 			}
 | |
| 			encoder, _ := zstd.NewWriter(nil)
 | |
| 			srcCompressed := encoder.EncodeAll(src, nil)
 | |
| 			respBody = h.privateKey.SealTo(machineKey, srcCompressed)
 | |
| 		} else {
 | |
| 			respBody, err = encode(mapResponse, &machineKey, h.privateKey)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 		data := make([]byte, reservedResponseHeaderSize)
 | |
| 		binary.LittleEndian.PutUint32(data, uint32(len(respBody)))
 | |
| 		data = append(data, respBody...)
 | |
| 
 | |
| 		return data, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleMachineLogOut(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	machine Machine,
 | |
| ) {
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	log.Info().
 | |
| 		Str("machine", machine.Name).
 | |
| 		Msg("Client requested logout")
 | |
| 
 | |
| 	h.ExpireMachine(&machine)
 | |
| 
 | |
| 	resp.AuthURL = ""
 | |
| 	resp.MachineAuthorized = false
 | |
| 	resp.User = *machine.Namespace.toUser()
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		ctx.String(http.StatusInternalServerError, "")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleMachineValidRegistration(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	machine Machine,
 | |
| ) {
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	// The machine registration is valid, respond with redirect to /map
 | |
| 	log.Debug().
 | |
| 		Str("machine", machine.Name).
 | |
| 		Msg("Client is registered and we have the current NodeKey. All clear to /map")
 | |
| 
 | |
| 	resp.AuthURL = ""
 | |
| 	resp.MachineAuthorized = true
 | |
| 	resp.User = *machine.Namespace.toUser()
 | |
| 	resp.Login = *machine.Namespace.toLogin()
 | |
| 
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		machineRegistrations.WithLabelValues("update", "web", "error", machine.Namespace.Name).
 | |
| 			Inc()
 | |
| 		ctx.String(http.StatusInternalServerError, "")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	machineRegistrations.WithLabelValues("update", "web", "success", machine.Namespace.Name).
 | |
| 		Inc()
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleMachineExpired(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	registerRequest tailcfg.RegisterRequest,
 | |
| 	machine Machine,
 | |
| ) {
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	// The client has registered before, but has expired
 | |
| 	log.Debug().
 | |
| 		Str("machine", machine.Name).
 | |
| 		Msg("Machine registration has expired. Sending a authurl to register")
 | |
| 
 | |
| 	if registerRequest.Auth.AuthKey != "" {
 | |
| 		h.handleAuthKey(ctx, machineKey, registerRequest)
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if h.cfg.OIDC.Issuer != "" {
 | |
| 		resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
 | |
| 			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
 | |
| 	} else {
 | |
| 		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
 | |
| 			strings.TrimSuffix(h.cfg.ServerURL, "/"), machine.NodeKey)
 | |
| 	}
 | |
| 
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		machineRegistrations.WithLabelValues("reauth", "web", "error", machine.Namespace.Name).
 | |
| 			Inc()
 | |
| 		ctx.String(http.StatusInternalServerError, "")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	machineRegistrations.WithLabelValues("reauth", "web", "success", machine.Namespace.Name).
 | |
| 		Inc()
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleMachineRefreshKey(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	registerRequest tailcfg.RegisterRequest,
 | |
| 	machine Machine,
 | |
| ) {
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	log.Debug().
 | |
| 		Str("machine", machine.Name).
 | |
| 		Msg("We have the OldNodeKey in the database. This is a key refresh")
 | |
| 	machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
 | |
| 	h.db.Save(&machine)
 | |
| 
 | |
| 	resp.AuthURL = ""
 | |
| 	resp.User = *machine.Namespace.toUser()
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		ctx.String(http.StatusInternalServerError, "Extremely sad!")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleMachineRegistrationNew(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	registerRequest tailcfg.RegisterRequest,
 | |
| ) {
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	// The machine registration is new, redirect the client to the registration URL
 | |
| 	log.Debug().
 | |
| 		Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 		Msg("The node is sending us a new NodeKey, sending auth url")
 | |
| 	if h.cfg.OIDC.Issuer != "" {
 | |
| 		resp.AuthURL = fmt.Sprintf(
 | |
| 			"%s/oidc/register/%s",
 | |
| 			strings.TrimSuffix(h.cfg.ServerURL, "/"),
 | |
| 			NodePublicKeyStripPrefix(registerRequest.NodeKey),
 | |
| 		)
 | |
| 	} else {
 | |
| 		resp.AuthURL = fmt.Sprintf("%s/register?key=%s",
 | |
| 			strings.TrimSuffix(h.cfg.ServerURL, "/"), NodePublicKeyStripPrefix(registerRequest.NodeKey))
 | |
| 	}
 | |
| 
 | |
| 	if machineKey.IsZero() {
 | |
| 		// TS2021
 | |
| 		ctx.JSON(http.StatusOK, resp)
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// The Tailscale legacy protocol requires to encrypt the NaCl box with the MachineKey
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		ctx.String(http.StatusInternalServerError, "")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| }
 | |
| 
 | |
| func (h *Headscale) handleAuthKey(
 | |
| 	ctx *gin.Context,
 | |
| 	machineKey key.MachinePublic,
 | |
| 	registerRequest tailcfg.RegisterRequest,
 | |
| ) {
 | |
| 	machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
 | |
| 
 | |
| 	log.Debug().
 | |
| 		Str("func", "handleAuthKey").
 | |
| 		Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 		Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
 | |
| 	resp := tailcfg.RegisterResponse{}
 | |
| 
 | |
| 	pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "handleAuthKey").
 | |
| 			Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 			Err(err).
 | |
| 			Msg("Failed authentication via AuthKey")
 | |
| 		resp.MachineAuthorized = false
 | |
| 		respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 		if err != nil {
 | |
| 			log.Error().
 | |
| 				Caller().
 | |
| 				Str("func", "handleAuthKey").
 | |
| 				Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 				Err(err).
 | |
| 				Msg("Cannot encode message")
 | |
| 			ctx.String(http.StatusInternalServerError, "")
 | |
| 			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
 | |
| 				Inc()
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "handleAuthKey").
 | |
| 			Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 			Msg("Failed authentication via AuthKey")
 | |
| 
 | |
| 		if pak != nil {
 | |
| 			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
 | |
| 				Inc()
 | |
| 		} else {
 | |
| 			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", "unknown").Inc()
 | |
| 		}
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	log.Debug().
 | |
| 		Str("func", "handleAuthKey").
 | |
| 		Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 		Msg("Authentication key was valid, proceeding to acquire IP addresses")
 | |
| 
 | |
| 	nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
 | |
| 
 | |
| 	// retrieve machine information if it exist
 | |
| 	// The error is not important, because if it does not
 | |
| 	// exist, then this is a new machine and we will move
 | |
| 	// on to registration.
 | |
| 	machine, _ := h.GetMachineByMachineKey(machineKey)
 | |
| 	if machine != nil {
 | |
| 		log.Trace().
 | |
| 			Caller().
 | |
| 			Str("machine", machine.Name).
 | |
| 			Msg("machine already registered, refreshing with new auth key")
 | |
| 
 | |
| 		machine.NodeKey = nodeKey
 | |
| 		machine.AuthKeyID = uint(pak.ID)
 | |
| 		h.RefreshMachine(machine, registerRequest.Expiry)
 | |
| 	} else {
 | |
| 		now := time.Now().UTC()
 | |
| 		machineToRegister := Machine{
 | |
| 			Name:           registerRequest.Hostinfo.Hostname,
 | |
| 			NamespaceID:    pak.Namespace.ID,
 | |
| 			MachineKey:     machineKeyStr,
 | |
| 			RegisterMethod: RegisterMethodAuthKey,
 | |
| 			Expiry:         ®isterRequest.Expiry,
 | |
| 			NodeKey:        nodeKey,
 | |
| 			LastSeen:       &now,
 | |
| 			AuthKeyID:      uint(pak.ID),
 | |
| 		}
 | |
| 
 | |
| 		machine, err = h.RegisterMachine(
 | |
| 			machineToRegister,
 | |
| 		)
 | |
| 		if err != nil {
 | |
| 			log.Error().
 | |
| 				Caller().
 | |
| 				Err(err).
 | |
| 				Msg("could not register machine")
 | |
| 			machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
 | |
| 				Inc()
 | |
| 			ctx.String(
 | |
| 				http.StatusInternalServerError,
 | |
| 				"could not register machine",
 | |
| 			)
 | |
| 
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	h.UsePreAuthKey(pak)
 | |
| 
 | |
| 	resp.MachineAuthorized = true
 | |
| 	resp.User = *pak.Namespace.toUser()
 | |
| 	respBody, err := encode(resp, &machineKey, h.privateKey)
 | |
| 	if err != nil {
 | |
| 		log.Error().
 | |
| 			Caller().
 | |
| 			Str("func", "handleAuthKey").
 | |
| 			Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 			Err(err).
 | |
| 			Msg("Cannot encode message")
 | |
| 		machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
 | |
| 			Inc()
 | |
| 		ctx.String(http.StatusInternalServerError, "Extremely sad!")
 | |
| 
 | |
| 		return
 | |
| 	}
 | |
| 	machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
 | |
| 		Inc()
 | |
| 	ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
 | |
| 	log.Info().
 | |
| 		Str("func", "handleAuthKey").
 | |
| 		Str("machine", registerRequest.Hostinfo.Hostname).
 | |
| 		Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
 | |
| 		Msg("Successfully authenticated via AuthKey")
 | |
| }
 |