Files
MirageServer/controller/console_api_machine.go
2023-05-25 18:38:57 +08:00

593 lines
20 KiB
Go

package controller
import (
_ "embed"
"encoding/json"
"net/http"
"net/netip"
"strconv"
"strings"
"time"
)
type machineData struct {
Address []string `json:"addresses"`
AllowedIPs []string `json:"allowedIPs"`
ExtraIPs []string `json:"extraIPs"`
AdvertisedIPs []string `json:"advertisedIPs"`
HasSubnets bool `json:"hasSubnets"`
AdvertisedExitNode bool `json:"advertisedExitNode"`
AllowedExitNode bool `json:"allowedExitNode"`
HasExitNode bool `json:"hasExitNode"` //未实现
AllowedTags []string `json:"allowedTags"` //未实现
InvalidTags []string `json:"invalidTags"` //未实现
HasTags bool `json:"hasTags"` //???移除?
Endpoints []string `json:"endpoints"`
IpnVersion string `json:"ipnVersion"` //未实现
Os string `json:"os"` //未实现
Name string `json:"name"` //未实现
Fqdn string `json:"fqdn"` //未实现
Domain string `json:"domain"` //未实现
Created string `json:"created"` //未实现
Hostname string `json:"hostname"` //未实现
MachineKey string `json:"machineKey"` //未实现
NodeKey string `json:"nodeKey"` //未实现
Id string `json:"id"` //未实现
StableId string `json:"stableId"` //未实现
User string `json:"user"` //未实现
Creator string `json:"creator"` //未实现
Expires string `json:"expires"`
NeverExpires bool `json:"neverExpires"`
Authorized bool `json:"authorized"` //未实现
IsExternal bool `json:"isExternal"` // ??? //未实现
BrokenIPForwarding bool `json:"brokenIPForwarding"` //未实现
IsEphemeral bool `json:"isEphemeral"` //未实现
AvailableUpdateVersion string `json:"availableUpdateVersion"` //未实现
LastSeen string `json:"lastSeen"` //未实现
ConnectedToControl bool `json:"connectedToControl"` //未实现
AutomaticNameMode bool `json:"automaticNameMode"`
TailnetLockKey string `json:"tailnetLockKey"` //未实现
ShareID string `json:"shareID"` //未实现
AcceptedShareCount int `json:"acceptedShareCount"` //未实现
ParsedLinuxVersion string `json:"parsedLinuxVersion"` //未实现
}
type machineItem struct {
Id string `json:"id"` //done
StableId string `json:"stableId"` //未实现
Name string `json:"name"` //done
Fqdn string `json:"fqdn"` //未实现
User string `json:"user"` //done
UserNameHead string `json:"usernamehead"` // TODO
Addresses []string `json:"addresses"` //done
Os string `json:"os"` //done
Hostname string `json:"hostname"` //done
IpnVersion string `json:"ipnVersion"` //done
ConnectedToControl bool `json:"connectedToControl"` //done
AvailableUpdateVersion string `json:"availableUpdateVersion"` //未实现
LastSeen string `json:"lastSeen"` //done
Created string `json:"created"` //done
IsExternal bool `json:"isExternal"`
IsEphemeral bool `json:"isEphemeral"`
IsSharedOut bool `json:"issharedout"`
NeverExpires bool `json:"neverExpires"` //done
AllowedIPs []string `json:"allowedIPs"`
ExtraIPs []string `json:"extraIPs"`
AdvertisedIPs []string `json:"advertisedIPs"`
HasSubnets bool `json:"hasSubnets"`
AdvertisedExitNode bool `json:"advertisedExitNode"`
AllowedExitNode bool `json:"allowedExitNode"`
AllowedTags []string `json:"allowedTags"`
InvalidTags []string `json:"invalidTags"`
HasTags bool `json:"hasTags"`
Expires time.Time `json:"expires"`
ExpiryDesc string `json:"expirydesc"`
Endpoints []string `json:"endpoints"`
AutomaticNameMode bool `json:"automaticNameMode"`
}
func IsUpdateAvailable(cur, latest string) bool {
curV := strings.Split(strings.Split(cur, "-")[0], ".")
latestV := strings.Split(strings.Split(latest, "-")[0], ".")
for i := 0; i < len(latestV); i++ {
curInt, _ := strconv.Atoi(curV[i])
latestInt, _ := strconv.Atoi(latestV[i])
if curInt < latestInt {
return true
}
}
return false
}
// 控制台获取设备信息列表的API
func (h *Mirage) ConsoleMachinesAPI(
w http.ResponseWriter,
r *http.Request,
) {
user, err := h.verifyTokenIDandGetUser(w, r)
if err != nil || user.CheckEmpty() {
h.doAPIResponse(w, "用户信息核对失败:"+err.Error(), nil)
return
}
OrgMachines, err := h.ListMachinesByOrgID(user.OrganizationID)
if err != nil {
h.doAPIResponse(w, "查询用户节点列表失败", nil)
return
}
mlist := make([]machineItem, 0)
for _, machine := range OrgMachines {
tz, _ := time.LoadLocation("Asia/Shanghai")
tmpMachine := machineItem{
Id: strconv.FormatInt(machine.ID, 10),
Name: machine.GivenName,
User: machine.User.Name,
UserNameHead: string([]rune(machine.User.Display_Name)[0]),
Os: machine.HostInfo.OS,
Hostname: machine.HostInfo.Hostname,
IpnVersion: machine.HostInfo.IPNVersion,
Created: machine.CreatedAt.In(tz).Format("2006年01月02日 15:04:05"),
LastSeen: machine.LastSeen.In(tz).Format("2006年01月02日 15:04:05"),
ConnectedToControl: machine.isOnline(),
AllowedTags: machine.ForcedTags,
InvalidTags: []string{},
HasTags: machine.ForcedTags != nil && len(machine.ForcedTags) > 0,
IsEphemeral: machine.isEphemeral(),
NeverExpires: *machine.Expiry == time.Time{},
Expires: *machine.Expiry,
Endpoints: machine.Endpoints,
AutomaticNameMode: machine.AutoGenName,
}
switch machine.HostInfo.OS {
case "linux":
if IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.Linux.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.Linux.Version, "-")[0]
}
case "windows":
if IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.Win.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.Win.Version, "-")[0]
}
case "macOS":
if h.cfg.ClientVersion.MacStore.Version != "" && IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.MacStore.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.MacStore.Version, "-")[0]
} else if IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.MacTestFlight.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.MacTestFlight.Version, "-")[0]
}
case "iOS":
if h.cfg.ClientVersion.IOSStore.Version != "" && IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.IOSStore.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.IOSStore.Version, "-")[0]
} else if IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.IOSTestFlight.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.IOSTestFlight.Version, "-")[0]
}
case "android":
if IsUpdateAvailable(machine.HostInfo.IPNVersion, h.cfg.ClientVersion.Android.Version) {
tmpMachine.AvailableUpdateVersion = strings.Split(h.cfg.ClientVersion.Android.Version, "-")[0]
}
}
if machine.User.Organization.EnableMagic {
tmpMachine.Fqdn = machine.GivenName + "." + machine.User.Organization.MagicDnsDomain
}
// 处理路由部分
machineRoutes, err := h.GetMachineRoutes(&machine)
if err != nil {
h.doAPIResponse(w, "查询设备路由失败", nil)
return
}
for _, route := range machineRoutes {
if route.isExitRoute() {
if route.Advertised {
tmpMachine.AdvertisedExitNode = true
if route.Enabled {
tmpMachine.AllowedExitNode = true
}
}
} else {
if route.Advertised {
tmpMachine.HasSubnets = true
routeV := netip.Prefix(route.Prefix).String()
if err != nil {
h.doAPIResponse(w, "子网路由地址转换失败", nil)
return
}
tmpMachine.AdvertisedIPs = append(tmpMachine.AdvertisedIPs, routeV)
if route.Enabled {
tmpMachine.AllowedIPs = append(tmpMachine.AllowedIPs, routeV)
} else {
tmpMachine.ExtraIPs = append(tmpMachine.ExtraIPs, routeV)
}
}
}
}
if !tmpMachine.NeverExpires {
ExpiryDuration := time.Until(*machine.Expiry)
tmpMachine.ExpiryDesc = convExpiryToStr(ExpiryDuration)
}
if machine.IPAddresses[0].Is4() {
tmpMachine.Addresses = []string{
machine.IPAddresses[0].String(),
machine.IPAddresses[1].String()}
} else if machine.IPAddresses[1].Is4() {
tmpMachine.Addresses = []string{
machine.IPAddresses[1].String(),
machine.IPAddresses[0].String()}
}
mlist = append(mlist, tmpMachine)
}
h.doAPIResponse(w, "", struct {
Machines []machineItem `json:"machines"`
}{
Machines: mlist,
})
}
type MachineDebugInfo struct {
MappingVariesByDestIP bool `json:"mappingVariesByDestIP"`
HairPinning bool `json:"hairPinning"`
IPv6 bool `json:"ipv6"`
UDP bool `json:"udp"`
UPnP bool `json:"upnp"`
PMP bool `json:"pmp"`
PCP bool `json:"pcp"`
Latency map[string]*Latency `json:"latency"` // "derp区域编号"-结构体
}
type Latency struct {
RegionName string `json:"regionName"`
Preferred bool `json:"preferred"`
LatencyMs float64 `json:"latencyMs"`
}
func (m *Mirage) ConsoleMachineDebugAPI(
w http.ResponseWriter,
r *http.Request,
) {
user, err := m.verifyTokenIDandGetUser(w, r)
if err != nil || user.CheckEmpty() {
m.doAPIResponse(w, "用户信息核对失败:"+err.Error(), nil)
return
}
targetMIPStr := r.URL.Query().Get("ip")
if targetMIPStr == "" {
m.doAPIResponse(w, "用户查询IP为空", nil)
return
}
targetMIP, err := netip.ParseAddr(targetMIPStr)
if err != nil {
m.doAPIResponse(w, "用户请求IP解析失败", nil)
return
}
targetMachine := m.GetMachineByIP(targetMIP)
if targetMachine == nil || targetMachine.User.OrganizationID != user.OrganizationID {
m.doAPIResponse(w, "组织内无此设备", nil)
return
}
resData := &MachineDebugInfo{
MappingVariesByDestIP: targetMachine.HostInfo.NetInfo.MappingVariesByDestIP.EqualBool(true),
HairPinning: targetMachine.HostInfo.NetInfo.HairPinning.EqualBool(true),
IPv6: targetMachine.HostInfo.NetInfo.WorkingIPv6.EqualBool(true),
UDP: targetMachine.HostInfo.NetInfo.WorkingUDP.EqualBool(true),
UPnP: targetMachine.HostInfo.NetInfo.UPnP.EqualBool(true),
PMP: targetMachine.HostInfo.NetInfo.PMP.EqualBool(true),
PCP: targetMachine.HostInfo.NetInfo.PCP.EqualBool(true),
Latency: make(map[string]*Latency),
}
derpMap, err := m.LoadOrgDERPs(targetMachine.User.OrganizationID)
if err != nil {
m.doAPIResponse(w, "获取组织中继信息失败", nil)
return
}
if targetMachine.HostInfo.NetInfo != nil && targetMachine.HostInfo.NetInfo.PreferredDERP != 0 {
for derpname, latency := range targetMachine.HostInfo.NetInfo.DERPLatency {
ipver := strings.Split(derpname, "-")[1]
derpRegionIDStr := strings.Split(derpname, "-")[0]
derpRegionID, _ := strconv.Atoi(derpRegionIDStr)
if resData.Latency[derpRegionIDStr] == nil {
resData.Latency[derpRegionIDStr] = &Latency{
RegionName: derpMap.Regions[derpRegionID].RegionName,
Preferred: targetMachine.HostInfo.NetInfo.PreferredDERP == derpRegionID,
}
}
if ipver == "v4" {
if peerlatency, ok := targetMachine.HostInfo.NetInfo.DERPLatency[derpRegionIDStr+"-v6"]; ok {
if latency < peerlatency {
resData.Latency[derpRegionIDStr].LatencyMs = latency * 1000
}
} else {
resData.Latency[derpRegionIDStr].LatencyMs = latency * 1000
}
} else {
if peerlatency, ok := targetMachine.HostInfo.NetInfo.DERPLatency[derpRegionIDStr+"-v4"]; ok {
if latency < peerlatency {
resData.Latency[derpRegionIDStr].LatencyMs = latency * 1000
}
} else {
resData.Latency[derpRegionIDStr].LatencyMs = latency * 1000
}
}
}
}
m.doAPIResponse(w, "", resData)
}
func (h *Mirage) ConsoleMachinesUpdateAPI(
writer http.ResponseWriter,
req *http.Request,
) {
user, err := h.verifyTokenIDandGetUser(writer, req)
if err != nil || user.CheckEmpty() {
h.doAPIResponse(writer, "用户信息核对失败:"+err.Error(), nil)
return
}
err = req.ParseForm()
if err != nil {
h.doAPIResponse(writer, "用户请求解析失败:"+err.Error(), nil)
return
}
reqData := make(map[string]interface{})
json.NewDecoder(req.Body).Decode(&reqData)
reqMID, ok := reqData["mid"].(string)
if !ok {
h.doAPIResponse(writer, "用户请求mid解析失败", nil)
return
}
MachineID, err := strconv.ParseInt(reqMID, 0, 64)
if err != nil {
h.doAPIResponse(writer, "用户请求mid处理失败", nil)
return
}
toUpdateMachine, err := h.GetMachineByID(MachineID)
if err != nil {
h.doAPIResponse(writer, "查询用户设备失败", nil)
return
}
/*
if toUpdateMachine.User.ID != user.ID {
h.doAPIResponse(writer, "用户没有该权限", nil)
return
}
*/
reqState, ok := reqData["state"].(string)
if !ok {
h.doAPIResponse(writer, "用户请求state解析失败", nil)
return
}
switch reqState {
case "set-expires": //切换密钥永不过期设置
msg, err := h.setMachineExpiry(toUpdateMachine)
if err != nil {
h.doAPIResponse(writer, msg, nil)
} else {
resData := machineData{
NeverExpires: *toUpdateMachine.Expiry == time.Time{},
Expires: msg,
}
h.doAPIResponse(writer, "", resData)
}
case "rename-node": //设置设备名称
newName := reqData["nodeName"].(string)
msg, _, err := h.setMachineName(toUpdateMachine, newName)
if err != nil {
h.doAPIResponse(writer, msg, nil)
} else {
resData := machineData{
AutomaticNameMode: toUpdateMachine.AutoGenName,
Name: toUpdateMachine.GivenName,
Hostname: toUpdateMachine.Hostname,
NeverExpires: *toUpdateMachine.Expiry == time.Time{},
Expires: msg,
}
h.doAPIResponse(writer, "", resData)
}
case "set-route-settings": //设置子网转发及出口节点
allowedIPsInterface := reqData["allowedIPs"].([]interface{})
allowExitNode := reqData["allowedExitNode"].(bool)
allowedIPs := new([]string)
for _, ip := range allowedIPsInterface {
*allowedIPs = append(*allowedIPs, ip.(string))
}
msg, err := h.setMachineSubnet(toUpdateMachine, allowExitNode, *allowedIPs)
if err != nil {
h.doAPIResponse(writer, msg, nil)
return
} else {
resData := machineData{
AutomaticNameMode: toUpdateMachine.AutoGenName,
Name: toUpdateMachine.GivenName,
Hostname: toUpdateMachine.Hostname,
NeverExpires: *toUpdateMachine.Expiry == time.Time{},
Expires: msg,
}
machineRoutes, err := h.GetMachineRoutes(toUpdateMachine)
if err != nil {
h.doAPIResponse(writer, "查询设备路由失败", nil)
return
}
for _, route := range machineRoutes {
if route.isExitRoute() {
if route.Advertised {
resData.AdvertisedExitNode = true
if route.Enabled {
resData.AllowedExitNode = true
}
}
} else {
if route.Advertised {
resData.HasSubnets = true
routeV := netip.Prefix(route.Prefix).String()
if err != nil {
h.doAPIResponse(writer, "子网路由地址转换失败", nil)
return
}
resData.AdvertisedIPs = append(resData.AdvertisedIPs, routeV)
if route.Enabled {
resData.AllowedIPs = append(resData.AllowedIPs, routeV)
} else {
resData.ExtraIPs = append(resData.ExtraIPs, routeV)
}
}
}
}
h.doAPIResponse(writer, "", resData)
}
case "set-tags": //设置设备标签
reqTags := reqData["tags"].([]interface{})
setTags := make([]string, len(reqTags))
for i, tag := range reqTags {
setTags[i] = tag.(string)
}
msg, err := h.setMachineTags(toUpdateMachine, setTags)
if err != nil {
h.doAPIResponse(writer, msg, nil)
} else {
invalidTags := []string{}
allowedTags := []string{}
org, err := h.GetOrgnaizationByID(user.OrganizationID)
if err != nil {
h.doAPIResponse(writer, msg, nil)
}
for _, tag := range setTags {
if _, ok := org.AclPolicy.TagOwners[tag]; ok {
allowedTags = append(allowedTags, tag)
} else {
invalidTags = append(invalidTags, tag)
}
}
resData := machineData{
AutomaticNameMode: toUpdateMachine.AutoGenName,
Name: toUpdateMachine.GivenName,
Hostname: toUpdateMachine.Hostname,
NeverExpires: *toUpdateMachine.Expiry == time.Time{},
Expires: msg,
HasTags: len(setTags) > 0,
AllowedTags: allowedTags,
InvalidTags: invalidTags,
}
h.doAPIResponse(writer, "", resData)
}
}
}
// 删除设备API
func (h *Mirage) ConsoleRemoveMachineAPI(
writer http.ResponseWriter,
req *http.Request,
) {
user, err := h.verifyTokenIDandGetUser(writer, req)
if err != nil || user.CheckEmpty() {
h.doAPIResponse(writer, "用户信息核对失败:"+err.Error(), nil)
return
}
UserMachines, err := h.ListMachinesByOrgID(user.OrganizationID)
if err != nil {
h.doAPIResponse(writer, "用户设备检索失败:"+err.Error(), nil)
return
}
err = req.ParseForm()
if err != nil {
h.doAPIResponse(writer, "用户请求解析失败:"+err.Error(), nil)
return
}
reqData := make(map[string]string)
json.NewDecoder(req.Body).Decode(&reqData)
wantRemoveID := reqData["mid"]
for _, machine := range UserMachines {
if strconv.FormatInt(machine.ID, 10) == wantRemoveID {
err = h.HardDeleteMachine(&machine)
if err != nil {
h.doAPIResponse(writer, "用户设备删除失败:"+err.Error(), nil)
return
}
h.NotifyNaviOrgNodesChange(user.OrganizationID, "", machine.NodeKey)
h.doAPIResponse(writer, "", nil)
return
}
}
h.doAPIResponse(writer, "未找到目标设备", nil)
}
// 切换设备密钥是否禁用过期
func (h *Mirage) setMachineExpiry(machine *Machine) (string, error) {
if (*machine.Expiry != time.Time{}) {
err := h.RefreshMachine(machine, time.Time{})
if err != nil {
return "设备密钥过期禁用失败", err
} else {
return "", err
}
} else {
expiryDuration := time.Hour * 24 * time.Duration(machine.User.Organization.ExpiryDuration)
newExpiry := time.Now().Add(expiryDuration)
err := h.RefreshMachine(machine, newExpiry)
if err != nil {
return "设备密钥过期启用失败", err
} else {
return convExpiryToStr(expiryDuration), nil
}
}
}
// 三个返回值:msg、nowName、err
func (h *Mirage) setMachineName(machine *Machine, newName string) (string, string, error) {
newGiveName, err := h.setAutoGenName(machine, newName)
if err != nil {
return "设置主机名失败", "", err
}
return "", newGiveName, nil
}
func (h *Mirage) setMachineTags(machine *Machine, tags []string) (string, error) {
err := h.SetTags(machine, tags)
if err != nil {
return "设置设备标签失败", err
}
return "", nil
}
func (h *Mirage) setMachineSubnet(machine *Machine, ExitNodeEnable bool, allowedIPs []string) (string, error) {
machineRoutes, err := h.GetMachineRoutes(machine)
if err != nil {
return "获取设备路由设置失败", err
}
for _, r := range machineRoutes {
if r.isExitRoute() {
if ExitNodeEnable {
err = h.EnableRoute(uint64(r.ID))
} else {
err = h.DisableRoute(uint64(r.ID))
}
if err != nil {
return "设置设备出口节点状态失败", err
}
} else {
err = h.DisableRoute(uint64(r.ID))
if err != nil {
return "设置设备出口节点状态失败", err
}
}
}
err = h.enableRoutes(machine, allowedIPs...)
if err != nil {
return "设置设备子网路由状态失败", err
}
return "", nil
}