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);
+});
+
+
+
+
+
+
+
+
+
+ {{ nr.Region.RegionID }} 号区-{{ nr.Region.RegionCode }}-{{
+ nr.Region.RegionName
+ }}
+ 共 {{ nr.Nodes ? nr.Nodes.length : 0 }} 只司南
+
+
+
+
+ 名称 |
+ 指定IP |
+ 端口 |
+ 架构 |
+
+ 租户操作菜单
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ {{ nn.IPv4 }}
+ {{ nn.IPv6 }}
+
+
+ |
+
+
+
+ {{ "中继端口:" + nn.DERPPort }}
+ {{ "导航端口:" + nn.STUNPort }}
+
+
+ |
+
+
+
+
+ {{ nn.Arch }}
+ {{ nn.Arch }}
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ 共 {{ selectTenant.userCount }} 用户
+
+
+ 共 {{ selectTenant.adminCount }} 管理员
+
+
+
+ 共 {{ selectTenant.deviceCount }} 设备
+
+
+ 共 {{ selectTenant.subnetCount }} 子网路由
+
+
+
+ 编辑租户…
+
+
+
+ 移除租户…
+
+
+
+
+
+
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=