Files
MirageServer/controller/protocol_common.go
Chenyang Gao 7e14829a0e 清理日志输出
Signed-off-by: Chenyang Gao <gps949@outlook.com>
2023-05-16 11:34:34 +08:00

587 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: &registerRequest.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")
}