mirror of
https://github.com/MirageNetwork/MirageServer.git
synced 2025-09-26 20:41:34 +08:00
587 lines
16 KiB
Go
587 lines
16 KiB
Go
package controller
|
||
|
||
import (
|
||
"crypto/rand"
|
||
_ "embed"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/rs/zerolog/log"
|
||
"tailscale.com/tailcfg"
|
||
"tailscale.com/types/key"
|
||
)
|
||
|
||
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
|
||
)
|
||
|
||
// KeyHandler provides the Mirage pub key
|
||
// Listens in /key.
|
||
func (h *Mirage) KeyHandler(
|
||
writer http.ResponseWriter,
|
||
req *http.Request,
|
||
) {
|
||
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
|
||
clientCapabilityStr := req.URL.Query().Get("v")
|
||
|
||
log.Debug().
|
||
Str("handler", "/key").
|
||
Str("v", clientCapabilityStr).
|
||
Msg("New noise client")
|
||
_, err := strconv.Atoi(clientCapabilityStr) // cgao6: 版本号暂时不判断不使用
|
||
if err != nil {
|
||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||
writer.WriteHeader(http.StatusBadRequest)
|
||
_, err := writer.Write([]byte("Wrong params"))
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
||
// cgao6: 我们只支持不低于39版本的客户端
|
||
//if clientCapabilityVersion >= NoiseCapabilityVersion {
|
||
resp := tailcfg.OverTLSPublicKeyResponse{
|
||
LegacyPublicKey: key.MachinePublic{},
|
||
PublicKey: h.noisePrivateKey.Public(),
|
||
}
|
||
writer.Header().Set("Content-Type", "application/json")
|
||
writer.WriteHeader(http.StatusOK)
|
||
err = json.NewEncoder(writer).Encode(resp)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
}
|
||
|
||
// cgao6: HS原本版本中存在诸多不合理的处理,这里我们需要根据自己的理解使用自己的版本
|
||
func (h *Mirage) handleRegisterCommon(
|
||
writer http.ResponseWriter,
|
||
req *http.Request,
|
||
registerRequest tailcfg.RegisterRequest,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
now := time.Now().UTC()
|
||
// 这一步目前考虑不使用MachineKey
|
||
machine, _ := h.GetMachineByNodeKey(registerRequest.NodeKey)
|
||
|
||
// 机器已存在,意味着:
|
||
// - 正常使用(NodeKey一致、未过期、未设置要求过期)
|
||
// - NodeKey一致但设置过期
|
||
// - NodeKey一致但已过期(此时应该当做不一致处理,因为旧的过期的本该删除)
|
||
// - 无一致NodeKey
|
||
// 后两种当新的处理,后续认证过后我们会再将原先的同用户node替掉或者创建新的
|
||
if machine != nil {
|
||
if !registerRequest.Expiry.IsZero() &&
|
||
registerRequest.Expiry.UTC().Before(now) {
|
||
h.handleMachineLogOutCommon(writer, *machine, machineKey)
|
||
return
|
||
}
|
||
if !machine.isExpired() {
|
||
h.handleMachineValidRegistrationCommon(writer, *machine, machineKey)
|
||
return
|
||
}
|
||
}
|
||
//cgao6: 因为除去NodeKey一致(正常)和NodeKey一致(请求过期)两种外我们预计同样处理,故后续不用再判断
|
||
|
||
//cgao6: 授权密钥注册模式 //TODO: 后续需要对授权密钥注册进行检查
|
||
if registerRequest.Auth.AuthKey != "" {
|
||
h.handleAuthKeyCommon(writer, registerRequest, machineKey)
|
||
return
|
||
}
|
||
|
||
// TODO: cgao6: 我们需要对Followup的使用要进一步思索
|
||
// 这里是非常有价值修复的问题所在
|
||
if registerRequest.Followup != "" {
|
||
aCode := registerRequest.Followup[len(registerRequest.Followup)-12:]
|
||
if _, ok := h.aCodeCache.Get(aCode); ok {
|
||
log.Debug().
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Str("machine_key", machineKey.ShortString()).
|
||
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")
|
||
|
||
longPollChan := make(chan string)
|
||
h.longPollChanPool[aCode] = longPollChan
|
||
select {
|
||
case <-req.Context().Done():
|
||
fmt.Println("DEBUG: 客户端断开long poll")
|
||
return
|
||
case loginNoticeMsg := <-longPollChan:
|
||
delete(h.longPollChanPool, aCode)
|
||
if loginNoticeMsg == "ok" {
|
||
h.sendLoginSuccess(writer, machineKey)
|
||
}
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
log.Debug().
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Str("machine_key", machineKey.ShortString()).
|
||
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")
|
||
|
||
// TODO: 原本对机器的givenName的随机数模式并不优雅,要改成模仿TS的做法(即在后面加-<递增数字>的方法)
|
||
// 而且要在实际入库时做
|
||
|
||
// 创建aCode缓存用来后续注册使用
|
||
// 因为过期时间取决于用户的过期设置,故此处不必记录!
|
||
// TODO: 创建ACode时是否要记录MachineKey???
|
||
log.Debug().Str("machine", registerRequest.Hostinfo.Hostname).Msg("The node seems to be new, sending auth url")
|
||
aCode := h.GenACode()
|
||
stateCode := h.GenStateCode()
|
||
h.aCodeCache.Set(
|
||
aCode,
|
||
ACacheItem{
|
||
stateCode: stateCode,
|
||
uid: -1,
|
||
mKey: machineKey,
|
||
regReq: registerRequest,
|
||
},
|
||
time.Until(time.Now().AddDate(0, 1, 0)),
|
||
)
|
||
h.stateCodeCache.Set(
|
||
stateCode,
|
||
StateCacheItem{
|
||
nextURL: "/a/" + aCode,
|
||
uid: -1,
|
||
machineKey: machineKey,
|
||
},
|
||
time.Until(time.Now().AddDate(0, 1, 0)),
|
||
)
|
||
// 创建新acode时,将原先机器对应的controlCode全部清除
|
||
if machineControlCodeC, ok := h.machineControlCodeCache.Get(machineKey.String()); ok {
|
||
for _, controlCode := range machineControlCodeC.(MachineControlCodeCacheItem).controlCodes {
|
||
h.controlCodeCache.Delete(controlCode)
|
||
}
|
||
}
|
||
|
||
h.SendACode(writer, aCode, registerRequest, machineKey)
|
||
}
|
||
|
||
type ACacheItem struct {
|
||
stateCode string
|
||
mKey key.MachinePublic
|
||
regReq tailcfg.RegisterRequest
|
||
uid tailcfg.UserID
|
||
}
|
||
|
||
func (h *Mirage) GenACode() string {
|
||
randomBlob := make([]byte, 6)
|
||
if _, err := rand.Read(randomBlob); err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Msg("could not read 6 bytes from rand")
|
||
return ""
|
||
}
|
||
stateStr := hex.EncodeToString(randomBlob)[:12]
|
||
return stateStr
|
||
}
|
||
|
||
func (h *Mirage) GenStateCode() string {
|
||
const letterBytes = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
b := make([]byte, 25)
|
||
b[0] = 'm'
|
||
b[1] = 'n'
|
||
b[2] = '-'
|
||
|
||
index := make([]byte, 22)
|
||
rand.Read(index)
|
||
for i := 0; i < 22; i++ {
|
||
b[i+3] = letterBytes[index[i]&63]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// cgao6: 用来测试longpoll解决方案,返回空authURL值
|
||
func (h *Mirage) sendLoginSuccess(
|
||
writer http.ResponseWriter,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
resp := tailcfg.RegisterResponse{}
|
||
|
||
resp.AuthURL = ""
|
||
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().Caller().Err(err).Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().Caller().Err(err).Msg("Failed to write response")
|
||
}
|
||
|
||
log.Trace().Msg("Successfully Empty authURL")
|
||
}
|
||
|
||
// cgao6: 替代handleNewMachineCommon,处理新设备注册,变更了返回值
|
||
func (h *Mirage) SendACode(
|
||
writer http.ResponseWriter,
|
||
aCode string,
|
||
registerRequest tailcfg.RegisterRequest,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
resp := tailcfg.RegisterResponse{}
|
||
|
||
resp.AuthURL = fmt.Sprintf(
|
||
"https://%s/a/%s",
|
||
h.cfg.ServerURL,
|
||
aCode,
|
||
)
|
||
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().Caller().Err(err).Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().Caller().Err(err).Msg("Failed to write response")
|
||
}
|
||
|
||
log.Debug().Str("AuthURL", resp.AuthURL).Str("machine", registerRequest.Hostinfo.Hostname).Msg("Successfully sent auth url")
|
||
}
|
||
|
||
// handleAuthKeyCommon contains the logic to manage auth key client registration
|
||
// It is used both by the legacy and the new Noise protocol.
|
||
//
|
||
// TODO: check if any locks are needed around IP allocation.
|
||
func (h *Mirage) handleAuthKeyCommon(
|
||
writer http.ResponseWriter,
|
||
registerRequest tailcfg.RegisterRequest,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
log.Debug().
|
||
Str("func", "handleAuthKeyCommon").
|
||
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", "handleAuthKeyCommon").
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Err(err).
|
||
Msg("Failed authentication via AuthKey")
|
||
resp.MachineAuthorized = false
|
||
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("func", "handleAuthKeyCommon").
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Err(err).
|
||
Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusUnauthorized)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
|
||
log.Error().
|
||
Caller().
|
||
Str("func", "handleAuthKeyCommon").
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Msg("Failed authentication via AuthKey")
|
||
|
||
return
|
||
}
|
||
|
||
log.Debug().
|
||
Str("func", "handleAuthKeyCommon").
|
||
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.GetMachineByAnyKey(machineKey, registerRequest.NodeKey, registerRequest.OldNodeKey)
|
||
machine, _ := h.GetUserMachineByMachineKey(machineKey, pak.User.toTailscaleUser().ID)
|
||
if machine != nil {
|
||
log.Trace().
|
||
Str("machine", machine.Hostname).
|
||
Msg("machine was already registered before, refreshing with new auth key")
|
||
|
||
h.NotifyNaviOrgNodesChange(machine.User.OrganizationID, nodeKey, machine.NodeKey)
|
||
|
||
machine.NodeKey = nodeKey
|
||
machine.AuthKeyID = uint(pak.ID)
|
||
machine.AuthKey = pak
|
||
err := h.RefreshMachine(machine, registerRequest.Expiry)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("machine", machine.Hostname).
|
||
Err(err).
|
||
Msg("Failed to refresh machine")
|
||
|
||
return
|
||
}
|
||
|
||
aclTags := pak.GetAclTags()
|
||
if len(aclTags) > 0 {
|
||
// This conditional preserves the existing behaviour, although SaaS would reset the tags on auth-key login
|
||
err = h.SetTags(machine, aclTags)
|
||
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("machine", machine.Hostname).
|
||
Strs("aclTags", aclTags).
|
||
Err(err).
|
||
Msg("Failed to set tags after refreshing machine")
|
||
|
||
return
|
||
}
|
||
}
|
||
} else {
|
||
now := time.Now().UTC()
|
||
|
||
givenName := h.GenMachineName(registerRequest.Hostinfo.Hostname, pak.UserID, pak.User.OrganizationID, MachinePublicKeyStripPrefix(machineKey))
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("func", "RegistrationHandler").
|
||
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
|
||
Err(err)
|
||
|
||
return
|
||
}
|
||
|
||
machineToRegister := Machine{
|
||
Hostname: registerRequest.Hostinfo.Hostname,
|
||
GivenName: givenName,
|
||
UserID: pak.User.ID,
|
||
MachineKey: MachinePublicKeyStripPrefix(machineKey),
|
||
RegisterMethod: RegisterMethodAuthKey,
|
||
Expiry: ®isterRequest.Expiry,
|
||
NodeKey: nodeKey,
|
||
LastSeen: &now,
|
||
AuthKeyID: uint(pak.ID),
|
||
ForcedTags: pak.GetAclTags(),
|
||
}
|
||
|
||
machine, err = h.RegisterMachine(
|
||
machineToRegister,
|
||
)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("could not register machine")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
h.NotifyNaviOrgNodesChange(machine.User.OrganizationID, nodeKey, "")
|
||
}
|
||
|
||
err = h.UsePreAuthKey(pak)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to use pre-auth key")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
|
||
resp.MachineAuthorized = true
|
||
resp.User = *pak.User.toTailscaleUser()
|
||
// Provide LoginName when registering with pre-auth key
|
||
// Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName*
|
||
resp.Login = *pak.User.toTailscaleLogin()
|
||
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("func", "handleAuthKeyCommon").
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Err(err).
|
||
Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
|
||
log.Info().
|
||
Str("func", "handleAuthKeyCommon").
|
||
Str("machine", registerRequest.Hostinfo.Hostname).
|
||
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
|
||
Msg("Successfully authenticated via AuthKey")
|
||
}
|
||
|
||
func (h *Mirage) handleMachineLogOutCommon(
|
||
writer http.ResponseWriter,
|
||
machine Machine,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
resp := tailcfg.RegisterResponse{}
|
||
|
||
log.Info().
|
||
Str("machine", machine.Hostname).
|
||
Msg("Client requested logout")
|
||
|
||
err := h.ExpireMachine(&machine)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Str("func", "handleMachineLogOutCommon").
|
||
Err(err).
|
||
Msg("Failed to expire machine")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
|
||
resp.AuthURL = ""
|
||
resp.MachineAuthorized = false
|
||
resp.NodeKeyExpired = true
|
||
resp.User = *machine.User.toTailscaleUser()
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
|
||
return
|
||
}
|
||
|
||
if machine.isEphemeral() {
|
||
err = h.HardDeleteMachine(&machine)
|
||
if err != nil {
|
||
log.Error().
|
||
Err(err).
|
||
Str("machine", machine.Hostname).
|
||
Msg("Cannot delete ephemeral machine from the database")
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
h.NotifyNaviOrgNodesChange(machine.User.OrganizationID, "", machine.NodeKey)
|
||
|
||
log.Info().
|
||
Str("machine", machine.Hostname).
|
||
Msg("Successfully logged out")
|
||
}
|
||
|
||
func (h *Mirage) handleMachineValidRegistrationCommon(
|
||
writer http.ResponseWriter,
|
||
machine Machine,
|
||
machineKey key.MachinePublic,
|
||
) {
|
||
resp := tailcfg.RegisterResponse{}
|
||
|
||
// The machine registration is valid, respond with redirect to /map
|
||
log.Debug().
|
||
Str("machine", machine.Hostname).
|
||
Msg("Client is registered and we have the current NodeKey. All clear to /map")
|
||
|
||
resp.AuthURL = ""
|
||
resp.MachineAuthorized = true
|
||
resp.User = *machine.User.toTailscaleUser()
|
||
resp.Login = *machine.User.toTailscaleLogin()
|
||
|
||
respBody, err := h.marshalResponse(resp, machineKey)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Cannot encode message")
|
||
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||
|
||
return
|
||
}
|
||
|
||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
writer.WriteHeader(http.StatusOK)
|
||
_, err = writer.Write(respBody)
|
||
if err != nil {
|
||
log.Error().
|
||
Caller().
|
||
Err(err).
|
||
Msg("Failed to write response")
|
||
}
|
||
|
||
log.Info().
|
||
Str("machine", machine.Hostname).
|
||
Msg("Machine successfully authorized")
|
||
}
|