diff --git a/cockpit_web/src/App.vue b/cockpit_web/src/App.vue index eeba913..38d274b 100644 --- a/cockpit_web/src/App.vue +++ b/cockpit_web/src/App.vue @@ -34,6 +34,7 @@ const currentRoute = computed(() => { if (curPath.substring(0, 6) == "/users") return "users"; if (curPath.substring(0, 8) == "/tenants") return "tenants"; if (curPath.substring(0, 8) == "/setting") return "setting"; + if (curPath.substring(0, 5) == "/navi") return "navi"; }); const serviceSwitch = ref(null); @@ -344,7 +345,40 @@ function doLogout() {
用户
- + +
+ + + + + +
司南
+
+
{ + if (toastShow.value) { + setTimeout(function () { + toastShow.value = false; + }, 5000); + } +}); + +const selectNaviNode = ref({}); +function mouseOnNaviNode(u) { + selectNaviNode.value = u; + NaviNodeBtnShow.value = true; +} +function mouseLeaveNaviNode() { + NaviNodeBtnShow.value = false; +} + +const NaviNodeMenuShow = ref(false); +const NaviNodeBtnShow = ref(false); + +const removeNaviNodeShow = ref(false); +function showRemoveNaviNode() { + NaviNodeBtnShow.value = false; + closeNaviNodeMenu(); + removeNaviNodeShow.value = true; +} + +const editNaviNodeShow = ref(false); +function showEditNaviNode() { + NaviNodeBtnShow.value = false; + closeNaviNodeMenu(); + editNaviNodeShow.value = true; +} + +const deployDERPShow = ref(false); +function showDeployDERP() { + deployDERPShow.value = true; +} + +function addNaviDone(newlist) { + toastShow.value = true; + toastMsg.value = "添加成功"; + NaviRegionList.value = newlist; + deployDERPShow.value = false; +} + +function doRemoveTenant() { + axios + .post("/cockpit/api/tenants", { + tenantID: selectTenant.value["id"], + action: "delete_tenant", + }) + .then(function (response) { + if (response.data["status"] != "success") { + toastMsg.value = response.data["status"].substring(6); + toastShow.value = true; + } else { + removeTenantShow.value = false; + toastMsg.value = "已删除 " + selectTenant.value["name"]; + toastShow.value = true; + getTenants().then().catch(); + } + }) + .catch(function (error) { + toastMsg.value = error; + toastShow.value = true; + }); +} + +function doUpdateTenant(newV) { + axios + .post("/cockpit/api/tenants", { + tenantID: selectTenant.value["id"], + action: "update_tenant", + newValue: newV, + }) + .then(function (response) { + if (response.data["status"] != "success") { + toastMsg.value = response.data["status"].substring(6); + toastShow.value = true; + } else { + editTenantShow.value = false; + toastMsg.value = "已更新 " + selectTenant.value["name"] + " 租户配置"; + toastShow.value = true; + getTenants().then().catch(); + } + }) + .catch(function (error) { + toastMsg.value = error; + toastShow.value = true; + }); +} + +//数据填充控制部分 +const NaviRegionList = ref([]); +const NaviRegionNum = computed(() => { + if (NaviRegionList.value == null) { + return 0; + } + return NaviRegionList.value.length; +}); +let getNaviRegionsIntID; +function getNaviRegions() { + return new Promise((resolve, reject) => { + axios + .get("/cockpit/api/derp/query") + .then(function (response) { + if (response.data["status"] != "success") { + toastMsg.value = "获租户信息出错:" + response.data["status"].substring(6); + toastShow.value = true; + reject(); + } + + // 处理成功情况 + NaviRegionList.value = response.data["data"]; + resolve(); + }) + .catch(function (error) { + // 处理错误情况 + toastMsg.value = "获取用户信息出错:" + error; + toastShow.value = true; + reject(); + }); + }); +} +onMounted(() => { + refreshNaviNodeMenuPos(); + window.addEventListener("resize", refreshNaviNodeMenuPos); + window.addEventListener("scroll", refreshNaviNodeMenuPos); + + getNaviRegions().then().catch(); + getNaviRegionsIntID = setInterval(() => { + getNaviRegions().then().catch(); + }, 20000); +}); + +onUnmounted(() => { + window.removeEventListener("resize", refreshNaviNodeMenuPos); + window.removeEventListener("scroll", refreshNaviNodeMenuPos); +}); + +onBeforeRouteLeave(() => { + clearInterval(getNaviRegionsIntID); +}); + + + + + diff --git a/cockpit_web/src/components/NaviNodeMenu.vue b/cockpit_web/src/components/NaviNodeMenu.vue new file mode 100644 index 0000000..07469a5 --- /dev/null +++ b/cockpit_web/src/components/NaviNodeMenu.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/cockpit_web/src/derp/Deploy.vue b/cockpit_web/src/derp/Deploy.vue new file mode 100644 index 0000000..a793605 --- /dev/null +++ b/cockpit_web/src/derp/Deploy.vue @@ -0,0 +1,565 @@ + + + + + diff --git a/cockpit_web/src/main.js b/cockpit_web/src/main.js index ffa16b3..0f38caa 100644 --- a/cockpit_web/src/main.js +++ b/cockpit_web/src/main.js @@ -6,6 +6,7 @@ import Setting from './Settings.vue' import Tenants from './Tenants.vue' import RegAdmin from './RegAdmin.vue' import Login from './Login.vue' +import DERPs from './DERPs.vue' import VueClickAway from "vue3-click-away" @@ -15,7 +16,8 @@ const routes = [ { path: '/login', component: Login }, { path: '/setting', redirect: '/setting/general' }, { path: '/setting/:setpart', component: Setting }, - {path:'/tenants',component:Tenants}, + { path:'/tenants',component:Tenants }, + { path:'/navi',component:DERPs }, ] const router = createRouter({ history: createWebHashHistory(), diff --git a/cockpit_web/src/setpart/ClientPublish.vue b/cockpit_web/src/setpart/ClientPublish.vue index 3c9e0fb..ff83a5f 100644 --- a/cockpit_web/src/setpart/ClientPublish.vue +++ b/cockpit_web/src/setpart/ClientPublish.vue @@ -364,6 +364,16 @@ function publishWin() { : "未设置" }}
+
diff --git a/controller/app.go b/controller/app.go index 0b19bad..7acfe13 100644 --- a/controller/app.go +++ b/controller/app.go @@ -368,7 +368,7 @@ func (h *Mirage) Serve(ctrlChn chan CtrlMsg) error { var err error // Fetch an initial DERP Map before we start serving - h.DERPMap, err = LoadDERPMapFromURL(h.cfg.DERPURL) + h.DERPMap, err = h.LoadDERPMapFromURL(h.cfg.DERPURL) if err != nil { return err } diff --git a/controller/cockpit.go b/controller/cockpit.go index 1985472..5ac416b 100644 --- a/controller/cockpit.go +++ b/controller/cockpit.go @@ -25,9 +25,8 @@ import ( ) type Cockpit struct { - db *gorm.DB - Addr string - ServerURL string + db *gorm.DB + Addr string serviceState bool CtrlChn chan CtrlMsg @@ -103,6 +102,18 @@ func (c *Cockpit) GetSysCfg() *SysConfig { if err != nil || cfg == nil || len(cfg) == 0 { return nil } + if cfg[0].NaviDeployKey == "" { + pri, pub, err := genSSHKeypair() + if err != nil { + log.Fatal().Msg(err.Error()) + } + cfg[0].NaviDeployPub = pub + cfg[0].NaviDeployKey = pri + err = c.db.Save(&cfg[0]).Error + if err != nil { + log.Fatal().Msg(err.Error()) + } + } return &cfg[0] } @@ -129,11 +140,13 @@ func (c *Cockpit) createRouter() *mux.Router { cockpit_router.HandleFunc("/api/service/stop", c.DoServiceStop).Methods(http.MethodPost) cockpit_router.HandleFunc("/api/tenants", c.CAPIPostTenants).Methods(http.MethodPost) cockpit_router.HandleFunc("/api/publish/{os}", c.CAPIPublishClient).Methods(http.MethodPost) + cockpit_router.HandleFunc("/api/derp/add", c.CAPIAddDERP).Methods(http.MethodPost) cockpit_router.HandleFunc("/api/logout", c.Logout).Methods(http.MethodGet) cockpit_router.HandleFunc("/api/service/state", c.GetServiceState).Methods(http.MethodGet) cockpit_router.HandleFunc("/api/setting/general", c.GetSettingGeneral).Methods(http.MethodGet) cockpit_router.HandleFunc("/api/tenants", c.CAPIGetTenant).Methods(http.MethodGet) + cockpit_router.HandleFunc("/api/derp/query", c.CAPIQueryDERP).Methods(http.MethodGet) cockpit_router.PathPrefix("").Handler(http.StripPrefix("/cockpit", http.FileServer(http.FS(cockpitDir)))) diff --git a/controller/cockpit_api_derp.go b/controller/cockpit_api_derp.go new file mode 100644 index 0000000..84f12c1 --- /dev/null +++ b/controller/cockpit_api_derp.go @@ -0,0 +1,313 @@ +package controller + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + _ "embed" + "encoding/json" + "encoding/pem" + "errors" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/google/uuid" + "github.com/pkg/sftp" + "github.com/rs/zerolog/log" + "golang.org/x/crypto/ssh" +) + +type NaviDeployREQ struct { + RegionCode string `json:"RegionCode"` + RegionName string `json:"RegionName"` + + NaviNode NaviNode `json:"NaviNode"` +} + +// 接受/cockpit/api/derp/query的Get请求,用于进行DERP查询 +func (c *Cockpit) CAPIQueryDERP( + w http.ResponseWriter, + r *http.Request, +) { + resData := []struct { + Region NaviRegion `json:"Region"` + Nodes []NaviNode `json:"Nodes"` + }{} + naviRegions := c.ListNaviRegions() + for _, naviRegion := range naviRegions { + naviNodes := c.ListNaviNodes(naviRegion.ID) + for index := range naviNodes { // 清除掉敏感信息 + naviNodes[index].NaviKey = "" + naviNodes[index].SSHPwd = "" + naviNodes[index].DNSKey = "" + } + resData = append(resData, struct { + Region NaviRegion `json:"Region"` + Nodes []NaviNode `json:"Nodes"` + }{ + Region: naviRegion, + Nodes: naviNodes, + }) + } + c.doAPIResponse(w, "", resData) +} + +// 接受/cockpit/api/derp/add的Post请求,用于进行DERP登记以及部署 +func (c *Cockpit) CAPIAddDERP( + w http.ResponseWriter, + r *http.Request, +) { + reqData := NaviDeployREQ{} + json.NewDecoder(r.Body).Decode(&reqData) + + remoteAuth := []ssh.AuthMethod{} + if reqData.NaviNode.SSHPwd != "" { + remoteAuth = append(remoteAuth, ssh.Password(reqData.NaviNode.SSHPwd)) + } else { + var keyData []byte + + sysCfg := c.GetSysCfg() + if sysCfg != nil && sysCfg.NaviDeployKey != "" { + keyData = []byte(sysCfg.NaviDeployKey) + } else { + c.doAPIResponse(w, "不存在远程主机认证信息", nil) + return + } + + pemBlock, _ := pem.Decode(keyData) + if pemBlock == nil { + c.doAPIResponse(w, "解析私钥失败", nil) + return + } + + ecdsaKey, err := x509.ParseECPrivateKey(pemBlock.Bytes) + if err != nil { + c.doAPIResponse(w, "解析私钥失败:"+err.Error(), nil) + return + } + pk, err := ssh.NewSignerFromKey(ecdsaKey) + if err != nil { + c.doAPIResponse(w, "解析私钥失败:"+err.Error(), nil) + return + } + remoteAuth = append(remoteAuth, ssh.PublicKeys(pk)) + } + + client, err := ssh.Dial("tcp", reqData.NaviNode.SSHAddr, &ssh.ClientConfig{ + User: "root", + Auth: remoteAuth, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) + if err != nil { + c.doAPIResponse(w, "连接远程主机失败:"+err.Error(), nil) + return + } + archCheckSession, err := client.NewSession() + if err != nil { + c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil) + return + } + defer archCheckSession.Close() + + // 检查目标机处理器架构以便传送对应版本 + arch, err := archCheckSession.Output("arch") + if err != nil { + c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil) + return + } + archStr := strings.TrimSuffix(string(arch), "\n") + if archStr != "x86_64" && archStr != "aarch64" { + c.doAPIResponse(w, "不支持的处理器架构:"+archStr, nil) + return + } + + // 开始处理服务端数据信息 + if reqData.NaviNode.NaviRegionID == -1 { + naviRegion := &NaviRegion{ + RegionCode: reqData.RegionCode, + RegionName: reqData.RegionName, + } + naviRegion = c.CreateNaviRegion(naviRegion) + if naviRegion == nil { + c.doAPIResponse(w, "创建区域失败", nil) + return + } + reqData.NaviNode.NaviRegionID = naviRegion.ID + } else { + naviRegion := c.GetNaviRegion(reqData.NaviNode.NaviRegionID) + if naviRegion == nil { + c.doAPIResponse(w, "区域不存在", nil) + return + } + } + + // TODO: 是否需要检查目标机曾部署过司南 + derpid := uuid.New().String() + reqData.NaviNode.ID = derpid + reqData.NaviNode.Arch = archStr + naviNode := c.CreateNaviNode(&reqData.NaviNode) + if naviNode == nil { + c.doAPIResponse(w, "新建司南档案失败", nil) + return + } + + //TODO: 司南建档成功后在目标机执行部署启动 + // 停止服务 + systemdStopSession, err := client.NewSession() + if err != nil { + c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil) + return + } + defer systemdStopSession.Close() + + _, err = systemdStopSession.Output("systemctl stop MirageNavi") + if err != nil { + c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil) + return + } + + err = sshSendFile(client, "download/"+archStr+"/MirageNavi", "/usr/local/bin/MirageNavi") + if err != nil { + c.doAPIResponse(w, "传送司南客户端到目标服务器失败:"+err.Error(), nil) + return + } + // 进行赋权 + chmodSession, err := client.NewSession() + if err != nil { + c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil) + return + } + defer chmodSession.Close() + + _, err = chmodSession.Output("chmod +x /usr/local/bin/MirageNavi") + if err != nil { + c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil) + return + } + + serviceScript := + `[Unit] +Description=Mirage Navigation Node Service + +[Service] +ExecStart=/usr/local/bin/MirageNavi -ctrl-url ${MIRAGE_CTRL_URL} -id ${MIRAGE_NAVI_ID} >> ${LOG_DIR}/MirageNavi.log 2>&1 +Restart=always +User=root +Group=root +Environment=PATH=/usr/local/bin:/usr/bin:/bin +Environment=LOG_DIR=/var/log +Environment=MIRAGE_CTRL_URL=https://` + c.GetSysCfg().ServerURL + ` +Environment=MIRAGE_NAVI_ID=` + derpid + ` + +[Install] +WantedBy=multi-user.target` + + // 将文本写入文件 + err = ioutil.WriteFile("download/"+derpid+".service.tmp", []byte(serviceScript), 0644) + if err != nil { + c.doAPIResponse(w, "创建临时服务文件失败:"+err.Error(), nil) + return + } + err = sshSendFile(client, "download/"+derpid+".service.tmp", "/etc/systemd/system/MirageNavi.service") + if err != nil { + c.doAPIResponse(w, "传送服务文件到目标服务器失败:"+err.Error(), nil) + return + } + err = os.Remove("download/" + derpid + ".service.tmp") + if err != nil { + log.Warn().Caller().Err(err).Msg("删除服务临时文件失败") + return + } + + // 重置服务配置 + systemdReloadSession, err := client.NewSession() + if err != nil { + c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil) + return + } + defer systemdReloadSession.Close() + + _, err = systemdReloadSession.Output("systemctl daemon-reload") + if err != nil { + c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil) + return + } + + // 启动服务 + systemdEnableSession, err := client.NewSession() + if err != nil { + c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil) + return + } + defer systemdEnableSession.Close() + + _, err = systemdEnableSession.Output("systemctl enable --now MirageNavi") + if err != nil { + c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil) + return + } + + c.CAPIQueryDERP(w, r) +} + +func genSSHKeypair() (priKey, pubKey string, err error) { + var privateKey *ecdsa.PrivateKey + var publicKey ssh.PublicKey + + if privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err != nil { + return + } + if publicKey, err = ssh.NewPublicKey(privateKey.Public()); err != nil { + return + } + + pubKey = string(ssh.MarshalAuthorizedKey(publicKey)) + var priKeyData []byte + if priKeyData, err = x509.MarshalECPrivateKey(privateKey); err != nil { + return + } + priKey = string(pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: priKeyData, + })) + + return +} + +func sshSendFile(sshClient *ssh.Client, localFilePath string, remoteFilePath string) error { + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + return err + } + defer sftpClient.Close() + + remoteFile, err := sftpClient.Create(remoteFilePath) + if err != nil { + return err + } + defer remoteFile.Close() + + localFile, err := os.Open(localFilePath) + if err != nil { + return err + } + defer localFile.Close() + + n, err := io.Copy(remoteFile, localFile) + if err != nil { + return err + } + + localFileInfo, err := os.Stat(localFilePath) + if err != nil { + return err + } + if n != localFileInfo.Size() { + return errors.New("文件大小不一致,传输失败") + } + return nil +} diff --git a/controller/cockpit_syscfg.go b/controller/cockpit_syscfg.go index dedca11..ee62222 100644 --- a/controller/cockpit_syscfg.go +++ b/controller/cockpit_syscfg.go @@ -40,6 +40,8 @@ type SysConfig struct { GoogleCfg GoogleCfg AppleCfg AppleCfg + NaviDeployPub string + NaviDeployKey string ClientVersion ClientVersionInfo CreatedAt time.Time @@ -68,6 +70,7 @@ type GeneralCfg struct { GoogleCfg GoogleCfg `json:"google"` AppleCfg AppleCfg `json:"apple"` + NaviDeployPub string `json:"navi_deploy_pub"` ClientVersion ClientVersionInfo `json:"client_version"` } @@ -93,6 +96,7 @@ func (s *SysConfig) toGeneralCfg() GeneralCfg { GoogleCfg: s.GoogleCfg, AppleCfg: s.AppleCfg, + NaviDeployPub: s.NaviDeployPub, ClientVersion: s.ClientVersion, } } diff --git a/controller/db.go b/controller/db.go index 8cb6f25..4d64e96 100644 --- a/controller/db.go +++ b/controller/db.go @@ -34,6 +34,16 @@ func (dp *DataPool) InitCockpitDB() error { if err != nil { return err } + + err = dp.db.AutoMigrate(&NaviRegion{}) + if err != nil { + return err + } + err = dp.db.AutoMigrate(&NaviNode{}) + if err != nil { + return err + } + return err } diff --git a/controller/derp.go b/controller/derp.go index 9e50c94..e092940 100644 --- a/controller/derp.go +++ b/controller/derp.go @@ -10,7 +10,147 @@ import ( "tailscale.com/tailcfg" ) -func LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) { +type NaviRegion struct { + ID int `gorm:"primary_key;unique;not null" json:"RegionID"` + OrgID int64 `gorm:";not null" json:"OrgID"` // 0代表全局向导 + RegionCode string `gorm:"not null" json:"RegionCode"` + RegionName string `gorm:"not null" json:"RegionName"` + //这个不知道有何用 Avoid bool `json:",omitempty"` +} +type NaviNode struct { + ID string `gorm:"primary_key;unique;not null" json:"Name"` //映射到DERPNode的Name + NaviKey string `json:"NaviKey"` //记录DERPNode的MachineKey公钥 + + NaviRegionID int `gorm:"not null" json:"RegionID"` //映射到DERPNode的RegionID + NaviRegion *NaviRegion `gorm:"foreignKey:NaviRegionID;references:ID" json:"-"` //映射到DERPNode的RegionID + + HostName string `json:"HostName"` //这个不需要独有,但是否必须域名呢? + //这个不用? CertName string `json:",omitempty"` + + IPv4 string `json:"IPv4"` // 不是ipv4地址则失效,为none则禁用ipv4 + IPv6 string `json:"IPv6"` // 不是ipv6地址则失效,为none则禁用ipv6 + + NoSTUN bool `json:"NoSTUN"` //禁用STUN + STUNPort int `json:"STUNPort"` //0代表3478,-1代表禁用 + + NoDERP bool `json:"NoDERP"` //禁用DERP + DERPPort int `json:"DERPPort"` //0代表443 + + SSHAddr string `json:"SSHAddr"` //SSH地址 + SSHPwd string `json:"SSHPwd"` //SSH口令 + DNSProvider string `json:"DNSProvider"` //DNS服务商 + DNSID string `json:"DNSID"` //DNS服务商的ID + DNSKey string `json:"DNSKey"` //DNS服务商的Key + + Arch string `json:"Arch"` //所在环境架构,x86_64或aarch64 +} + +func (c *Cockpit) toDERPRegion(nr NaviRegion) (tailcfg.DERPRegion, error) { + nodes := c.ListNaviNodes(nr.ID) + derpNodes, err := c.toDERPNodes(nodes) + if err != nil { + return tailcfg.DERPRegion{}, err + } + return tailcfg.DERPRegion{ + RegionID: nr.ID, + RegionCode: nr.RegionCode, + RegionName: nr.RegionName, + Nodes: derpNodes, + }, nil +} + +func (m *Cockpit) toDERPNodes(nodes []NaviNode) ([]*tailcfg.DERPNode, error) { + derpNodes := make([]*tailcfg.DERPNode, len(nodes)) + for index, node := range nodes { + derpNode, err := m.toDERPNode(node) + if err != nil { + return nil, err + } + derpNodes[index] = derpNode + } + return derpNodes, nil + +} + +func (c *Cockpit) toDERPNode(node NaviNode) (*tailcfg.DERPNode, error) { + derp := &tailcfg.DERPNode{ + Name: node.ID, + RegionID: node.NaviRegionID, + HostName: node.HostName, + IPv4: node.IPv4, + IPv6: node.IPv6, + STUNPort: node.STUNPort, + STUNOnly: node.NoDERP, + DERPPort: node.DERPPort, + } + if node.NoSTUN { + derp.STUNPort = -1 + } + return derp, nil +} + +func (c *Cockpit) ListNaviRegions() []NaviRegion { + naviRegions := []NaviRegion{} + if err := c.db.Find(&naviRegions).Error; err != nil { + return nil + } + return naviRegions +} + +func (c *Cockpit) GetNaviRegion(id int) *NaviRegion { + naviRegion := NaviRegion{} + if err := c.db.First(&naviRegion, id).Error; err != nil { + return nil + } + return &naviRegion +} + +func (c *Cockpit) CreateNaviRegion(naviRegion *NaviRegion) *NaviRegion { + if err := c.db.Create(naviRegion).Error; err != nil { + return nil + } + return naviRegion +} + +func (c *Cockpit) UpdateNaviRegion(naviRegion *NaviRegion) *NaviRegion { + if err := c.db.Save(naviRegion).Error; err != nil { + return nil + } + return naviRegion +} + +func (c *Cockpit) ListNaviNodes(regionID int) []NaviNode { + naviNodes := []NaviNode{} + if err := c.db.Preload("NaviRegion").Where("navi_region_id = ?", regionID).Find(&naviNodes).Error; err != nil { + return nil + } + return naviNodes +} + +func (c *Cockpit) GetNaviNode(id string) *NaviNode { + naviNode := NaviNode{} + if err := c.db.Preload("NaviRegion").First(&naviNode, "id = ?", id).Error; err != nil { + return nil + } + return &naviNode +} + +func (c *Cockpit) CreateNaviNode(naviNode *NaviNode) *NaviNode { + if err := c.db.Create(naviNode).Error; err != nil { + return nil + } + return naviNode +} + +func (c *Cockpit) UpdateNaviNode(naviNode *NaviNode) *NaviNode { + if err := c.db.Save(naviNode).Error; err != nil { + return nil + } + return naviNode +} + +// cgao6: 以下为Mirage的实现 +func (m *Mirage) LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) { ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout) defer cancel() @@ -42,5 +182,112 @@ func LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) { Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region") } + // 从数据库读取DERP信息 + naviRegions := m.ListNaviRegions() + if len(naviRegions) != 0 { + for _, nr := range naviRegions { + derpRegion, err := m.toDERPRegion(nr) + if err != nil { + log.Error().Err(err).Msg("Cannot convert NaviRegion to DERPRegion") + return nil, err + } + derpMap.Regions[derpRegion.RegionID] = &derpRegion + } + } + return &derpMap, err } + +func (m *Mirage) toDERPRegion(nr NaviRegion) (tailcfg.DERPRegion, error) { + nodes := m.ListNaviNodes(nr.ID) + derpNodes, err := m.toDERPNodes(nodes) + if err != nil { + return tailcfg.DERPRegion{}, err + } + return tailcfg.DERPRegion{ + RegionID: nr.ID, + RegionCode: nr.RegionCode, + RegionName: nr.RegionName, + Nodes: derpNodes, + }, nil +} +func (m *Mirage) toDERPNodes(nodes []NaviNode) ([]*tailcfg.DERPNode, error) { + derpNodes := make([]*tailcfg.DERPNode, len(nodes)) + for index, node := range nodes { + derpNode, err := m.toDERPNode(node) + if err != nil { + return nil, err + } + derpNodes[index] = derpNode + } + return derpNodes, nil + +} + +func (m *Mirage) toDERPNode(node NaviNode) (*tailcfg.DERPNode, error) { + derp := &tailcfg.DERPNode{ + Name: node.ID, + RegionID: node.NaviRegionID, + HostName: node.HostName, + IPv4: node.IPv4, + IPv6: node.IPv6, + STUNPort: node.STUNPort, + STUNOnly: node.NoDERP, + DERPPort: node.DERPPort, + } + if node.NoSTUN { + derp.STUNPort = -1 + } + return derp, nil +} + +func (m *Mirage) ListNaviRegions() []NaviRegion { + naviRegions := []NaviRegion{} + if err := m.db.Find(&naviRegions).Error; err != nil { + return nil + } + return naviRegions +} + +func (m *Mirage) GetNaviRegion(id int64) *NaviRegion { + naviRegion := NaviRegion{} + if err := m.db.First(&naviRegion, id).Error; err != nil { + return nil + } + return &naviRegion +} + +func (m *Mirage) CreateNaviRegion(naviRegion *NaviRegion) *NaviRegion { + if err := m.db.Create(naviRegion).Error; err != nil { + return nil + } + return naviRegion +} +func (m *Mirage) UpdateNaviRegion(naviRegion *NaviRegion) *NaviRegion { + if err := m.db.Save(naviRegion).Error; err != nil { + return nil + } + return naviRegion +} + +func (m *Mirage) ListNaviNodes(regionID int) []NaviNode { + naviNodes := []NaviNode{} + if err := m.db.Preload("NaviRegion").Where("navi_region_id = ?", regionID).Find(&naviNodes).Error; err != nil { + return nil + } + return naviNodes +} + +func (m *Mirage) GetNaviNode(id string) *NaviNode { + naviNode := NaviNode{} + if err := m.db.Preload("NaviRegion").First(&naviNode, "id = ?", id).Error; err != nil { + return nil + } + return &naviNode +} +func (m *Mirage) UpdateNaviNode(naviNode *NaviNode) *NaviNode { + if err := m.db.Save(naviNode).Error; err != nil { + return nil + } + return naviNode +} diff --git a/controller/noise.go b/controller/noise.go index cb750a0..d65ec70 100644 --- a/controller/noise.go +++ b/controller/noise.go @@ -67,6 +67,9 @@ func (h *Mirage) NoiseUpgradeHandler( Methods(http.MethodPost) router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler) + router.HandleFunc("/navi/register", ts2021App.NoiseNaviRegisterHandler). + Methods(http.MethodPost) + server := http.Server{ ReadTimeout: HTTPReadTimeout, } diff --git a/controller/protocol_noise.go b/controller/protocol_noise.go index 9365f70..6c7293b 100644 --- a/controller/protocol_noise.go +++ b/controller/protocol_noise.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "net/http" + "time" "github.com/rs/zerolog/log" "tailscale.com/tailcfg" @@ -34,3 +35,91 @@ func (t *ts2021App) NoiseRegistrationHandler( t.mirage.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer()) } + +type NaviRegisterRequest struct { + ID string + Timestamp *time.Time +} + +type NaviRegisterResponse struct { + NodeInfo NaviNode + Timestamp *time.Time +} + +// 司南注册noise协议接口 +func (t *ts2021App) NoiseNaviRegisterHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace().Caller().Msgf("Noise registration handler for Navi %s", req.RemoteAddr) + if req.Method != http.MethodPost { + http.Error(writer, "Wrong method", http.StatusMethodNotAllowed) + + return + } + body, _ := io.ReadAll(req.Body) + registerRequest := NaviRegisterRequest{} + if err := json.Unmarshal(body, ®isterRequest); err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Cannot parse RegisterRequest") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + node := t.mirage.GetNaviNode(registerRequest.ID) + if node == nil { + log.Warn().Caller().Msgf("Navi node %s not found", registerRequest.ID) + http.Error(writer, "Navi node not found", http.StatusNotFound) + return + } + if node.NaviKey == "" || node.NaviKey == MachinePublicKeyStripPrefix(t.conn.Peer()) { + node.NaviKey = MachinePublicKeyStripPrefix(t.conn.Peer()) + node := t.mirage.UpdateNaviNode(node) + if node == nil { + log.Warn().Caller().Msgf("Navi node %s update failed", registerRequest.ID) + http.Error(writer, "Internal error", http.StatusInternalServerError) + return + } + log.Trace().Caller().Msgf("Navi node %s registered", node.ID) + now := time.Now().Round(time.Second) + resp := NaviRegisterResponse{ + NodeInfo: *node, + Timestamp: &now, + } + respBody, err := t.mirage.marshalResponse(resp, t.conn.Peer()) + if err != nil { + log.Error(). + Caller(). + Str("func", "handleNaviRegister"). + 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", "handleNaviRegister"). + Str("derpID", registerRequest.ID). + Msg("Successfully register Navi node") + + return + } + + log.Error(). + Caller(). + Msg("Navi node not created yet or key mismatch") + http.Error(writer, "Internal error", http.StatusInternalServerError) + return +} diff --git a/go.mod b/go.mod index 0ad54a7..4014333 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/lib/pq v1.10.7 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect @@ -93,6 +94,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.5 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect @@ -143,6 +145,7 @@ require ( github.com/mattn/go-isatty v0.0.17 // indirect github.com/mdlayher/netlink v1.7.1 // indirect github.com/mdlayher/socket v0.4.0 // indirect + github.com/melbahja/goph v1.3.1 github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sethvargo/go-diceware v0.3.0 diff --git a/go.sum b/go.sum index c1ad214..a7d4655 100644 --- a/go.sum +++ b/go.sum @@ -387,6 +387,7 @@ github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0P github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -426,6 +427,8 @@ github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTm github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= +github.com/melbahja/goph v1.3.1 h1:FxFevAwCCpLkM4WBmnVVxcJBcBz6lKQpsN5biV2hA6w= +github.com/melbahja/goph v1.3.1/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -460,6 +463,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= +github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= @@ -641,9 +646,11 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -732,6 +739,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -826,12 +834,14 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -843,6 +853,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=