diff --git a/protocol_legacy.go b/protocol_legacy.go new file mode 100644 index 00000000..5c23b172 --- /dev/null +++ b/protocol_legacy.go @@ -0,0 +1,199 @@ +package headscale + +import ( + "errors" + "io" + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +// RegistrationHandler handles the actual registration process of a machine +// Endpoint /machine/:mkey. +func (h *Headscale) RegistrationHandler( + writer http.ResponseWriter, + req *http.Request, +) { + vars := mux.Vars(req) + machineKeyStr, ok := vars["mkey"] + if !ok || machineKeyStr == "" { + log.Error(). + Str("handler", "RegistrationHandler"). + Msg("No machine ID in request") + http.Error(writer, "No machine ID in request", http.StatusBadRequest) + + return + } + + body, _ := io.ReadAll(req.Body) + + 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() + http.Error(writer, "Cannot parse machine key", http.StatusBadRequest) + + return + } + registerRequest := tailcfg.RegisterRequest{} + err = decode(body, ®isterRequest, &machineKey, h.privateKey) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot decode message") + machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() + http.Error(writer, "Cannot decode message", http.StatusBadRequest) + + return + } + + now := time.Now().UTC() + machine, err := h.GetMachineByMachineKey(machineKey) + if errors.Is(err, gorm.ErrRecordNotFound) { + machineKeyStr := MachinePublicKeyStripPrefix(machineKey) + + // If the machine has AuthKey set, handle registration via PreAuthKeys + if registerRequest.Auth.AuthKey != "" { + h.handleAuthKey(writer, req, machineKey, registerRequest) + + return + } + + // Check if the node is waiting for interactive login. + // + // TODO(juan): We could use this field to improve our protocol implementation, + // and hold the request until the client closes it, or the interactive + // login is completed (i.e., the user registers the machine). + // This is not implemented yet, as it is no strictly required. The only side-effect + // is that the client will hammer headscale with requests until it gets a + // successful RegisterResponse. + if registerRequest.Followup != "" { + if _, ok := h.registrationCache.Get(NodePublicKeyStripPrefix(registerRequest.NodeKey)); ok { + log.Debug(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("Machine is waiting for interactive login") + + ticker := time.NewTicker(registrationHoldoff) + select { + case <-req.Context().Done(): + return + case <-ticker.C: + h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) + + return + } + } + } + + log.Info(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Str("node_key", registerRequest.NodeKey.ShortString()). + Str("node_key_old", registerRequest.OldNodeKey.ShortString()). + Str("follow_up", registerRequest.Followup). + Msg("New machine not yet in the database") + + givenName, err := h.GenerateGivenName(registerRequest.Hostinfo.Hostname) + if err != nil { + log.Error(). + Caller(). + Str("func", "RegistrationHandler"). + Str("hostinfo.name", registerRequest.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, + Hostname: registerRequest.Hostinfo.Hostname, + GivenName: givenName, + NodeKey: NodePublicKeyStripPrefix(registerRequest.NodeKey), + LastSeen: &now, + Expiry: &time.Time{}, + } + + if !registerRequest.Expiry.IsZero() { + log.Trace(). + Caller(). + Str("machine", registerRequest.Hostinfo.Hostname). + Time("expiry", registerRequest.Expiry). + Msg("Non-zero expiry time requested") + newMachine.Expiry = ®isterRequest.Expiry + } + + h.registrationCache.Set( + newMachine.NodeKey, + newMachine, + registerCacheExpiration, + ) + + h.handleMachineRegistrationNew(writer, req, machineKey, registerRequest) + + 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(registerRequest.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 !registerRequest.Expiry.IsZero() && + registerRequest.Expiry.UTC().Before(now) { + h.handleMachineLogOut(writer, req, 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(writer, req, machineKey, *machine) + + return + } + } + + // The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration + if machine.NodeKey == NodePublicKeyStripPrefix(registerRequest.OldNodeKey) && + !machine.isExpired() { + h.handleMachineRefreshKey( + writer, + req, + machineKey, + registerRequest, + *machine, + ) + + return + } + + // The machine has expired + h.handleMachineExpired(writer, req, machineKey, registerRequest, *machine) + + return + } +}