Pārlūkot izejas kodu

update v0.9.3 for anylink

Stille 2 gadi atpakaļ
vecāks
revīzija
3926fd8640
99 mainītis faili ar 4052 papildinājumiem un 2209 dzēšanām
  1. 4 4
      anylink/Dockerfile
  2. 0 11
      anylink/README.md
  3. 3 2
      anylink/build.sh
  4. 2 1
      anylink/build_docker.sh
  5. 0 31
      anylink/doc/README.md
  6. 0 42
      anylink/doc/question.md
  7. BIN
      anylink/doc/screenshot/group.jpg
  8. BIN
      anylink/doc/screenshot/ip_map.jpg
  9. BIN
      anylink/doc/screenshot/jetbrains.png
  10. BIN
      anylink/doc/screenshot/online.jpg
  11. BIN
      anylink/doc/screenshot/qq.png
  12. BIN
      anylink/doc/screenshot/setting.jpg
  13. BIN
      anylink/doc/screenshot/system.jpg
  14. BIN
      anylink/doc/screenshot/users.jpg
  15. BIN
      anylink/doc/screenshot/wxpay.png
  16. BIN
      anylink/doc/screenshot/wxpay2.png
  17. 0 6
      anylink/docker/Dockerfile
  18. 0 41
      anylink/docker/docker_entrypoint.sh
  19. 0 37
      anylink/docker/docker_entrypoint_fix.sh
  20. 0 41
      anylink/docker/generate-certs.sh
  21. 2 2
      anylink/docker_entrypoint.sh
  22. 99 0
      anylink/server/admin/api_cert.go
  23. 29 2
      anylink/server/admin/api_group.go
  24. 15 4
      anylink/server/admin/api_ip_map.go
  25. 5 3
      anylink/server/admin/api_other.go
  26. 2 2
      anylink/server/admin/api_policy.go
  27. 26 0
      anylink/server/admin/api_set_audit.go
  28. 114 0
      anylink/server/admin/api_uploaduser.go
  29. 45 29
      anylink/server/admin/api_user.go
  30. 3 3
      anylink/server/admin/resp_test.go
  31. 18 1
      anylink/server/admin/server.go
  32. 1 1
      anylink/server/base/app_ver.go
  33. 6 1
      anylink/server/base/cfg.go
  34. 12 2
      anylink/server/base/cmd.go
  35. 18 13
      anylink/server/base/config.go
  36. 30 16
      anylink/server/base/log.go
  37. 20 10
      anylink/server/conf/server-sample.toml
  38. 13 0
      anylink/server/conf/server.toml
  39. 24 24
      anylink/server/conf/vpn_cert.crt
  40. 25 25
      anylink/server/conf/vpn_cert.key
  41. 0 5
      anylink/server/cron/clear_statsinfo.go
  42. 20 0
      anylink/server/cron/clear_user_act_log.go
  43. 5 0
      anylink/server/cron/start.go
  44. 6 0
      anylink/server/dbdata/audit.go
  45. 1 1
      anylink/server/dbdata/audit_test.go
  46. 412 0
      anylink/server/dbdata/cert.go
  47. 41 1
      anylink/server/dbdata/db.go
  48. 29 0
      anylink/server/dbdata/group.go
  49. 8 7
      anylink/server/dbdata/group_test.go
  50. 20 11
      anylink/server/dbdata/ip_map.go
  51. 3 3
      anylink/server/dbdata/setting.go
  52. 0 6
      anylink/server/dbdata/statsinfo.go
  53. 23 18
      anylink/server/dbdata/tables.go
  54. 21 1
      anylink/server/dbdata/user.go
  55. 210 0
      anylink/server/dbdata/user_act_log.go
  56. 82 0
      anylink/server/dbdata/user_act_log_test.go
  57. 8 7
      anylink/server/dbdata/user_test.go
  58. 48 10
      anylink/server/dbdata/userauth_ldap.go
  59. 53 24
      anylink/server/go.mod
  60. 228 59
      anylink/server/go.sum
  61. 7 3
      anylink/server/handler/dtls.go
  62. 38 8
      anylink/server/handler/link_auth.go
  63. 1 0
      anylink/server/handler/link_base.go
  64. 50 18
      anylink/server/handler/link_cstp.go
  65. 46 15
      anylink/server/handler/link_dtls.go
  66. 5 2
      anylink/server/handler/link_home.go
  67. 1 1
      anylink/server/handler/link_tap.go
  68. 24 1
      anylink/server/handler/link_tun.go
  69. 28 6
      anylink/server/handler/link_tunnel.go
  70. 9 5
      anylink/server/handler/payload.go
  71. 90 96
      anylink/server/handler/payload_access_audit.go
  72. 28 15
      anylink/server/handler/payload_tcp_parser.go
  73. 8 4
      anylink/server/handler/payload_test.go
  74. 22 7
      anylink/server/handler/server.go
  75. 1 0
      anylink/server/main.go
  76. 0 290
      anylink/server/pkg/proxyproto/protocol.go
  77. 0 486
      anylink/server/pkg/proxyproto/protocol_test.go
  78. 35 0
      anylink/server/sessdata/compress.go
  79. 28 0
      anylink/server/sessdata/compress_test.go
  80. 171 58
      anylink/server/sessdata/ip_pool.go
  81. 32 17
      anylink/server/sessdata/ip_pool_test.go
  82. 2 0
      anylink/server/sessdata/online.go
  83. 136 33
      anylink/server/sessdata/session.go
  84. 25 0
      anylink/server/sessdata/session_test.go
  85. 1 0
      anylink/server/sessdata/start.go
  86. 7 0
      anylink/systemd/anylink.service
  87. 1 0
      anylink/web/package.json
  88. BIN
      anylink/web/public/批量添加用户模版.xlsx
  89. 321 0
      anylink/web/src/components/audit/Access.vue
  90. 263 0
      anylink/web/src/components/audit/ActLog.vue
  91. 6 1
      anylink/web/src/layout/LayoutAside.vue
  92. 3 0
      anylink/web/src/pages/Home.vue
  93. 116 13
      anylink/web/src/pages/group/List.vue
  94. 39 278
      anylink/web/src/pages/set/Audit.vue
  95. 456 112
      anylink/web/src/pages/set/Other.vue
  96. 246 230
      anylink/web/src/pages/user/IpMap.vue
  97. 56 3
      anylink/web/src/pages/user/List.vue
  98. 9 0
      anylink/web/src/pages/user/Online.vue
  99. 7 0
      anylink/web/yarn.lock

+ 4 - 4
anylink/Dockerfile

@@ -1,6 +1,6 @@
 # web
 FROM node:16.17.1-alpine3.15 as builder_node
-ENV VERSION 0.9.1
+ENV VERSION 0.9.3
 WORKDIR /web
 COPY ./web /web
 RUN yarn install \
@@ -8,7 +8,7 @@ RUN yarn install \
     && ls /web/ui
 
 # server
-FROM golang:1.18-alpine as builder_golang
+FROM golang:1.19-alpine as builder_golang
 #TODO 本地打包时使用镜像
 ENV GOPROXY=https://goproxy.io
 ENV GOOS=linux
@@ -26,13 +26,13 @@ RUN cd /anylink/server;go mod tidy;go build -o anylink -ldflags "-X main.CommitI
 FROM alpine
 LABEL maintainer="github.com/bjdgyc"
 
-ENV IPV4_CIDR="192.168.10.0/24"
+#ENV IPV4_CIDR="192.168.10.0/24"
 
 WORKDIR /app
 COPY --from=builder_golang /anylink/server/anylink  /app/
 COPY docker_entrypoint.sh  /app/
 
-COPY ./server/bridge-init.sh /app/
+#COPY ./server/bridge-init.sh /app/
 COPY ./server/conf  /app/conf
 COPY ./LICENSE  /app/LICENSE
 

+ 0 - 11
anylink/README.md

@@ -7,17 +7,6 @@ Docker [stilleshan/anylink](https://hub.docker.com/r/stilleshan/anylink)
 ## 简介
 基于 [bjdgyc/anylink](https://github.com/bjdgyc/anylink) 项目的 docker 镜像.
 
-## 更新
-- **2022-11-10** 更新`0.9.1-beta1`版 docker 镜像.
-- **2022-07-04** 更新`0.8.1`版 docker 镜像.
-- **2022-04-07** 更新`0.7.4`版 docker 镜像.
-- **2022-02-16** 更新`0.7.3`版 docker 镜像.
-- **2021-12-31** 更新`0.7.2`版 docker 镜像.
-- **2021-12-29** 更新`0.7.1`版 docker 镜像.
-- **2021-08-26** 更新`0.6.2`版 docker 镜像.
-- **2021-08-02** 更新`0.5.1`版 docker 镜像.
-- **2021-07-05** 更新`0.4.2`版 docker 镜像.
-- **2021-06-09** 更新`0.3.3`版 docker 镜像,新增同时支持 X86 和 ARM 架构.
 
 ## 部署
 ### docker

+ 3 - 2
anylink/build.sh

@@ -20,9 +20,10 @@ cd $cpath/web
 #npm install
 #npm run build
 
-yarn install
+yarn install --registry=https://registry.npmmirror.com
 yarn run build
 
+
 RETVAL $?
 
 echo "编译二进制文件"
@@ -43,7 +44,7 @@ rm -rf $deploy ${deploy}.tar.gz
 mkdir $deploy
 
 cp -r server/anylink $deploy
-cp -r server/bridge-init.sh $deploy
+#cp -r server/bridge-init.sh $deploy
 cp -r server/conf $deploy
 
 cp -r systemd $deploy

+ 2 - 1
anylink/build_docker.sh

@@ -5,7 +5,8 @@ echo $ver
 
 #docker login -u bjdgyc
 
-docker build -t bjdgyc/anylink .
+#docker build -t bjdgyc/anylink .
+docker build -t bjdgyc/anylink -f docker/Dockerfile .
 
 docker tag bjdgyc/anylink:latest bjdgyc/anylink:$ver
 

+ 0 - 31
anylink/doc/README.md

@@ -1,31 +0,0 @@
-## Donate
-
-> 如果您觉得 AnyLink 对你有帮助,欢迎给我们打赏,也是帮助 AnyLink 更好的发展。
-
-<p>
-    <img src="screenshot/wxpay2.png" width="500" />
-</p>
-
-## Donator
-
-> 感谢以下同学的打赏,AnyLink 有你更美好!
-
-| 昵称           | 主页                         |
-| -------------- | ---------------------------- |
-| 代码 oo8       |                              |
-| 甘磊           | https://github.com/ganlei333 |
-| Oo@            | https://github.com/chooop    |
-| 虚极静笃       |                              |
-| 请喝可乐       |                              |
-| 加油加油       |                              |
-| 李建           |                              |
-| lanbin         |                              |
-| 乐在东途       |                              |
-| 孤鸿           |                              |
-| 刘国华         |                              |
-| 改名好无聊     |                              |
-| 全能互联网专家 |                              |
-
-
-
-

+ 0 - 42
anylink/doc/question.md

@@ -1,42 +0,0 @@
-# 常见问题
-
-### anyconnect 客户端问题
-> 客户端请使用群共享文件的版本,其他版本没有测试过,不保证使用正常
-> 
-> 添加QQ群: 567510628
-
-### OTP 动态码
-> 请使用手机安装 freeotp ,然后扫描otp二维码,生成的数字即是动态码
-
-### 远程桌面连接
-> 本软件已经支持远程桌面里面连接anyconnect。
-
-### 私有证书问题
-> anylink 默认不支持私有证书
-> 
-> 其他使用私有证书的问题,请自行解决
-
-### dpd timeout 设置问题
-```
-#客户端失效检测时间(秒) dpd > keepalive
-cstp_keepalive = 20
-cstp_dpd = 30
-mobile_keepalive = 40
-mobile_dpd = 50
-```
-> 以上dpd参数为客户端的超时检测时间, 如一段时间内,没有数据传输,防火墙会主动关闭连接
-> 
-> 如经常出现 timeout 的错误信息,应根据当前防火墙的设置,适当减小dpd数值
-
-### 性能问题
-```
-内网环境测试数据
-虚拟服务器:  centos7 4C8G
-anylink:    tun模式 tcp传输
-客户端文件下载速度:240Mb/s
-客户端网卡下载速度:270Mb/s
-服务端网卡上传速度:280Mb/s
-```
-> 客户端tls加密协议、隧道header头都会占用一定带宽
-
-

BIN
anylink/doc/screenshot/group.jpg


BIN
anylink/doc/screenshot/ip_map.jpg


BIN
anylink/doc/screenshot/jetbrains.png


BIN
anylink/doc/screenshot/online.jpg


BIN
anylink/doc/screenshot/qq.png


BIN
anylink/doc/screenshot/setting.jpg


BIN
anylink/doc/screenshot/system.jpg


BIN
anylink/doc/screenshot/users.jpg


BIN
anylink/doc/screenshot/wxpay.png


BIN
anylink/doc/screenshot/wxpay2.png


+ 0 - 6
anylink/docker/Dockerfile

@@ -1,6 +0,0 @@
-FROM ubuntu:18.04
-WORKDIR /
-COPY docker_entrypoint.sh docker_entrypoint.sh
-RUN mkdir /anylink && apt update && apt install -y wget iptables tar iproute2
-ENTRYPOINT ["/docker_entrypoint.sh"]
-#CMD ["/anylink/anylink","-conf=/anylink/conf/server.toml"]

+ 0 - 41
anylink/docker/docker_entrypoint.sh

@@ -1,41 +0,0 @@
-#!/bin/sh
-USER="admin"
-MM=$(pwgen -1s)
-CREATE_USER=1
-CONFIG_FILE='/app/conf/server.toml'
-
-if [ $CREATE_USER -eq 1 ]; then
-  if [ ! -e $CREATE_USER ]; then
-	    MM=$(pwgen -1s)
-            touch $CREATE_USER
-	    bash /app/generate-certs.sh
-            cd /app/conf/ && cp *.crt  /usr/local/share/ca-certificates/
-	    update-ca-certificates --fresh
-            userpass=$(/app/anylink -passwd "${MM}"| cut -d : -f2)
-	    echo "${userpass}"
-            jwttoken=$(/app/anylink -secret | cut -d : -f2)
-            echo "-- First container startup --user:${USER} pwd:${MM}"
-            sed -i "s/admin/${USER}/g" /app/server-example.toml
-            sed -i "s/123456/${MM}/g" /app/server-example.toml
-            sed -i "s#usertoken#${userpass}#g" /app/server-example.toml
-            sed -i "s/jwttoken/${jwttoken}/g" /app/server-example.toml
-            else
-                        echo "-- Not first container startup --"
-  fi
-
-else
-                echo "user switch not create"
-
-fi
-
-if [ ! -f $CONFIG_FILE ]; then
-echo "#####Generating configuration file#####"
-cp /app/server-example.toml /app/conf/server.toml
-else
-        echo "#####Configuration file already exists#####"
-fi
-
-rtaddr=$(grep "cidr" /app/conf/server.toml |awk -F \" '{print $2}')
-sysctl -w net.ipv4.ip_forward=1
-iptables -t nat -A POSTROUTING -s "${rtaddr}" -o eth0+ -j MASQUERADE
-/app/anylink -conf="/app/conf/server.toml"

+ 0 - 37
anylink/docker/docker_entrypoint_fix.sh

@@ -1,37 +0,0 @@
-#! /bin/bash
-version=(`wget -qO- -t1 -T2 "https://api.github.com/repos/bjdgyc/anylink/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g'`)
-count=(`ls anylink | wc -w `)
-wget https://github.com/bjdgyc/anylink/releases/download/${version}/anylink-deploy.tar.gz
-tar xf anylink-deploy.tar.gz
-rm -rf anylink-deploy.tar.gz
-if [ ${count} -eq 0 ]; then
-	echo "init anylink"
-	mv anylink-deploy/* anylink/
-else
-	if [ ! -d "/anylink/log" ]; then
-		mv anylink-deploy/log anylink/
-	fi
-	if [ ! -d "/anylink/conf" ]; then
-                mv anylink-deploy/conf anylink/
-        fi
-	echo "update anylink"
-	rm -rf anylink/ui anylink/anylink anylink/files
-	mv anylink-deploy/ui anylink/
-	mv anylink-deploy/anylink anylink/
-	mv anylink-deploy/files anylink/
-fi
-rm -rf anylink-deploy
-sysctl -w net.ipv4.ip_forward=1
-if [[ ${mode} == pro ]];then
-	iptables -t nat -A POSTROUTING -s ${iproute} -o eth0 -j MASQUERADE
-	iptables -L -n -t nat
-	/anylink/anylink -conf=/anylink/conf/server.toml
-elif [[ ${mode} == password ]];then
-	if [ -z ${password} ];then
-		echo "invalid password"
-	else
-		/anylink/anylink -passwd ${password}
-	fi
-elif [[ ${mode} -eq jwt ]];then
-	/anylink/anylink -secret
-fi

+ 0 - 41
anylink/docker/generate-certs.sh

@@ -1,41 +0,0 @@
-#!/bin/sh
-
-mkdir -p /ssl
-
-OUTPUT_FILENAME="vpn.xx.com"
-
-printf "[req]
-prompt                  = no
-default_bits            = 4096
-default_md              = sha256
-encrypt_key             = no
-string_mask             = utf8only
-
-distinguished_name      = cert_distinguished_name
-req_extensions          = req_x509v3_extensions
-x509_extensions         = req_x509v3_extensions
-
-[ cert_distinguished_name ]
-C  = CN
-ST = BJ
-L  = BJ
-O  = xx.com
-OU = xx.com
-CN = xx.com
-
-[req_x509v3_extensions]
-basicConstraints        = critical,CA:true
-subjectKeyIdentifier    = hash
-keyUsage                = critical,digitalSignature,keyCertSign,cRLSign #,keyEncipherment
-extendedKeyUsage        = critical,serverAuth #, clientAuth
-subjectAltName          = @alt_names
-
-[alt_names]
-DNS.1 = xx.com
-DNS.2 = *.xx.com
-
-">/ssl/${OUTPUT_FILENAME}.conf
-
-openssl req -x509 -newkey rsa:2048 -keyout /ssl/test_vpn_key.pem -out /ssl/test_vpn_cert.pem \
--days 3600 -nodes -config /ssl/${OUTPUT_FILENAME}.conf
-

+ 2 - 2
anylink/docker_entrypoint.sh

@@ -15,8 +15,8 @@ case $var1 in
 
 *)
   sysctl -w net.ipv4.ip_forward=1
-  iptables -t nat -A POSTROUTING -s "${IPV4_CIDR}" -o eth0+ -j MASQUERADE
-  iptables -nL -t nat
+  #iptables -t nat -A POSTROUTING -s "${IPV4_CIDR}" -o eth0+ -j MASQUERADE
+  #iptables -nL -t nat
 
   exec /app/anylink "$@"
   ;;

+ 99 - 0
anylink/server/admin/api_cert.go

@@ -0,0 +1,99 @@
+package admin
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+
+	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
+)
+
+func CustomCert(w http.ResponseWriter, r *http.Request) {
+	cert, _, err := r.FormFile("cert")
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	key, _, err := r.FormFile("key")
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	certFile, err := os.OpenFile(base.Cfg.CertFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	defer certFile.Close()
+	if _, err := io.Copy(certFile, cert); err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	keyFile, err := os.OpenFile(base.Cfg.CertKey, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	defer keyFile.Close()
+	if _, err := io.Copy(keyFile, key); err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	if tlscert, _, err := dbdata.ParseCert(); err != nil {
+		RespError(w, RespInternalErr, fmt.Sprintf("证书不合法,请重新上传:%v", err))
+		return
+	} else {
+		dbdata.LoadCertificate(tlscert)
+	}
+	RespSucess(w, "上传成功")
+}
+func GetCertSetting(w http.ResponseWriter, r *http.Request) {
+	sess := dbdata.GetXdb().NewSession()
+	defer sess.Close()
+	data := &dbdata.SettingLetsEncrypt{}
+	if err := dbdata.SettingGet(data); err != nil {
+		dbdata.SettingSessAdd(sess, data)
+		RespError(w, RespInternalErr, err)
+	}
+	userData := &dbdata.LegoUserData{}
+	if err := dbdata.SettingGet(userData); err != nil {
+		dbdata.SettingSessAdd(sess, userData)
+	}
+	RespSucess(w, data)
+}
+func CreatCert(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	body, err := io.ReadAll(r.Body)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	defer r.Body.Close()
+	config := &dbdata.SettingLetsEncrypt{}
+	if err := json.Unmarshal(body, config); err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	if err := dbdata.SettingSet(config); err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	client := dbdata.LeGoClient{}
+	if err := client.NewClient(config); err != nil {
+		base.Error(err)
+		RespError(w, RespInternalErr, fmt.Sprintf("获取证书失败:%v", err))
+		return
+	}
+	if err := client.GetCert(config.Domain); err != nil {
+		base.Error(err)
+		RespError(w, RespInternalErr, fmt.Sprintf("获取证书失败:%v", err))
+		return
+	}
+	RespSucess(w, "生成证书成功")
+}

+ 29 - 2
anylink/server/admin/api_group.go

@@ -2,7 +2,7 @@ package admin
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strconv"
 
@@ -79,7 +79,7 @@ func GroupDetail(w http.ResponseWriter, r *http.Request) {
 }
 
 func GroupSet(w http.ResponseWriter, r *http.Request) {
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return
@@ -118,3 +118,30 @@ func GroupDel(w http.ResponseWriter, r *http.Request) {
 	}
 	RespSucess(w, nil)
 }
+
+func GroupAuthLogin(w http.ResponseWriter, r *http.Request) {
+	type AuthLoginData struct {
+		Name string                 `json:"name"`
+		Pwd  string                 `json:"pwd"`
+		Auth map[string]interface{} `json:"auth"`
+	}
+
+	body, err := io.ReadAll(r.Body)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	defer r.Body.Close()
+	v := &AuthLoginData{}
+	err = json.Unmarshal(body, &v)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	err = dbdata.GroupAuthLogin(v.Name, v.Pwd, v.Auth)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	RespSucess(w, "ok")
+}

+ 15 - 4
anylink/server/admin/api_ip_map.go

@@ -2,7 +2,7 @@ package admin
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strconv"
 
@@ -59,7 +59,7 @@ func UserIpMapDetail(w http.ResponseWriter, r *http.Request) {
 func UserIpMapSet(w http.ResponseWriter, r *http.Request) {
 	_ = r.ParseForm()
 
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return
@@ -80,6 +80,8 @@ func UserIpMapSet(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// sessdata.IpAllSet(v)
+
 	RespSucess(w, nil)
 }
 
@@ -93,11 +95,20 @@ func UserIpMapDel(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	data := dbdata.IpMap{Id: id}
-	err := dbdata.Del(&data)
+	var data dbdata.IpMap
+	err := dbdata.One("Id", id, &data)
+	if err != nil {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+
+	err = dbdata.Del(&data)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return
 	}
+
+	// sessdata.IpAllDel(&data)
+
 	RespSucess(w, nil)
 }

+ 5 - 3
anylink/server/admin/api_other.go

@@ -3,10 +3,11 @@ package admin
 import (
 	"encoding/json"
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"regexp"
 
+	"github.com/bjdgyc/anylink/base"
 	"github.com/bjdgyc/anylink/dbdata"
 )
 
@@ -25,7 +26,7 @@ func setOtherGet(data interface{}, w http.ResponseWriter) {
 }
 
 func setOtherEdit(data interface{}, w http.ResponseWriter, r *http.Request) {
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return
@@ -82,11 +83,12 @@ func SetOtherAuditLog(w http.ResponseWriter, r *http.Request) {
 		RespError(w, RespInternalErr, err)
 		return
 	}
+	data.AuditInterval = base.Cfg.AuditInterval
 	RespSucess(w, data)
 }
 
 func SetOtherAuditLogEdit(w http.ResponseWriter, r *http.Request) {
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return

+ 2 - 2
anylink/server/admin/api_policy.go

@@ -2,7 +2,7 @@ package admin
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strconv"
 
@@ -57,7 +57,7 @@ func PolicyDetail(w http.ResponseWriter, r *http.Request) {
 }
 
 func PolicySet(w http.ResponseWriter, r *http.Request) {
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return

+ 26 - 0
anylink/server/admin/api_set_audit.go

@@ -51,3 +51,29 @@ func SetAuditExport(w http.ResponseWriter, r *http.Request) {
 	gocsv.Marshal(datas, w)
 
 }
+
+func UserActLogList(w http.ResponseWriter, r *http.Request) {
+	_ = r.ParseForm()
+	pageS := r.FormValue("page")
+	page, _ := strconv.Atoi(pageS)
+	if page < 1 {
+		page = 1
+	}
+	var datas []dbdata.UserActLog
+	session := dbdata.UserActLogIns.GetSession(r.Form)
+	count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page)
+	if err != nil && !dbdata.CheckErrNotFound(err) {
+		RespError(w, RespInternalErr, err)
+		return
+	}
+	data := map[string]interface{}{
+		"count":     count,
+		"page_size": dbdata.PageSize,
+		"datas":     datas,
+		"statusOps": dbdata.UserActLogIns.GetStatusOpsWithTag(),
+		"osOps":     dbdata.UserActLogIns.OsOps,
+		"clientOps": dbdata.UserActLogIns.ClientOps,
+	}
+
+	RespSucess(w, data)
+}

+ 114 - 0
anylink/server/admin/api_uploaduser.go

@@ -0,0 +1,114 @@
+package admin
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
+	"github.com/bjdgyc/anylink/pkg/utils"
+	mapset "github.com/deckarep/golang-set"
+	"github.com/spf13/cast"
+	"github.com/xuri/excelize/v2"
+)
+
+func UserUpload(w http.ResponseWriter, r *http.Request) {
+	r.ParseMultipartForm(8 << 20)
+	file, header, err := r.FormFile("file")
+	if err != nil || !strings.Contains(header.Filename, ".xlsx") || !strings.Contains(header.Filename, ".xls") {
+		RespError(w, RespInternalErr, "文件解析失败:仅支持xlsx或xls文件")
+		return
+	}
+	defer file.Close()
+	newFile, err := os.Create(base.Cfg.FilesPath + header.Filename)
+	if err != nil {
+		RespError(w, RespInternalErr, "创建文件失败:", err)
+		return
+	}
+	defer newFile.Close()
+	io.Copy(newFile, file)
+	if err = UploadUser(newFile.Name()); err != nil {
+		RespError(w, RespInternalErr, err)
+		os.Remove(base.Cfg.FilesPath + header.Filename)
+		return
+	}
+	os.Remove(base.Cfg.FilesPath + header.Filename)
+	RespSucess(w, "批量添加成功")
+}
+func UploadUser(file string) error {
+	f, err := excelize.OpenFile(file)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if err := f.Close(); err != nil {
+			return
+		}
+	}()
+	rows, err := f.GetRows("Sheet1")
+	if err != nil {
+		return err
+	}
+	if rows[0][0] != "id" || rows[0][1] != "username" || rows[0][2] != "nickname" || rows[0][3] != "email" || rows[0][4] != "pin_code" || rows[0][5] != "limittime" || rows[0][6] != "otp_secret" || rows[0][7] != "disable_otp" || rows[0][8] != "groups" || rows[0][9] != "status" || rows[0][10] != "send_email" {
+		return fmt.Errorf("批量添加失败,表格格式不正确")
+	}
+	var k []interface{}
+	for _, v := range dbdata.GetGroupNames() {
+		k = append(k, v)
+	}
+	for index, row := range rows {
+		if index == 0 {
+			continue
+		}
+		id, _ := strconv.Atoi(row[0])
+		if len(row[4]) < 6 {
+			row[4] = utils.RandomRunes(8)
+		}
+		limittime, _ := time.ParseInLocation("2006-01-02 15:04:05", row[5], time.Local)
+		disableOtp, _ := strconv.ParseBool(row[7])
+		var group []string
+		if row[8] == "" {
+			return fmt.Errorf("第%d行数据错误,用户组不允许为空", index)
+		}
+		for _, v := range strings.Split(row[8], ",") {
+			if s := mapset.NewSetFromSlice(k); s.Contains(v) {
+				group = append(group, v)
+			} else {
+				return fmt.Errorf("用户组【%s】不存在,请检查第%d行数据", v, index)
+			}
+		}
+		status := cast.ToInt8(row[9])
+		sendmail, _ := strconv.ParseBool(row[10])
+		// createdAt, _ := time.ParseInLocation("2006-01-02 15:04:05", row[11], time.Local)
+		// updatedAt, _ := time.ParseInLocation("2006-01-02 15:04:05", row[12], time.Local)
+		user := &dbdata.User{
+			Id:         id,
+			Username:   row[1],
+			Nickname:   row[2],
+			Email:      row[3],
+			PinCode:    row[4],
+			LimitTime:  &limittime,
+			OtpSecret:  row[6],
+			DisableOtp: disableOtp,
+			Groups:     group,
+			Status:     status,
+			SendEmail:  sendmail,
+			// CreatedAt:  createdAt,
+			// UpdatedAt:  updatedAt,
+		}
+		if err := dbdata.AddBatch(user); err != nil {
+			return fmt.Errorf("请检查第%d行数据是否导入有重复用户", index)
+		}
+		if user.SendEmail {
+			if err := userAccountMail(user); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}

+ 45 - 29
anylink/server/admin/api_user.go

@@ -5,7 +5,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -80,7 +80,7 @@ func UserDetail(w http.ResponseWriter, r *http.Request) {
 func UserSet(w http.ResponseWriter, r *http.Request) {
 	_ = r.ParseForm()
 
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		RespError(w, RespInternalErr, err)
 		return
@@ -107,7 +107,8 @@ func UserSet(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-
+	//修改用户资料后执行过期用户检测
+	sessdata.CloseUserLimittimeSession()
 	RespSucess(w, nil)
 }
 
@@ -132,33 +133,44 @@ func UserDel(w http.ResponseWriter, r *http.Request) {
 
 func UserOtpQr(w http.ResponseWriter, r *http.Request) {
 	_ = r.ParseForm()
-	b64 := r.FormValue("b64")
+	b64S := r.FormValue("b64")
 	idS := r.FormValue("id")
 	id, _ := strconv.Atoi(idS)
+
+	var b64 bool
+	if b64S == "1" {
+		b64 = true
+	}
+	data, err := userOtpQr(id, b64)
+	if err != nil {
+		base.Error(err)
+	}
+	io.WriteString(w, data)
+}
+
+func userOtpQr(uid int, b64 bool) (string, error) {
 	var user dbdata.User
-	err := dbdata.One("Id", id, &user)
+	err := dbdata.One("Id", uid, &user)
 	if err != nil {
-		RespError(w, RespInternalErr, err)
-		return
+		return "", err
 	}
 
 	issuer := url.QueryEscape(base.Cfg.Issuer)
 	qrstr := fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s", issuer, user.Email, issuer, user.OtpSecret)
 	qr, _ := qrcode.New(qrstr, qrcode.High)
 
-	if b64 == "1" {
-		data, _ := qr.PNG(300)
-		s := base64.StdEncoding.EncodeToString(data)
-		_, err = fmt.Fprint(w, s)
+	if b64 {
+		data, err := qr.PNG(300)
 		if err != nil {
-			base.Error(err)
+			return "", err
 		}
-		return
-	}
-	err = qr.Write(300, w)
-	if err != nil {
-		base.Error(err)
+		s := base64.StdEncoding.EncodeToString(data)
+		return s, nil
 	}
+
+	buf := bytes.NewBuffer(nil)
+	err = qr.Write(300, buf)
+	return buf.String(), err
 }
 
 // 在线用户
@@ -177,7 +189,7 @@ func UserOnline(w http.ResponseWriter, r *http.Request) {
 func UserOffline(w http.ResponseWriter, r *http.Request) {
 	_ = r.ParseForm()
 	token := r.FormValue("token")
-	sessdata.CloseSess(token)
+	sessdata.CloseSess(token, dbdata.UserLogoutAdmin)
 	RespSucess(w, nil)
 }
 
@@ -189,12 +201,13 @@ func UserReline(w http.ResponseWriter, r *http.Request) {
 }
 
 type userAccountMailData struct {
-	Issuer   string
-	LinkAddr string
-	Group    string
-	Username string
-	PinCode  string
-	OtpImg   string
+	Issuer       string
+	LinkAddr     string
+	Group        string
+	Username     string
+	PinCode      string
+	OtpImg       string
+	OtpImgBase64 string
 }
 
 func userAccountMail(user *dbdata.User) error {
@@ -235,12 +248,15 @@ func userAccountMail(user *dbdata.User) error {
 		return err
 	}
 
+	otpData, _ := userOtpQr(user.Id, true)
+
 	data := userAccountMailData{
-		LinkAddr: setting.LinkAddr,
-		Group:    strings.Join(user.Groups, ","),
-		Username: user.Username,
-		PinCode:  user.PinCode,
-		OtpImg:   fmt.Sprintf("https://%s/otp_qr?id=%d&jwt=%s", setting.LinkAddr, user.Id, tokenString),
+		LinkAddr:     setting.LinkAddr,
+		Group:        strings.Join(user.Groups, ","),
+		Username:     user.Username,
+		PinCode:      user.PinCode,
+		OtpImg:       fmt.Sprintf("https://%s/otp_qr?id=%d&jwt=%s", setting.LinkAddr, user.Id, tokenString),
+		OtpImgBase64: "data:image/png;base64," + otpData,
 	}
 	w := bytes.NewBufferString("")
 	t, _ := template.New("auth_complete").Parse(htmlBody)

+ 3 - 3
anylink/server/admin/resp_test.go

@@ -2,7 +2,7 @@ package admin
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"net/http/httptest"
 	"testing"
 
@@ -15,7 +15,7 @@ func TestRespSucess(t *testing.T) {
 	RespSucess(w, "data")
 	// fmt.Println(w)
 	assert.Equal(w.Code, 200)
-	body, _ := ioutil.ReadAll(w.Body)
+	body, _ := io.ReadAll(w.Body)
 	res := Resp{}
 	err := json.Unmarshal(body, &res)
 	assert.Nil(err)
@@ -30,7 +30,7 @@ func TestRespError(t *testing.T) {
 	RespError(w, 10, "err-msg")
 	// fmt.Println(w)
 	assert.Equal(w.Code, 200)
-	body, _ := ioutil.ReadAll(w.Body)
+	body, _ := io.ReadAll(w.Body)
 	res := Resp{}
 	err := json.Unmarshal(body, &res)
 	assert.Nil(err)

+ 18 - 1
anylink/server/admin/server.go

@@ -9,6 +9,7 @@ import (
 
 	"github.com/arl/statsviz"
 	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
 )
@@ -45,10 +46,15 @@ func StartAdmin() {
 	r.HandleFunc("/set/other/audit_log/edit", SetOtherAuditLogEdit)
 	r.HandleFunc("/set/audit/list", SetAuditList)
 	r.HandleFunc("/set/audit/export", SetAuditExport)
+	r.HandleFunc("/set/audit/act_log_list", UserActLogList)
+	r.HandleFunc("/set/other/createcert", CreatCert)
+	r.HandleFunc("/set/other/getcertset", GetCertSetting)
+	r.HandleFunc("/set/other/customcert", CustomCert)
 
 	r.HandleFunc("/user/list", UserList)
 	r.HandleFunc("/user/detail", UserDetail)
 	r.HandleFunc("/user/set", UserSet)
+	r.HandleFunc("/user/uploaduser", UserUpload).Methods(http.MethodPost)
 	r.HandleFunc("/user/del", UserDel)
 	r.HandleFunc("/user/online", UserOnline)
 	r.HandleFunc("/user/offline", UserOffline)
@@ -69,6 +75,7 @@ func StartAdmin() {
 	r.HandleFunc("/group/detail", GroupDetail)
 	r.HandleFunc("/group/set", GroupSet)
 	r.HandleFunc("/group/del", GroupDel)
+	r.HandleFunc("/group/auth_login", GroupAuthLogin)
 
 	r.HandleFunc("/statsinfo/list", StatsInfoList)
 
@@ -93,18 +100,28 @@ func StartAdmin() {
 	for _, s := range cipherSuites {
 		selectedCipherSuites = append(selectedCipherSuites, s.ID)
 	}
+
+	if tlscert, _, err := dbdata.ParseCert(); err != nil {
+		base.Fatal("证书加载失败", err)
+	} else {
+		dbdata.LoadCertificate(tlscert)
+	}
+
 	// 设置tls信息
 	tlsConfig := &tls.Config{
 		NextProtos:   []string{"http/1.1"},
 		MinVersion:   tls.VersionTLS12,
 		CipherSuites: selectedCipherSuites,
+		GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
+			return dbdata.GetCertificateBySNI(chi.ServerName)
+		},
 	}
 	srv := &http.Server{
 		Addr:      base.Cfg.AdminAddr,
 		Handler:   r,
 		TLSConfig: tlsConfig,
 	}
-	err := srv.ListenAndServeTLS(base.Cfg.CertFile, base.Cfg.CertKey)
+	err := srv.ListenAndServeTLS("", "")
 	if err != nil {
 		base.Fatal(err)
 	}

+ 1 - 1
anylink/server/base/app_ver.go

@@ -3,5 +3,5 @@ package base
 const (
 	APP_NAME = "AnyLink"
 	// app版本号
-	APP_VER = "0.9.1-beta1"
+	APP_VER = "0.9.3"
 )

+ 6 - 1
anylink/server/base/cfg.go

@@ -73,7 +73,12 @@ type ServerConfig struct {
 	// AuthTimeout    int `json:"auth_timeout"`    // in seconds
 	AuditInterval int `json:"audit_interval"` // in seconds
 
-	ShowSQL bool `json:"show_sql"` // bool
+	ShowSQL         bool `json:"show_sql"` // bool
+	IptablesNat     bool `json:"iptables_nat"`
+	Compression     bool `json:"compression"`       // bool
+	NoCompressLimit int  `json:"no_compress_limit"` // int
+
+	DisplayError bool `json:"display_error"`
 }
 
 func initServerCfg() {

+ 12 - 2
anylink/server/base/cmd.go

@@ -69,6 +69,11 @@ func initCmd() {
 		Run: func(cmd *cobra.Command, args []string) {
 			// fmt.Println("cmd:", cmd.Use, args)
 			runSrv = true
+
+			if rev {
+				printVersion()
+				os.Exit(0)
+			}
 		},
 	}
 
@@ -92,6 +97,7 @@ func initCmd() {
 		// viper.SetDefault(v.Name, v.Value)
 	}
 
+	rootCmd.Flags().BoolVarP(&rev, "version", "v", false, "display version info")
 	rootCmd.AddCommand(initToolCmd())
 
 	cobra.OnInitialize(func() {
@@ -127,8 +133,7 @@ func initToolCmd() *cobra.Command {
 	toolCmd.Run = func(cmd *cobra.Command, args []string) {
 		switch {
 		case rev:
-			fmt.Printf("%s v%s build on %s [%s, %s] commit_id(%s) \n",
-				APP_NAME, APP_VER, runtime.Version(), runtime.GOOS, runtime.GOARCH, CommitId)
+			printVersion()
 		case secret:
 			s, _ := utils.RandSecret(40, 60)
 			s = strings.Trim(s, "=")
@@ -145,3 +150,8 @@ func initToolCmd() *cobra.Command {
 
 	return toolCmd
 }
+
+func printVersion() {
+	fmt.Printf("%s v%s build on %s [%s, %s] commit_id(%s) \n",
+		APP_NAME, APP_VER, runtime.Version(), runtime.GOOS, runtime.GOARCH, CommitId)
+}

+ 18 - 13
anylink/server/base/config.go

@@ -24,7 +24,7 @@ var configs = []config{
 	{Typ: cfgStr, Name: "profile", Usage: "profile.xml file", ValStr: "./conf/profile.xml"},
 	{Typ: cfgStr, Name: "server_addr", Usage: "服务监听地址", ValStr: ":443"},
 	{Typ: cfgBool, Name: "server_dtls", Usage: "开启DTLS", ValBool: false},
-	{Typ: cfgStr, Name: "server_dtls_addr", Usage: "DTLS监听地址", ValStr: ":4433"},
+	{Typ: cfgStr, Name: "server_dtls_addr", Usage: "DTLS监听地址", ValStr: ":443"},
 	{Typ: cfgStr, Name: "admin_addr", Usage: "后台服务监听地址", ValStr: ":8800"},
 	{Typ: cfgBool, Name: "proxy_protocol", Usage: "TCP代理协议", ValBool: false},
 	{Typ: cfgStr, Name: "db_type", Usage: "数据库类型 [sqlite3 mysql postgres]", ValStr: "sqlite3"},
@@ -33,7 +33,7 @@ var configs = []config{
 	{Typ: cfgStr, Name: "cert_key", Usage: "证书密钥", ValStr: "./conf/vpn_cert.key"},
 	{Typ: cfgStr, Name: "files_path", Usage: "外部下载文件路径", ValStr: "./conf/files"},
 	{Typ: cfgStr, Name: "log_path", Usage: "日志文件路径,默认标准输出", ValStr: ""},
-	{Typ: cfgStr, Name: "log_level", Usage: "日志等级 [debug info warn error]", ValStr: "info"},
+	{Typ: cfgStr, Name: "log_level", Usage: "日志等级 [debug info warn error]", ValStr: "debug"},
 	{Typ: cfgBool, Name: "pprof", Usage: "开启pprof", ValBool: false},
 	{Typ: cfgStr, Name: "issuer", Usage: "系统名称", ValStr: "XX公司VPN"},
 	{Typ: cfgStr, Name: "admin_user", Usage: "管理用户名", ValStr: "admin"},
@@ -41,26 +41,31 @@ var configs = []config{
 	{Typ: cfgStr, Name: "jwt_secret", Usage: "JWT密钥", ValStr: defaultJwt},
 	{Typ: cfgStr, Name: "link_mode", Usage: "虚拟网络类型[tun tap macvtap ipvtap]", ValStr: "tun"},
 	{Typ: cfgStr, Name: "ipv4_master", Usage: "ipv4主网卡名称", ValStr: "eth0"},
-	{Typ: cfgStr, Name: "ipv4_cidr", Usage: "ip地址网段", ValStr: "192.168.10.0/24"},
-	{Typ: cfgStr, Name: "ipv4_gateway", Usage: "ipv4_gateway", ValStr: "192.168.10.1"},
-	{Typ: cfgStr, Name: "ipv4_start", Usage: "IPV4开始地址", ValStr: "192.168.10.100"},
-	{Typ: cfgStr, Name: "ipv4_end", Usage: "IPV4结束", ValStr: "192.168.10.200"},
+	{Typ: cfgStr, Name: "ipv4_cidr", Usage: "ip地址网段", ValStr: "192.168.90.0/24"},
+	{Typ: cfgStr, Name: "ipv4_gateway", Usage: "ipv4_gateway", ValStr: "192.168.90.1"},
+	{Typ: cfgStr, Name: "ipv4_start", Usage: "IPV4开始地址", ValStr: "192.168.90.100"},
+	{Typ: cfgStr, Name: "ipv4_end", Usage: "IPV4结束", ValStr: "192.168.90.200"},
 	{Typ: cfgStr, Name: "default_group", Usage: "默认用户组", ValStr: "one"},
 	{Typ: cfgStr, Name: "default_domain", Usage: "要发布的默认域", ValStr: ""},
 
-	{Typ: cfgInt, Name: "ip_lease", Usage: "IP租期(秒)", ValInt: 1209600},
-	{Typ: cfgInt, Name: "max_client", Usage: "最大用户连接", ValInt: 100},
+	{Typ: cfgInt, Name: "ip_lease", Usage: "IP租期(秒)", ValInt: 86400},
+	{Typ: cfgInt, Name: "max_client", Usage: "最大用户连接", ValInt: 200},
 	{Typ: cfgInt, Name: "max_user_client", Usage: "最大单用户连接", ValInt: 3},
-	{Typ: cfgInt, Name: "cstp_keepalive", Usage: "keepalive时间(秒)", ValInt: 20},
-	{Typ: cfgInt, Name: "cstp_dpd", Usage: "死链接检测时间(秒)", ValInt: 30},
-	{Typ: cfgInt, Name: "mobile_keepalive", Usage: "移动端keepalive接检测时间(秒)", ValInt: 50},
-	{Typ: cfgInt, Name: "mobile_dpd", Usage: "移动端死链接检测时间(秒)", ValInt: 60},
+	{Typ: cfgInt, Name: "cstp_keepalive", Usage: "keepalive时间(秒)", ValInt: 4},
+	{Typ: cfgInt, Name: "cstp_dpd", Usage: "死链接检测时间(秒)", ValInt: 10},
+	{Typ: cfgInt, Name: "mobile_keepalive", Usage: "移动端keepalive接检测时间(秒)", ValInt: 7},
+	{Typ: cfgInt, Name: "mobile_dpd", Usage: "移动端死链接检测时间(秒)", ValInt: 15},
 	{Typ: cfgInt, Name: "mtu", Usage: "最大传输单元MTU", ValInt: 1460},
-	{Typ: cfgInt, Name: "session_timeout", Usage: "session过期时间(秒)", ValInt: 3600},
+	{Typ: cfgInt, Name: "session_timeout", Usage: "session过期时间(秒)-用于断线重连,0永不过期", ValInt: 3600},
 	// {Typ: cfgInt, Name: "auth_timeout", Usage: "auth_timeout", ValInt: 0},
 	{Typ: cfgInt, Name: "audit_interval", Usage: "审计去重间隔(秒),-1关闭", ValInt: -1},
 
 	{Typ: cfgBool, Name: "show_sql", Usage: "显示sql语句,用于调试", ValBool: false},
+	{Typ: cfgBool, Name: "iptables_nat", Usage: "是否自动添加NAT", ValBool: true},
+	{Typ: cfgBool, Name: "compression", Usage: "启用压缩", ValBool: false},
+	{Typ: cfgInt, Name: "no_compress_limit", Usage: "低于及等于多少字节不压缩", ValInt: 256},
+
+	{Typ: cfgBool, Name: "display_error", Usage: "客户端显示详细错误信息(线上环境慎开启)", ValBool: false},
 }
 
 var envs = map[string]string{}

+ 30 - 16
anylink/server/base/log.go

@@ -10,11 +10,12 @@ import (
 )
 
 const (
-	_Debug = iota
-	_Info
-	_Warn
-	_Error
-	_Fatal
+	LogLevelTrace = iota
+	LogLevelDebug
+	LogLevelInfo
+	LogLevelWarn
+	LogLevelError
+	LogLevelFatal
 )
 
 var (
@@ -87,15 +88,20 @@ func GetBaseLog() *log.Logger {
 	return baseLog
 }
 
+func GetLogLevel() int {
+	return baseLevel
+}
+
 func logLevel2Int(l string) int {
 	levels = map[int]string{
-		_Debug: "Debug",
-		_Info:  "Info",
-		_Warn:  "Warn",
-		_Error: "Error",
-		_Fatal: "Fatal",
+		LogLevelTrace: "Trace",
+		LogLevelDebug: "Debug",
+		LogLevelInfo:  "Info",
+		LogLevelWarn:  "Warn",
+		LogLevelError: "Error",
+		LogLevelFatal: "Fatal",
 	}
-	lvl := _Info
+	lvl := LogLevelInfo
 	for k, v := range levels {
 		if strings.EqualFold(strings.ToLower(l), strings.ToLower(v)) {
 			lvl = k
@@ -109,8 +115,16 @@ func output(l int, s ...interface{}) {
 	_ = baseLog.Output(3, lvl+fmt.Sprintln(s...))
 }
 
+func Trace(v ...interface{}) {
+	l := LogLevelTrace
+	if baseLevel > l {
+		return
+	}
+	output(l, v...)
+}
+
 func Debug(v ...interface{}) {
-	l := _Debug
+	l := LogLevelDebug
 	if baseLevel > l {
 		return
 	}
@@ -118,7 +132,7 @@ func Debug(v ...interface{}) {
 }
 
 func Info(v ...interface{}) {
-	l := _Info
+	l := LogLevelInfo
 	if baseLevel > l {
 		return
 	}
@@ -126,7 +140,7 @@ func Info(v ...interface{}) {
 }
 
 func Warn(v ...interface{}) {
-	l := _Warn
+	l := LogLevelWarn
 	if baseLevel > l {
 		return
 	}
@@ -134,7 +148,7 @@ func Warn(v ...interface{}) {
 }
 
 func Error(v ...interface{}) {
-	l := _Error
+	l := LogLevelError
 	if baseLevel > l {
 		return
 	}
@@ -142,7 +156,7 @@ func Error(v ...interface{}) {
 }
 
 func Fatal(v ...interface{}) {
-	l := _Fatal
+	l := LogLevelFatal
 	if baseLevel > l {
 		return
 	}

+ 20 - 10
anylink/server/conf/server-sample.toml

@@ -30,7 +30,7 @@ jwt_secret = "abcdef.0123456789.abcdef"
 server_addr = ":443"
 #开启 DTLS, 默认关闭
 server_dtls = false
-server_dtls_addr = ":4433"
+server_dtls_addr = ":443"
 #后台服务监听地址
 admin_addr = ":8800"
 #开启tcp proxy protocol协议
@@ -40,26 +40,26 @@ link_mode = "tun"
 
 #客户端分配的ip地址池
 ipv4_master = "eth0"
-ipv4_cidr = "192.168.10.0/24"
-ipv4_gateway = "192.168.10.1"
-ipv4_start = "192.168.10.100"
-ipv4_end = "192.168.10.200"
+ipv4_cidr = "192.168.90.0/24"
+ipv4_gateway = "192.168.90.1"
+ipv4_start = "192.168.90.100"
+ipv4_end = "192.168.90.200"
 
 #最大客户端数量
 max_client = 100
 #单个用户同时在线数量
 max_user_client = 3
 #IP租期(秒)
-ip_lease = 1209600
+ip_lease = 86400
 
 #默认选择的组
 default_group = "one"
 
 #客户端失效检测时间(秒) dpd > keepalive
-cstp_keepalive = 20
-cstp_dpd = 30
-mobile_keepalive = 40
-mobile_dpd = 50
+cstp_keepalive = 6
+cstp_dpd = 10
+mobile_keepalive = 15
+mobile_dpd = 20
 
 #设置最大传输单元
 mtu = 1460
@@ -75,5 +75,15 @@ audit_interval = -1
 
 show_sql = false
 
+#是否自动添加nat
+iptables_nat = true
+
+#启用压缩
+compression = false
+#低于及等于多少字节不压缩
+no_compress_limit = 256
+
+#客户端显示详细错误信息(线上环境慎开启)
+display_error = false
 
 

+ 13 - 0
anylink/server/conf/server.toml

@@ -26,5 +26,18 @@ server_addr = ":443"
 admin_addr = ":8800"
 
 
+#客户端分配的ip地址池
+ipv4_master = "eth0"
+ipv4_cidr = "192.168.90.0/24"
+ipv4_gateway = "192.168.90.1"
+ipv4_start = "192.168.90.100"
+ipv4_end = "192.168.90.200"
+
+#是否自动添加nat
+iptables_nat = true
+
+
+#客户端显示详细错误信息(线上环境慎开启)
+display_error = false
 
 

+ 24 - 24
anylink/server/conf/vpn_cert.crt

@@ -1,36 +1,36 @@
 -----BEGIN CERTIFICATE-----
-MIIF9zCCBN+gAwIBAgIQBNH+cm5YH1O2NhfT+zB+ATANBgkqhkiG9w0BAQsFADBu
+MIIF9jCCBN6gAwIBAgIQAuUy6Rv6Bo3nDXn5FackbDANBgkqhkiG9w0BAQsFADBu
 MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
 d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg
-RFYgVExTIENBIC0gRzEwHhcNMjExMjEyMDAwMDAwWhcNMjIxMjEzMjM1OTU5WjAc
+RFYgVExTIENBIC0gRzEwHhcNMjMwMTAzMDAwMDAwWhcNMjQwMTAzMjM1OTU5WjAc
 MRowGAYDVQQDExF2cG4udGVzdC52cWlsdS5jbjCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAK2XO6Na//i0sMiV0nF+aDTbDibGiTLr+LFlhTIi1KX9IAU2
-Xboz1B8cxDro3g+CzgrGg0YMI4CxBiY56UT3jUTsLYBNpWPkbhlH+mpf0J7fgH29
-V1LAZKm2qR28y/krKHIbcGrfMAbXi6iVkVHhc+edvGCdAiDSyJgVSZbYV/s0LXLF
-0B0BokagwtvGIx7ik5uG4exuRCUKE3z0n6RXdN0eWBvKKHFhWEeaBIGzHjoDgAx/
-4VJ8XsW0tcwByiVRqpMFa1eG3HLMvi34M1qLzNv7dGPIkr1zjvlvTqhDpimXOi9C
-4N5ZOfZfNAyR8zU5+tBqSCvByavxLJwC//F7VQcCAwEAAaOCAuEwggLdMB8GA1Ud
-IwQYMBaAFFV0T7JyT/VgulDR1+ZRXJoBhxrXMB0GA1UdDgQWBBQKyNOGPzBPyqY9
-nxahHC+B6xT83TAcBgNVHREEFTATghF2cG4udGVzdC52cWlsdS5jbjAOBgNVHQ8B
+ggEPADCCAQoCggEBANJAJPYBvOP/7v8SgMIkVLIulN/ziPALvFcEwVnQDImUIky8
+4udy0fmvJ2E3E3NL6Qv14ZHDGtH7CafukimNWTT2BVmQBYiO1ZlUkHcHUX4IoYEh
+egdy2xw0WwknJWTOyvkRkeDhtT9QUpA/zeemS4q1TG95zRDf5htUR4OMZXsZpkQ2
+bkSgnLtdyUmw2nhfSWgsD9fbwr6WnOx/swsUe52N3sIDZ6JTgn3N7xeT3/lVJKVN
+wyYkZldialmRzrs6btr3mmnqpWObcc4FvKr/CLmoOSXl0I1wWsr+HnQ4X9hHsJUk
+jk3EZKfhH3mM37HF8apqztb6WjC3R96Zam6Z8bMCAwEAAaOCAuAwggLcMB8GA1Ud
+IwQYMBaAFFV0T7JyT/VgulDR1+ZRXJoBhxrXMB0GA1UdDgQWBBSUpPcW3emC2l0o
+q6qRBOBDMjQ2rDAcBgNVHREEFTATghF2cG4udGVzdC52cWlsdS5jbjAOBgNVHQ8B
 Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMD4GA1UdIAQ3
 MDUwMwYGZ4EMAQIBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQu
 Y29tL0NQUzCBgAYIKwYBBQUHAQEEdDByMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
 cC5kaWdpY2VydC5jb20wSgYIKwYBBQUHMAKGPmh0dHA6Ly9jYWNlcnRzLmRpZ2lj
 ZXJ0LmNvbS9FbmNyeXB0aW9uRXZlcnl3aGVyZURWVExTQ0EtRzEuY3J0MAkGA1Ud
-EwQCMAAwggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2ACl5vvCeOTkh8FZzn2Ol
-d+W+V32cYAr4+U1dJlwlXceEAAABfa0lBgAAAAQDAEcwRQIgEQ4wS5gyLMK30aeD
-xF3kWvsUhkd94HKIl13ckYnukGMCIQD1/6fFUAPjdw2k8f/ctJ7STUHeA1WoBy5H
-O/iXBRCkWgB2AFGjsPX9AXmcVm24N3iPDKR6zBsny/eeiEKaDf7UiwXlAAABfa0l
-BmYAAAQDAEcwRQIgOoguGrrlpwoxGiJHJNcEWbuH2AOJCDSDiun80DX9hUwCIQCJ
-cFCOe5E5VbgHrTWbQ0OUFS0epDgUiG8y9kjfkN1M5QB2AEHIyrHfIkZKEMahOglC
-h15OMYsbA+vrS8do8JBilgb2AAABfa0lBfoAAAQDAEcwRQIhAIHCUjXv+M3/jFOU
-AzjjMCISczShjqQ5FKqsIYNTUN46AiAom+II914ifwdFiS2xWI0ncSj8cxH6f+WZ
-UUQj9RczMDANBgkqhkiG9w0BAQsFAAOCAQEALj5oEwyU+gxVKhLFrBBtkoi9F0HQ
-jjSQZvOcKApSXjKS11VdmLGKuy85FSocw7VvDtZ4o43OhO79GMAMiPXroTnPIS5O
-ZNxfuusF6HpS+2Dq9UidnlxQmIaJ4A7PkX+NqAI4V6yr839SXKyHJROfXf9hNoJZ
-PJeZ94oMwXdeNjFkOismFpvaZcYq7t51xi5tkH/NaJHV5FEU8Or4zk/OoaPe3r+b
-2hpltIIaapoNVYLWLW7YS7hlvhjfwPypsR3ev4bTRWvT1tu9+AE+TG0OZqeWGucP
-6MjZI5gecOnkQVmBovkRi2lr26PDWrwnAlyoMI3ioU1XaTftIrBL2YalfQ==
+EwQCMAAwggF9BgorBgEEAdZ5AgQCBIIBbQSCAWkBZwB2AO7N0GTV2xrOxVy3nbTN
+E6Iyh0Z8vOzew1FIWUZxH7WbAAABhXXkyXMAAAQDAEcwRQIgVhLvLOPcW0V1xhBv
+5KSeqGHbAnRVhew3kutV3Bu1x+ICIQCbYjRtmkDo1hx6p0YNdNfkZ3N5u+syVjwH
+Al3a9NpVxgB1AEiw42vapkc0D+VqAvqdMOscUgHLVt0sgdm7v6s52IRzAAABhXXk
+yY4AAAQDAEYwRAIgcuscG2kkSGNvAsVH9CAtXjNUwk9UJriY0+3OtQ4WVrMCIAsC
+CkqEI1Ek5M26yrWt0Q7+u+UZ8rXhfYu3kcMMq7PVAHYAO1N3dT4tuYBOizBbBv5A
+O2fYT8P0x70ADS1yb+H61BcAAAGFdeTJkAAABAMARzBFAiAEJbJTN8hrRUZ6UaaD
+2TlyDQfzUvTkex0XGT6PGKHkagIhAJ+Kg6tdt/csKde2vdweu+dT01fzg/fq4q3o
+mjfPhFm1MA0GCSqGSIb3DQEBCwUAA4IBAQAKFTUHbpgKsXARCBIIfEZGqkOvaafm
+QaoNodc6cj0+LJCbuMzrTlkzmII0X/U52MBG8JCEIO8BPe5R4NIFqqaE066zQANq
+HOsROOJi2A+WTTZcSEHbH3uhdVwcEQHvDzaOEEJc9Ilz6pdYsrv+trOmeR5PeIxv
+t1jQacSwN1z6z0N4CRjBpePV/9nwETkEaKjQuXSoYlN+pczK/4nX2W9+E/OnwtZs
+ScyFffPtTLHf1u4eSYuBT/AdwaKHXetxWzh98GP9LRfQhm63Gs+/WcloYl489dG/
+FOFjch2TdmrPcUwxxGEbbPt3zXRxSVlzvIaf4gTUl2+PsKwbKy/w4OLS
 -----END CERTIFICATE-----
 -----BEGIN CERTIFICATE-----
 MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh

+ 25 - 25
anylink/server/conf/vpn_cert.key

@@ -1,27 +1,27 @@
 -----BEGIN RSA PRIVATE KEY-----
-MIIEpAIBAAKCAQEArZc7o1r/+LSwyJXScX5oNNsOJsaJMuv4sWWFMiLUpf0gBTZd
-ujPUHxzEOujeD4LOCsaDRgwjgLEGJjnpRPeNROwtgE2lY+RuGUf6al/Qnt+Afb1X
-UsBkqbapHbzL+Ssochtwat8wBteLqJWRUeFz5528YJ0CINLImBVJlthX+zQtcsXQ
-HQGiRqDC28YjHuKTm4bh7G5EJQoTfPSfpFd03R5YG8oocWFYR5oEgbMeOgOADH/h
-UnxexbS1zAHKJVGqkwVrV4bccsy+LfgzWovM2/t0Y8iSvXOO+W9OqEOmKZc6L0Lg
-3lk59l80DJHzNTn60GpIK8HJq/EsnAL/8XtVBwIDAQABAoIBACXjPEELO5Ms3Ojq
-ymO7E0N2DECqVIeouT7+yXOH5qHT/YkltI9PgJzJyoqRCOaZxh7T9RL000rjWFQ/
-j4pd/ZdtdQDr8Y077kvWSfGtt/r1DTZkfQqys0XXeFHlQx+/K7S8CG1LCVB0+yZw
-fqdAbeu/ob30huJjHyUSgF1MGufYvuII6x0CGORwzruWWFniXkg2z+9SP4x4RSfm
-exMUE4T4tlzR63QaW02xWEDTWCSQw/FgjpCWwryDVCmnLf63UhI+4hITqZLL+ROd
-sG/8Yp284q7BYBKk4/N1HD4W1vU+dls3glxZ22NCQKx+2RVtqTrRUd/d4AnxOmMR
-dnfh4AECgYEA7cl9NIRrtQdW+KFcoSdyP2F+SU74nSAh6Uolzwr9lHB+NbMJ5g79
-eU1zp3RAvSFg249L4cnceaFL1LTPcNN0xhpaJ7v5FQWk5tkddSmy2T3CAh8VwLXF
-487pgakO1SpS6uz+BtwsAFOS8k/GjYeSbPR4e9F/FbYAvGYwOLNj2ocCgYEAuuL8
-xnFnt95TwWptu4T97YXTeZRB17jiH1BhX+QawsSafagsWlSKihKMxYhfCHiwztS/
-KsCnkS6cH9slU3y4gvCiT1S4z1Qkw93ljUQXCzRIVEd9SxXoQMeRi+/5c239Fhnu
-aoxESAFWNXJZ5r9Jp3qukHvEtYn2FoE1Zkmu0YECgYApULgDdvqr4pGW85p/mbX9
-Ezh5DlKeImYh/bMiDTvQHdegBvKyWWprOCzfLJDPC8yjeXtqyMMZExB07dGZPfRt
-M0j03HFD2M41GgZHRC6CFnvuGG6UJEE0+s+Rqskb+pWbof/lOz4d9Gd02K2cC7FC
-YxvID7dwE0Z/dZXtVCYGYwKBgQCjckPKtoIUcBBmV1NzLiP66REEAuL27Q5ufpk7
-CT9SWioXfc6Ujd3AVeriE5uxyAQyUCSFGosy0UXgIoRpmOmyMwxxP1KGmTuyRc4u
-l39j4Czl8MQmuBkxFpk3fwB2sJopCzLV4qkRJIImKkVwJpofLI+hc22dq/QayJRQ
-Sl7ngQKBgQCkfcbQDvhkL6QKUC/K7MDGw9JMICLUpRyp6D3ibeL7i6WO6dkKde2t
-O/oLz2XvG0NR0nulhThpWUdyUWco3FZ038jiuY8ZZum5wdVBDOcDcnuBisE3Kzh8
-p7WycoWItAVxmyTKzHJIZ7pFQULYjap7gFSUPE9uBQZu09VKBtGPHA==
+MIIEpQIBAAKCAQEA0kAk9gG84//u/xKAwiRUsi6U3/OI8Au8VwTBWdAMiZQiTLzi
+53LR+a8nYTcTc0vpC/XhkcMa0fsJp+6SKY1ZNPYFWZAFiI7VmVSQdwdRfgihgSF6
+B3LbHDRbCSclZM7K+RGR4OG1P1BSkD/N56ZLirVMb3nNEN/mG1RHg4xlexmmRDZu
+RKCcu13JSbDaeF9JaCwP19vCvpac7H+zCxR7nY3ewgNnolOCfc3vF5Pf+VUkpU3D
+JiRmV2JqWZHOuzpu2veaaeqlY5txzgW8qv8Iuag5JeXQjXBayv4edDhf2EewlSSO
+TcRkp+EfeYzfscXxqmrO1vpaMLdH3plqbpnxswIDAQABAoIBAGKKzugAi4Q/Vch2
+ZyPXPF0hCQToE3QSxAzy/R53rRCkfekClMTO44xHpEjjs/mTiCBjd3xGeiEVrIJp
+hlb0WW3Bq2M9ZeKJs6JAaM9o/jB4oh2wT44DLqALB+oDz3puk+Jl8j34++a3YmMa
+jIq4veo+rBsJduwkTKjdeQE2ge/ODZEQ6bUmSjYo1P9LNGEyO2wmcVk+jHx0zBi7
+8fR3oY03Io+byuN0494Di1m3IpIdj3ma0MV5zJf31urLXqqYtOApouWL/yhZOIuo
+YW+mcuS7ZgK5FsqrUm0vGBcf24GcKhhlBlUu0mfLrCRrWLDsqJDQ/8alvuNP0IVm
+gqz0H5UCgYEA8yaKzMfkeRXSTERt6NZHo/8ShIn26Yf+pMDpVdYKfVBL254vGTeq
+B+LQhDpxZV1iMr1FNvkhHtNGQ74ZbWOj4+5Xjsllaw1ao2iGp1w/chuJ6FG/Go4q
+9FaY3eGiCqRJOQNivBxU/D7sN0y4b48HAEQdmp516pItlGtG+C8TY38CgYEA3VyD
+6EczHdAmPO7bdbYn/irfe78so1lHT04P0FiVg2W77ZKuQINTNDK9w/alYyZ2tH1b
+N2JznulJ6UDcl4xw43xJixxhme2jWPaYzmQUuQHviZ0D0tCgmOkN8bUnc9LSvEGA
+SnaiKbOtUfP8Z3c/mF997wuFNfdmhww8LBpbO80CgYEAmdRKf9/+5bQuhd3NAz99
+t31KQ9vdAEXvjmAVvx5ZKIrCU0EyXuvegHq4nM80qoJ3+83Omkbm80+K5pTAFXqy
+VyOU9Vro9N9P9o3MktlDsndFulrtYmmLN2YJ9GYpVD43rQA9WPE7uxI784hwLvP3
+4+00JXwW8b5lY76y+ZUe2RUCgYEAt6BQN/YgPCH4JmHKIWp64If2HbQntlWQJwRN
+b/qcBIT3EQu1iwSll85jxtSqu4YjwHOgoGAGI5PIYTsSApFY8AyhAUoI2OTdtSXS
++prg6dvmNhTPICk6n73seE5bLOR9NfdsEdk5ijhnlW09OyMb2S2VzR+UYIEbRvnq
+THeMqR0CgYEAwQ9aAIGD4t4DXjHHtz6Wqpbq6jj6mmYBsL9jqRgu8ASj1/THgWWn
+iUrlCbFIXWu2vnP1h0SBV56GA7MSTqAt0ZdTzpI1PRkMOIO9z1dN4Q2R1SEya1eF
+7/LPLqJIoLbILGvy6U3DLYdMckPZaoTPf5BNKD52paZcNbjkXTlVLSY=
 -----END RSA PRIVATE KEY-----

+ 0 - 5
anylink/server/cron/clear_statsinfo.go

@@ -43,10 +43,5 @@ func getTimeAgo(days int) string {
 	ts := time.Now().AddDate(0, 0, -days)
 	tsZero := time.Date(ts.Year(), ts.Month(), ts.Day(), 0, 0, 0, 0, time.Local)
 	timeS = tsZero.Format(dbdata.LayoutTimeFormat)
-	// UTC
-	switch base.Cfg.DbType {
-	case "sqlite3", "postgres":
-		timeS = tsZero.UTC().Format(dbdata.LayoutTimeFormat)
-	}
 	return timeS
 }

+ 20 - 0
anylink/server/cron/clear_user_act_log.go

@@ -0,0 +1,20 @@
+package cron
+
+import (
+	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
+)
+
+// 清除用户活动日志
+func ClearUserActLog() {
+	lifeDay, timesUp := isClearTime()
+	if !timesUp {
+		return
+	}
+	// 当审计日志永久保存时,则退出
+	if lifeDay <= 0 {
+		return
+	}
+	affected, err := dbdata.UserActLogIns.ClearUserActLog(getTimeAgo(lifeDay))
+	base.Info("Cron ClearUserActLog: ", affected, err)
+}

+ 5 - 0
anylink/server/cron/start.go

@@ -3,6 +3,8 @@ package cron
 import (
 	"time"
 
+	"github.com/bjdgyc/anylink/dbdata"
+	"github.com/bjdgyc/anylink/sessdata"
 	"github.com/go-co-op/gocron"
 )
 
@@ -10,5 +12,8 @@ func Start() {
 	s := gocron.NewScheduler(time.Local)
 	s.Cron("0 * * * *").Do(ClearAudit)
 	s.Cron("0 * * * *").Do(ClearStatsInfo)
+	s.Cron("0 * * * *").Do(ClearUserActLog)
+	s.Every(1).Day().At("00:00").Do(sessdata.CloseUserLimittimeSession)
+	s.Every(1).Day().At("00:00").Do(dbdata.ReNewCert)
 	s.StartAsync()
 }

+ 6 - 0
anylink/server/dbdata/audit.go

@@ -14,6 +14,7 @@ type SearchCon struct {
 	AccessProto string   `json:"access_proto"`
 	Date        []string `json:"date"`
 	Info        string   `json:"info"`
+	Sort        int      `json:"sort"`
 }
 
 func GetAuditSession(search string) *xorm.Session {
@@ -47,6 +48,11 @@ func GetAuditSession(search string) *xorm.Session {
 	if searchData.Info != "" {
 		session.And("info LIKE ?", "%"+searchData.Info+"%")
 	}
+	if searchData.Sort == 1 {
+		session.OrderBy("id desc")
+	} else {
+		session.OrderBy("id asc")
+	}
 	return session
 }
 

+ 1 - 1
anylink/server/dbdata/audit_test.go

@@ -15,7 +15,7 @@ func TestSearchAudit(t *testing.T) {
 	defer closeIpdata()
 
 	currDateVal := "2022-07-24 00:00:00"
-	CreatedAt, _ := time.Parse("2006-01-02 15:04:05", currDateVal)
+	CreatedAt, _ := time.ParseInLocation("2006-01-02 15:04:05", currDateVal, time.Local)
 
 	dataTest := AccessAudit{
 		Username:    "Test",

+ 412 - 0
anylink/server/dbdata/cert.go

@@ -0,0 +1,412 @@
+package dbdata
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"math/big"
+	"net"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/pion/dtls/v2/pkg/crypto/selfsign"
+
+	"github.com/bjdgyc/anylink/base"
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/challenge"
+	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/providers/dns/alidns"
+	"github.com/go-acme/lego/v4/providers/dns/cloudflare"
+	"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
+	"github.com/go-acme/lego/v4/registration"
+)
+
+var (
+	// nameToCertificate mutex
+	ntcMux            sync.RWMutex
+	nameToCertificate = make(map[string]*tls.Certificate)
+	tempCert          *tls.Certificate
+)
+
+func init() {
+	c, _ := selfsign.GenerateSelfSignedWithDNS("localhost")
+	tempCert = &c
+}
+
+type SettingLetsEncrypt struct {
+	Domain   string `json:"domain"`
+	Legomail string `json:"legomail"`
+	Name     string `json:"name"`
+	Renew    bool   `json:"renew"`
+	DNSProvider
+}
+
+type DNSProvider struct {
+	AliYun struct {
+		APIKey    string `json:"apiKey"`
+		SecretKey string `json:"secretKey"`
+	} `json:"aliyun"`
+
+	TXCloud struct {
+		SecretID  string `json:"secretId"`
+		SecretKey string `json:"secretKey"`
+	} `json:"txcloud"`
+	CfCloud struct {
+		AuthEmail string `json:"authEmail"`
+		AuthKey   string `json:"authKey"`
+	} `json:"cfcloud"`
+}
+type LegoUserData struct {
+	Email        string                 `json:"email"`
+	Registration *registration.Resource `json:"registration"`
+	Key          []byte                 `json:"key"`
+}
+type LegoUser struct {
+	Email        string
+	Registration *registration.Resource
+	Key          *ecdsa.PrivateKey
+}
+
+type LeGoClient struct {
+	mutex  sync.Mutex
+	Client *lego.Client
+	Cert   *certificate.Resource
+	LegoUserData
+}
+
+func GetDNSProvider(l *SettingLetsEncrypt) (Provider challenge.Provider, err error) {
+	switch l.Name {
+	case "aliyun":
+		if Provider, err = alidns.NewDNSProviderConfig(&alidns.Config{APIKey: l.DNSProvider.AliYun.APIKey, SecretKey: l.DNSProvider.AliYun.SecretKey, TTL: 600}); err != nil {
+			return
+		}
+	case "txcloud":
+		if Provider, err = tencentcloud.NewDNSProviderConfig(&tencentcloud.Config{SecretID: l.DNSProvider.TXCloud.SecretID, SecretKey: l.DNSProvider.TXCloud.SecretKey, TTL: 600}); err != nil {
+			return
+		}
+	case "cloudflare":
+		if Provider, err = cloudflare.NewDNSProviderConfig(&cloudflare.Config{AuthEmail: l.DNSProvider.CfCloud.AuthEmail, AuthKey: l.DNSProvider.CfCloud.AuthKey, TTL: 600}); err != nil {
+			return
+		}
+	}
+	return
+}
+func (u *LegoUser) GetEmail() string {
+	return u.Email
+}
+func (u LegoUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+func (u *LegoUser) GetPrivateKey() crypto.PrivateKey {
+	return u.Key
+}
+
+func (l *LegoUserData) SaveUserData(u *LegoUser) error {
+	key, err := x509.MarshalECPrivateKey(u.Key)
+	if err != nil {
+		return err
+	}
+	l.Email = u.Email
+	l.Registration = u.Registration
+	l.Key = key
+	if err := SettingSet(l); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (l *LegoUserData) GetUserData(d *SettingLetsEncrypt) (*LegoUser, error) {
+	if err := SettingGet(l); err != nil {
+		return nil, err
+	}
+	if l.Email != "" {
+		key, err := x509.ParseECPrivateKey(l.Key)
+		if err != nil {
+			return nil, err
+		}
+		return &LegoUser{
+			Email:        l.Email,
+			Registration: l.Registration,
+			Key:          key,
+		}, nil
+	}
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+	return &LegoUser{
+		Email: d.Legomail,
+		Key:   privateKey,
+	}, nil
+}
+func ReNewCert() {
+	_, certtime, err := ParseCert()
+	if err != nil {
+		base.Error(err)
+		return
+	}
+	if certtime.AddDate(0, 0, -7).Before(time.Now()) {
+		config := &SettingLetsEncrypt{}
+		if err := SettingGet(config); err != nil {
+			base.Error(err)
+			return
+		}
+		if config.Renew {
+			client := &LeGoClient{}
+			if err := client.NewClient(config); err != nil {
+				base.Error(err)
+				return
+			}
+			if err := client.RenewCert(base.Cfg.CertFile, base.Cfg.CertKey); err != nil {
+				base.Error(err)
+				return
+			}
+			base.Info("证书续期成功")
+		}
+	} else {
+		base.Info(fmt.Sprintf("证书过期时间:%s", certtime.Local().Format("2006-1-2 15:04:05")))
+	}
+}
+
+func (c *LeGoClient) NewClient(l *SettingLetsEncrypt) error {
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+	legouser, err := c.GetUserData(l)
+	if err != nil {
+		return err
+	}
+	config := lego.NewConfig(legouser)
+	config.CADirURL = lego.LEDirectoryProduction
+	config.Certificate.KeyType = certcrypto.RSA2048
+
+	client, err := lego.NewClient(config)
+	if err != nil {
+		return err
+	}
+	Provider, err := GetDNSProvider(l)
+	if err != nil {
+		return err
+	}
+	if err := client.Challenge.SetDNS01Provider(Provider, dns01.AddRecursiveNameservers([]string{"114.114.114.114", "114.114.115.115"})); err != nil {
+		return err
+	}
+	if legouser.Registration == nil {
+		reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+		if err != nil {
+			return err
+		}
+		legouser.Registration = reg
+		c.SaveUserData(legouser)
+	}
+	c.Client = client
+	return nil
+}
+
+func (c *LeGoClient) GetCert(domain string) error {
+	// 申请证书
+	certificates, err := c.Client.Certificate.Obtain(
+		certificate.ObtainRequest{
+			Domains: []string{domain},
+			Bundle:  true,
+		})
+	if err != nil {
+		return err
+	}
+	c.Cert = certificates
+	// 保存证书
+	if err := c.SaveCert(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *LeGoClient) RenewCert(certFile, keyFile string) error {
+	cert, err := os.ReadFile(certFile)
+	if err != nil {
+		return err
+	}
+	key, err := os.ReadFile(keyFile)
+	if err != nil {
+		return err
+	}
+	// 续期证书
+	renewcert, err := c.Client.Certificate.Renew(certificate.Resource{
+		Certificate: cert,
+		PrivateKey:  key,
+	}, true, false, "")
+	if err != nil {
+		return err
+	}
+	c.Cert = renewcert
+	// 保存更新证书
+	if err := c.SaveCert(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *LeGoClient) SaveCert() error {
+	err := os.WriteFile(base.Cfg.CertFile, c.Cert.Certificate, 0600)
+	if err != nil {
+		return err
+	}
+	err = os.WriteFile(base.Cfg.CertKey, c.Cert.PrivateKey, 0600)
+	if err != nil {
+		return err
+	}
+	if tlscert, _, err := ParseCert(); err != nil {
+		return err
+	} else {
+		LoadCertificate(tlscert)
+	}
+	return nil
+}
+
+func ParseCert() (*tls.Certificate, *time.Time, error) {
+	_, errCert := os.Stat(base.Cfg.CertFile)
+	_, errKey := os.Stat(base.Cfg.CertKey)
+	if os.IsNotExist(errCert) || os.IsNotExist(errKey) {
+		err := PrivateCert()
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+	cert, err := tls.LoadX509KeyPair(base.Cfg.CertFile, base.Cfg.CertKey)
+	if err != nil || errors.Is(err, os.ErrNotExist) {
+		PrivateCert()
+		return nil, nil, err
+	}
+	parseCert, err := x509.ParseCertificate(cert.Certificate[0])
+	if err != nil {
+		return nil, nil, err
+	}
+	return &cert, &parseCert.NotAfter, nil
+}
+
+func PrivateCert() error {
+	// 创建一个RSA密钥对
+	priv, _ := rsa.GenerateKey(rand.Reader, 2048)
+	pub := &priv.PublicKey
+
+	// 生成一个自签名证书
+	template := x509.Certificate{
+		SerialNumber:          big.NewInt(1658),
+		Subject:               pkix.Name{CommonName: "localhost"},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().Add(time.Hour * 24 * 365),
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		IPAddresses:           []net.IP{},
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)
+	if err != nil {
+		return err
+	}
+
+	// 将证书编码为PEM格式并将其写入文件
+	certOut, _ := os.OpenFile(base.Cfg.CertFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
+	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+	certOut.Close()
+
+	// 将私钥编码为PEM格式并将其写入文件
+	keyOut, _ := os.OpenFile(base.Cfg.CertKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
+	keyOut.Close()
+	cert, err := tls.LoadX509KeyPair(base.Cfg.CertFile, base.Cfg.CertKey)
+	if err != nil {
+		return err
+	}
+	LoadCertificate(&cert)
+	return nil
+}
+
+func getTempCertificate() (*tls.Certificate, error) {
+	var err error
+	var cert tls.Certificate
+	if tempCert == nil {
+		cert, err = selfsign.GenerateSelfSignedWithDNS("localhost")
+		tempCert = &cert
+	}
+	return tempCert, err
+}
+
+func GetCertificateBySNI(commonName string) (*tls.Certificate, error) {
+	ntcMux.RLock()
+	defer ntcMux.RUnlock()
+
+	// Copy from tls.Config getCertificate()
+	name := strings.ToLower(commonName)
+	if cert, ok := nameToCertificate[name]; ok {
+		return cert, nil
+	}
+	if len(name) > 0 {
+		labels := strings.Split(name, ".")
+		labels[0] = "*"
+		wildcardName := strings.Join(labels, ".")
+		if cert, ok := nameToCertificate[wildcardName]; ok {
+			return cert, nil
+		}
+	}
+	// TODO 默认证书 兼容不支持 SNI 的客户端
+	if cert, ok := nameToCertificate["default"]; ok {
+		return cert, nil
+	}
+
+	return getTempCertificate()
+}
+
+func LoadCertificate(cert *tls.Certificate) {
+	buildNameToCertificate(cert)
+}
+
+// Copy from tls.Config BuildNameToCertificate()
+func buildNameToCertificate(cert *tls.Certificate) {
+	ntcMux.Lock()
+	defer ntcMux.Unlock()
+
+	// TODO 设置默认证书
+	nameToCertificate["default"] = cert
+
+	x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
+	if err != nil {
+		return
+	}
+	startTime := x509Cert.NotBefore.String()
+	expiredTime := x509Cert.NotAfter.String()
+	if x509Cert.Subject.CommonName != "" && len(x509Cert.DNSNames) == 0 {
+		commonName := x509Cert.Subject.CommonName
+		fmt.Printf("┏ Load Certificate: %s\n", commonName)
+		fmt.Printf("┠╌╌ Start Time:     %s\n", startTime)
+		fmt.Printf("┖╌╌ Expired Time:   %s\n", expiredTime)
+		nameToCertificate[commonName] = cert
+	}
+	for _, san := range x509Cert.DNSNames {
+		fmt.Printf("┏ Load Certificate: %s\n", san)
+		fmt.Printf("┠╌╌ Start Time:     %s\n", startTime)
+		fmt.Printf("┖╌╌ Expired Time:   %s\n", expiredTime)
+		nameToCertificate[san] = cert
+	}
+}
+
+// func Scrypt(passwd string) string {
+// 	salt := []byte{0xc8, 0x28, 0xf2, 0x58, 0xa7, 0x6a, 0xad, 0x7b}
+// 	hashPasswd, err := scrypt.Key([]byte(passwd), salt, 1<<15, 8, 1, 32)
+// 	if err != nil {
+// 		return err.Error()
+// 	}
+// 	return base64.StdEncoding.EncodeToString(hashPasswd)
+// }

+ 41 - 1
anylink/server/dbdata/db.go

@@ -1,6 +1,8 @@
 package dbdata
 
 import (
+	"time"
+
 	"github.com/bjdgyc/anylink/base"
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
@@ -19,6 +21,9 @@ func GetXdb() *xorm.Engine {
 func initDb() {
 	var err error
 	xdb, err = xorm.NewEngine(base.Cfg.DbType, base.Cfg.DbSource)
+	// 初始化xorm时区
+	xdb.DatabaseTZ = time.Local
+	xdb.TZLocation = time.Local
 	if err != nil {
 		base.Fatal(err)
 	}
@@ -28,7 +33,7 @@ func initDb() {
 	}
 
 	// 初始化数据库
-	err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{})
+	err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{}, &UserActLog{})
 	if err != nil {
 		base.Fatal(err)
 	}
@@ -94,6 +99,36 @@ func addInitData() error {
 		return err
 	}
 
+	// SettingDnsProvider
+	provider := &SettingLetsEncrypt{
+		Domain:   "vpn.xxx.com",
+		Legomail: "legomail",
+		Name:     "aliyun",
+		Renew:    false,
+		DNSProvider: DNSProvider{
+			AliYun: struct {
+				APIKey    string `json:"apiKey"`
+				SecretKey string `json:"secretKey"`
+			}{APIKey: "", SecretKey: ""},
+			TXCloud: struct {
+				SecretID  string `json:"secretId"`
+				SecretKey string `json:"secretKey"`
+			}{SecretID: "", SecretKey: ""},
+			CfCloud: struct {
+				AuthEmail string `json:"authEmail"`
+				AuthKey   string `json:"authKey"`
+			}{AuthEmail: "", AuthKey: ""}},
+	}
+	err = SettingSessAdd(sess, provider)
+	if err != nil {
+		return err
+	}
+	// LegoUser
+	legouser := &LegoUserData{}
+	err = SettingSessAdd(sess, legouser)
+	if err != nil {
+		return err
+	}
 	// SettingOther
 	other := &SettingOther{
 		LinkAddr:    "vpn.xx.com",
@@ -123,6 +158,7 @@ func addInitData() error {
 		AllowLan:     true,
 		ClientDns:    []ValData{{Val: "114.114.114.114"}},
 		RouteInclude: []ValData{{Val: All}},
+		Status:       1,
 	}
 	err = SetGroup(&g1)
 	if err != nil {
@@ -143,8 +179,12 @@ const accountMail = `<p>您好:</p>
     用户组: <b>{{.Group}}</b> <br/>
     用户名: <b>{{.Username}}</b> <br/>
     用户PIN码: <b>{{.PinCode}}</b> <br/>
+    <!-- 
     用户动态码(3天后失效):<br/>
     <img src="{{.OtpImg}}"/>
+    -->
+    用户动态码(请妥善保存):<br/>
+    <img src="{{.OtpImgBase64}}"/>
 </p>
 <div>
     使用说明:

+ 29 - 0
anylink/server/dbdata/group.go

@@ -74,6 +74,20 @@ func GetGroupNames() []string {
 	return names
 }
 
+func GetGroupNamesNormal() []string {
+	var datas []Group
+	err := FindWhere(&datas, 0, 0, "status=1")
+	if err != nil {
+		base.Error(err)
+		return nil
+	}
+	var names []string
+	for _, v := range datas {
+		names = append(names, v.Name)
+	}
+	return names
+}
+
 func GetGroupNamesIds() []GroupNameId {
 	var datas []Group
 	err := Find(&datas, 0, 0)
@@ -211,6 +225,21 @@ func SetGroup(g *Group) error {
 	return err
 }
 
+func GroupAuthLogin(name, pwd string, authData map[string]interface{}) error {
+	g := &Group{Auth: authData}
+	authType := g.Auth["type"].(string)
+	if _, ok := authRegistry[authType]; !ok {
+		return errors.New("未知的认证方式: " + authType)
+	}
+	auth := makeInstance(authType).(IUserAuth)
+	err := auth.checkData(g.Auth)
+	if err != nil {
+		return err
+	}
+	err = auth.checkUser(name, pwd, g)
+	return err
+}
+
 func parseIpNet(s string) (string, *net.IPNet, error) {
 	ip, ipNet, err := net.ParseCIDR(s)
 	if err != nil {

+ 8 - 7
anylink/server/dbdata/group_test.go

@@ -46,13 +46,14 @@ func TestGetGroupNames(t *testing.T) {
 	authData = map[string]interface{}{
 		"type": "ldap",
 		"ldap": map[string]interface{}{
-			"addr":        "192.168.8.12:389",
-			"tls":         true,
-			"bind_name":   "[email protected]",
-			"bind_pwd":    "afdbfdsafds",
-			"base_dn":     "dc=abc,dc=com",
-			"search_attr": "sAMAccountName",
-			"member_of":   "cn=vpn,cn=user,dc=abc,dc=com",
+			"addr":         "192.168.8.12:389",
+			"tls":          true,
+			"bind_name":    "[email protected]",
+			"bind_pwd":     "afdbfdsafds",
+			"base_dn":      "dc=abc,dc=com",
+			"object_class": "person",
+			"search_attr":  "sAMAccountName",
+			"member_of":    "cn=vpn,cn=user,dc=abc,dc=com",
 		},
 	}
 	g7 := Group{Name: "g7", ClientDns: []ValData{{Val: "114.114.114.114"}}, Auth: authData}

+ 20 - 11
anylink/server/dbdata/ip_map.go

@@ -2,20 +2,22 @@ package dbdata
 
 import (
 	"errors"
+	"net"
 	"time"
 )
 
-// type IpMap struct {
-// 	Id        int       `json:"id" xorm:"pk autoincr not null"`
-// 	IpAddr    string    `json:"ip_addr" xorm:"not null unique"`
-// 	MacAddr   string    `json:"mac_addr" xorm:"not null unique"`
-// 	Username  string    `json:"username"`
-// 	Keep      bool      `json:"keep"` // 保留 ip-mac 绑定
-// 	KeepTime  time.Time `json:"keep_time"`
-// 	Note      string    `json:"note"` // 备注
-// 	LastLogin time.Time `json:"last_login"`
-// 	UpdatedAt time.Time `json:"updated_at"`
-// }
+type IpMap struct {
+	Id        int       `json:"id" xorm:"pk autoincr not null"`
+	IpAddr    string    `json:"ip_addr" xorm:"varchar(32) not null unique"`
+	MacAddr   string    `json:"mac_addr" xorm:"varchar(32) not null unique"`
+	UniqueMac bool      `json:"unique_mac" xorm:"Bool index"`
+	Username  string    `json:"username" xorm:"varchar(60)"`
+	Keep      bool      `json:"keep" xorm:"Bool"` // 保留 ip-mac 绑定
+	KeepTime  time.Time `json:"keep_time" xorm:"DateTime"`
+	Note      string    `json:"note" xorm:"varchar(255)"` // 备注
+	LastLogin time.Time `json:"last_login" xorm:"DateTime"`
+	UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
+}
 
 func SetIpMap(v *IpMap) error {
 	var err error
@@ -24,6 +26,13 @@ func SetIpMap(v *IpMap) error {
 		return errors.New("IP或MAC错误")
 	}
 
+	macHw, err := net.ParseMAC(v.MacAddr)
+	if err != nil {
+		return errors.New("MAC错误")
+	}
+	// 统一macAddr的格式
+	v.MacAddr = macHw.String()
+
 	v.UpdatedAt = time.Now()
 	if v.Id > 0 {
 		err = Set(v)

+ 3 - 3
anylink/server/dbdata/setting.go

@@ -21,8 +21,9 @@ type SettingSmtp struct {
 }
 
 type SettingAuditLog struct {
-	LifeDay   int    `json:"life_day"`
-	ClearTime string `json:"clear_time"`
+	AuditInterval int    `json:"audit_interval"`
+	LifeDay       int    `json:"life_day"`
+	ClearTime     string `json:"clear_time"`
 }
 
 type SettingOther struct {
@@ -48,7 +49,6 @@ func SettingSessAdd(sess *xorm.Session, data interface{}) error {
 	v, _ := json.Marshal(data)
 	s := &Setting{Name: name, Data: v}
 	_, err := sess.InsertOne(s)
-
 	return err
 }
 

+ 0 - 6
anylink/server/dbdata/statsinfo.go

@@ -199,12 +199,6 @@ func (s *StatsInfo) getScopeDetail(scope string) (sd *ScopeDetail) {
 	}
 	sd.fsTime = sd.sTime.Format(LayoutTimeFormat)
 	sd.feTime = sd.eTime.Format(LayoutTimeFormat)
-	// UTC
-	switch base.Cfg.DbType {
-	case "sqlite3", "postgres":
-		sd.fsTime = sd.sTime.UTC().Format(LayoutTimeFormat)
-		sd.feTime = sd.eTime.UTC().Format(LayoutTimeFormat)
-	}
 	return
 }
 

+ 23 - 18
anylink/server/dbdata/tables.go

@@ -29,26 +29,31 @@ type User struct {
 	Nickname string `json:"nickname" xorm:"varchar(255)"`
 	Email    string `json:"email" xorm:"varchar(255)"`
 	// Password  string    `json:"password"`
-	PinCode    string    `json:"pin_code" xorm:"varchar(32)"`
-	OtpSecret  string    `json:"otp_secret" xorm:"varchar(255)"`
-	DisableOtp bool      `json:"disable_otp" xorm:"Bool"` // 禁用otp
-	Groups     []string  `json:"groups" xorm:"Text"`
-	Status     int8      `json:"status" xorm:"Int"` // 1正常
-	SendEmail  bool      `json:"send_email" xorm:"Bool"`
-	CreatedAt  time.Time `json:"created_at" xorm:"DateTime created"`
-	UpdatedAt  time.Time `json:"updated_at" xorm:"DateTime updated"`
+	PinCode    string     `json:"pin_code" xorm:"varchar(32)"`
+	LimitTime  *time.Time `json:"limittime,omitempty" xorm:"Datetime limittime"` // 值为null时,前端不显示
+	OtpSecret  string     `json:"otp_secret" xorm:"varchar(255)"`
+	DisableOtp bool       `json:"disable_otp" xorm:"Bool"` // 禁用otp
+	Groups     []string   `json:"groups" xorm:"Text"`
+	Status     int8       `json:"status" xorm:"Int"` // 1正常
+	SendEmail  bool       `json:"send_email" xorm:"Bool"`
+	CreatedAt  time.Time  `json:"created_at" xorm:"DateTime created"`
+	UpdatedAt  time.Time  `json:"updated_at" xorm:"DateTime updated"`
 }
 
-type IpMap struct {
-	Id        int       `json:"id" xorm:"pk autoincr not null"`
-	IpAddr    string    `json:"ip_addr" xorm:"varchar(32) not null unique"`
-	MacAddr   string    `json:"mac_addr" xorm:"varchar(32) not null unique"`
-	Username  string    `json:"username" xorm:"varchar(60)"`
-	Keep      bool      `json:"keep" xorm:"Bool"` // 保留 ip-mac 绑定
-	KeepTime  time.Time `json:"keep_time" xorm:"DateTime"`
-	Note      string    `json:"note" xorm:"varchar(255)"` // 备注
-	LastLogin time.Time `json:"last_login" xorm:"DateTime"`
-	UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
+type UserActLog struct {
+	Id              int       `json:"id" xorm:"pk autoincr not null"`
+	Username        string    `json:"username" xorm:"varchar(60)"`
+	GroupName       string    `json:"group_name" xorm:"varchar(60)"`
+	IpAddr          string    `json:"ip_addr" xorm:"varchar(32)"`
+	RemoteAddr      string    `json:"remote_addr" xorm:"varchar(32)"`
+	Os              uint8     `json:"os" xorm:"not null default 0 Int"`
+	Client          uint8     `json:"client" xorm:"not null default 0 Int"`
+	Version         string    `json:"version" xorm:"varchar(15)"`
+	DeviceType      string    `json:"device_type" xorm:"varchar(128) not null default ''"`
+	PlatformVersion string    `json:"platform_version" xorm:"varchar(128) not null default ''"`
+	Status          uint8     `json:"status" xorm:"not null default 0 Int"`
+	Info            string    `json:"info" xorm:"varchar(255) not null default ''"` // 详情
+	CreatedAt       time.Time `json:"created_at" xorm:"DateTime created"`
 }
 
 type Setting struct {

+ 21 - 1
anylink/server/dbdata/user.go

@@ -104,7 +104,12 @@ func checkLocalUser(name, pwd, group string) error {
 	v := &User{}
 	err := One("Username", name, v)
 	if err != nil || v.Status != 1 {
-		return fmt.Errorf("%s %s", name, "用户名错误")
+		switch v.Status {
+		case 0:
+			return fmt.Errorf("%s %s", name, "用户不存在或用户已停用")
+		case 2:
+			return fmt.Errorf("%s %s", name, "用户已过期")
+		}
 	}
 	// 判断用户组信息
 	if !utils.InArrStr(v.Groups, group) {
@@ -128,6 +133,21 @@ func checkLocalUser(name, pwd, group string) error {
 	return nil
 }
 
+// 用户过期时间到达后,更新用户状态,并返回一个状态为过期的用户切片
+func CheckUserlimittime() (limitUser []interface{}) {
+	if _, err := xdb.Where("limittime <= ?", time.Now()).And("status = ?", 1).Update(&User{Status: 2}); err != nil {
+		return
+	}
+	user := make(map[int64]User)
+	if err := xdb.Where("status != ?", 1).Find(user); err != nil {
+		return
+	}
+	for _, v := range user {
+		limitUser = append(limitUser, v.Username)
+	}
+	return
+}
+
 var (
 	userOtpMux = sync.Mutex{}
 	userOtp    = map[string]time.Time{}

+ 210 - 0
anylink/server/dbdata/user_act_log.go

@@ -0,0 +1,210 @@
+package dbdata
+
+import (
+	"net/url"
+	"regexp"
+	"strings"
+
+	"github.com/bjdgyc/anylink/base"
+	"github.com/ivpusic/grpool"
+	"github.com/spf13/cast"
+	"xorm.io/xorm"
+)
+
+const (
+	UserAuthFail      = 0 // 认证失败
+	UserAuthSuccess   = 1 // 认证成功
+	UserConnected     = 2 // 连线成功
+	UserLogout        = 3 // 用户登出
+	UserLogoutLose    = 0 // 用户掉线
+	UserLogoutBanner  = 1 // 用户banner弹窗取消
+	UserLogoutClient  = 2 // 用户主动登出
+	UserLogoutTimeout = 3 // 用户超时登出
+	UserLogoutAdmin   = 4 // 账号被管理员踢下线
+	UserLogoutExpire  = 5 // 账号过期被踢下线
+)
+
+type UserActLogProcess struct {
+	Pool      *grpool.Pool
+	StatusOps []string
+	OsOps     []string
+	ClientOps []string
+	InfoOps   []string
+}
+
+var (
+	UserActLogIns = &UserActLogProcess{
+		Pool: grpool.NewPool(1, 100),
+		StatusOps: []string{ // 操作类型
+			UserAuthFail:    "认证失败",
+			UserAuthSuccess: "认证成功",
+			UserConnected:   "连接成功",
+			UserLogout:      "用户登出",
+		},
+		OsOps: []string{ // 操作系统
+			0: "Unknown",
+			1: "Windows",
+			2: "macOS",
+			3: "Linux",
+			4: "Android",
+			5: "iOS",
+		},
+		ClientOps: []string{ // 客户端
+			0: "Unknown",
+			1: "AnyConnect",
+			2: "OpenConnect",
+			3: "AnyLink",
+		},
+		InfoOps: []string{ // 信息
+			UserLogoutLose:    "用户掉线",
+			UserLogoutBanner:  "用户取消弹窗/客户端发起的logout",
+			UserLogoutClient:  "用户/客户端主动断开",
+			UserLogoutTimeout: "Session过期被踢下线",
+			UserLogoutAdmin:   "账号被管理员踢下线",
+			UserLogoutExpire:  "账号过期被踢下线",
+		},
+	}
+)
+
+// 异步写入用户操作日志
+func (ua *UserActLogProcess) Add(u UserActLog, userAgent string) {
+	// os, client, ver
+	os_idx, client_idx, ver := ua.ParseUserAgent(userAgent)
+	u.Os = os_idx
+	u.Client = client_idx
+	u.Version = ver
+	u.RemoteAddr = strings.Split(u.RemoteAddr, ":")[0]
+	// remove extra characters
+	infoSlice := strings.Split(u.Info, " ")
+	infoLen := len(infoSlice)
+	if infoLen > 1 {
+		if u.Username == infoSlice[0] {
+			u.Info = strings.Join(infoSlice[1:], " ")
+		}
+		// delete - char
+		if infoLen > 2 && infoSlice[1] == "-" {
+			u.Info = u.Info[2:]
+		}
+	}
+	// limit the max length of char
+	u.Version = substr(u.Version, 0, 15)
+	u.DeviceType = substr(u.DeviceType, 0, 128)
+	u.PlatformVersion = substr(u.PlatformVersion, 0, 128)
+	u.Info = substr(u.Info, 0, 255)
+
+	UserActLogIns.Pool.JobQueue <- func() {
+		err := Add(u)
+		if err != nil {
+			base.Error("Add UserActLog error: ", err)
+		}
+	}
+}
+
+// 转义操作类型, 方便vue显示
+func (ua *UserActLogProcess) GetStatusOpsWithTag() interface{} {
+	type StatusTag struct {
+		Key   int    `json:"key"`
+		Value string `json:"value"`
+		Tag   string `json:"tag"`
+	}
+	var res []StatusTag
+	for k, v := range ua.StatusOps {
+		tag := "info"
+		switch k {
+		case UserAuthFail:
+			tag = "danger"
+		case UserAuthSuccess:
+			tag = "success"
+		case UserConnected:
+			tag = ""
+		}
+		res = append(res, StatusTag{k, v, tag})
+	}
+	return res
+}
+
+func (ua *UserActLogProcess) GetInfoOpsById(id uint8) string {
+	return ua.InfoOps[id]
+}
+
+// 解析user agent
+func (ua *UserActLogProcess) ParseUserAgent(userAgent string) (os_idx, client_idx uint8, ver string) {
+	// Unknown
+	if len(userAgent) == 0 {
+		return 0, 0, ""
+	}
+	// OS
+	os_idx = 0
+	if strings.Contains(userAgent, "windows") {
+		os_idx = 1
+	} else if strings.Contains(userAgent, "mac os") || strings.Contains(userAgent, "darwin_i386") {
+		os_idx = 2
+	} else if strings.Contains(userAgent, "darwin_arm") || strings.Contains(userAgent, "apple") {
+		os_idx = 5
+	} else if strings.Contains(userAgent, "android") {
+		os_idx = 4
+	} else if strings.Contains(userAgent, "linux") {
+		os_idx = 3
+	}
+	// Client
+	client_idx = 0
+	if strings.Contains(userAgent, "anyconnect") {
+		client_idx = 1
+	} else if strings.Contains(userAgent, "openconnect") {
+		client_idx = 2
+	} else if strings.Contains(userAgent, "anylink") {
+		client_idx = 3
+	}
+	// Version
+	uaSlice := strings.Split(userAgent, " ")
+	ver = uaSlice[len(uaSlice)-1]
+	if ver[0] == 'v' {
+		ver = ver[1:]
+	}
+	if !regexp.MustCompile(`^(\d+\.?)+$`).MatchString(ver) {
+		ver = ""
+	}
+	return
+}
+
+// 清除用户操作日志
+func (ua *UserActLogProcess) ClearUserActLog(ts string) (int64, error) {
+	affected, err := xdb.Where("created_at < '" + ts + "'").Delete(&UserActLog{})
+	return affected, err
+}
+
+// 后台筛选用户操作日志
+func (ua *UserActLogProcess) GetSession(values url.Values) *xorm.Session {
+	session := xdb.Where("1=1")
+	if values.Get("username") != "" {
+		session.And("username = ?", values.Get("username"))
+	}
+	if values.Get("sdate") != "" {
+		session.And("created_at >= ?", values.Get("sdate")+" 00:00:00'")
+	}
+	if values.Get("edate") != "" {
+		session.And("created_at <= ?", values.Get("edate")+" 23:59:59'")
+	}
+	if values.Get("status") != "" {
+		session.And("status = ?", cast.ToUint8(values.Get("status"))-1)
+	}
+	if values.Get("os") != "" {
+		session.And("os = ?", cast.ToUint8(values.Get("os"))-1)
+	}
+	if values.Get("sort") == "1" {
+		session.OrderBy("id desc")
+	} else {
+		session.OrderBy("id asc")
+	}
+	return session
+}
+
+// 截取字符串
+func substr(s string, pos, length int) string {
+	runes := []rune(s)
+	l := pos + length
+	if l > len(runes) {
+		l = len(runes)
+	}
+	return string(runes[pos:l])
+}

+ 82 - 0
anylink/server/dbdata/user_act_log_test.go

@@ -0,0 +1,82 @@
+package dbdata
+
+import "testing"
+
+func TestParseUserAgent(t *testing.T) {
+	type args struct {
+		userAgent string
+	}
+	type res struct {
+		os_idx     uint8
+		client_idx uint8
+		ver        string
+	}
+	tests := []struct {
+		name string
+		args args
+		want res
+	}{
+		{
+			name: "mac os 1",
+			args: args{userAgent: "cisco anyconnect vpn agent for mac os x 4.10.05085"},
+			want: res{os_idx: 2, client_idx: 1, ver: "4.10.05085"},
+		},
+		{
+			name: "mac os 2",
+			args: args{userAgent: "anyconnect darwin_i386 4.10.05085"},
+			want: res{os_idx: 2, client_idx: 1, ver: "4.10.05085"},
+		},
+		{
+			name: "windows",
+			args: args{userAgent: "cisco anyconnect vpn agent for windows 4.8.02042"},
+			want: res{os_idx: 1, client_idx: 1, ver: "4.8.02042"},
+		},
+		{
+			name: "iPad",
+			args: args{userAgent: "anyconnect applesslvpn_darwin_arm (ipad) 4.10.04060"},
+			want: res{os_idx: 5, client_idx: 1, ver: "4.10.04060"},
+		},
+		{
+			name: "iPhone",
+			args: args{userAgent: "cisco anyconnect vpn agent for apple iphone 4.10.04060"},
+			want: res{os_idx: 5, client_idx: 1, ver: "4.10.04060"},
+		},
+		{
+			name: "android",
+			args: args{userAgent: "anyconnect android 4.10.05096"},
+			want: res{os_idx: 4, client_idx: 1, ver: "4.10.05096"},
+		},
+		{
+			name: "linux",
+			args: args{userAgent: "cisco anyconnect vpn agent for linux v7.08"},
+			want: res{os_idx: 3, client_idx: 1, ver: "7.08"},
+		},
+		{
+			name: "openconnect",
+			args: args{userAgent: "openconnect-gui 1.5.3 v7.08"},
+			want: res{os_idx: 0, client_idx: 2, ver: "7.08"},
+		},
+		{
+			name: "unknown",
+			args: args{userAgent: "unknown 1.4.3 aabcd"},
+			want: res{os_idx: 0, client_idx: 0, ver: ""},
+		},
+		{
+			name: "unknown 2",
+			args: args{userAgent: ""},
+			want: res{os_idx: 0, client_idx: 0, ver: ""},
+		},
+		{
+			name: "anylink",
+			args: args{userAgent: "anylink vpn agent for linux v1.0"},
+			want: res{os_idx: 3, client_idx: 3, ver: "1.0"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if os_idx, client_idx, ver := UserActLogIns.ParseUserAgent(tt.args.userAgent); os_idx != tt.want.os_idx || client_idx != tt.want.client_idx || ver != tt.want.ver {
+				t.Errorf("ParseUserAgent() = %v, %v, %v, want %v, %v, %v", os_idx, client_idx, ver, tt.want.os_idx, tt.want.client_idx, tt.want.ver)
+			}
+		})
+	}
+}

+ 8 - 7
anylink/server/dbdata/user_test.go

@@ -70,13 +70,14 @@ func TestCheckUser(t *testing.T) {
 	authData = map[string]interface{}{
 		"type": "ldap",
 		"ldap": map[string]interface{}{
-			"addr":        "192.168.8.12:389",
-			"tls":         true,
-			"bind_name":   "[email protected]",
-			"bind_pwd":    "afdbfdsafds",
-			"base_dn":     "dc=abc,dc=com",
-			"search_attr": "sAMAccountName",
-			"member_of":   "cn=vpn,cn=user,dc=abc,dc=com",
+			"addr":         "192.168.8.12:389",
+			"tls":          true,
+			"bind_name":    "[email protected]",
+			"bind_pwd":     "afdbfdsafds",
+			"base_dn":      "dc=abc,dc=com",
+			"object_class": "person",
+			"search_attr":  "sAMAccountName",
+			"member_of":    "cn=vpn,cn=user,dc=abc,dc=com",
 		},
 	}
 	g3 := Group{Name: group3, Status: 1, ClientDns: dns, RouteInclude: route, Auth: authData}

+ 48 - 10
anylink/server/dbdata/userauth_ldap.go

@@ -8,19 +8,21 @@ import (
 	"net"
 	"reflect"
 	"regexp"
+	"strconv"
 	"time"
 
 	"github.com/go-ldap/ldap"
 )
 
 type AuthLdap struct {
-	Addr       string `json:"addr"`
-	Tls        bool   `json:"tls"`
-	BindName   string `json:"bind_name"`
-	BindPwd    string `json:"bind_pwd"`
-	BaseDn     string `json:"base_dn"`
-	SearchAttr string `json:"search_attr"`
-	MemberOf   string `json:"member_of"`
+	Addr        string `json:"addr"`
+	Tls         bool   `json:"tls"`
+	BindName    string `json:"bind_name"`
+	BindPwd     string `json:"bind_pwd"`
+	BaseDn      string `json:"base_dn"`
+	ObjectClass string `json:"object_class"`
+	SearchAttr  string `json:"search_attr"`
+	MemberOf    string `json:"member_of"`
 }
 
 func init() {
@@ -39,7 +41,7 @@ func (auth AuthLdap) checkData(authData map[string]interface{}) error {
 		return errors.New("LDAP的服务器地址(含端口)填写有误")
 	}
 	if auth.BindName == "" {
-		return errors.New("LDAP的管理员账号不能为空")
+		return errors.New("LDAP的管理员 DN不能为空")
 	}
 	if auth.BindPwd == "" {
 		return errors.New("LDAP的管理员密码不能为空")
@@ -47,6 +49,9 @@ func (auth AuthLdap) checkData(authData map[string]interface{}) error {
 	if auth.BaseDn == "" || !ValidateDN(auth.BaseDn) {
 		return errors.New("LDAP的Base DN填写有误")
 	}
+	if auth.ObjectClass == "" {
+		return errors.New("LDAP的用户对象类填写有误")
+	}
 	if auth.SearchAttr == "" {
 		return errors.New("LDAP的用户唯一ID不能为空")
 	}
@@ -93,9 +98,12 @@ func (auth AuthLdap) checkUser(name, pwd string, g *Group) error {
 	}
 	err = l.Bind(auth.BindName, auth.BindPwd)
 	if err != nil {
-		return fmt.Errorf("%s LDAP 管理员账号或密码填写有误 %s", name, err.Error())
+		return fmt.Errorf("%s LDAP 管理员 DN或密码填写有误 %s", name, err.Error())
+	}
+	if auth.ObjectClass == "" {
+		auth.ObjectClass = "person"
 	}
-	filterAttr := "(objectClass=person)"
+	filterAttr := "(objectClass=" + auth.ObjectClass + ")"
 	filterAttr += "(" + auth.SearchAttr + "=" + name + ")"
 	if auth.MemberOf != "" {
 		filterAttr += "(memberOf:=" + auth.MemberOf + ")"
@@ -117,6 +125,10 @@ func (auth AuthLdap) checkUser(name, pwd string, g *Group) error {
 		}
 		return fmt.Errorf("LDAP发现 %s 用户,存在多个账号", name)
 	}
+	err = parseEntries(sr)
+	if err != nil {
+		return fmt.Errorf("LDAP %s 用户 %s", name, err.Error())
+	}
 	userDN := sr.Entries[0].DN
 	err = l.Bind(userDN, pwd)
 	if err != nil {
@@ -125,6 +137,32 @@ func (auth AuthLdap) checkUser(name, pwd string, g *Group) error {
 	return nil
 }
 
+func parseEntries(sr *ldap.SearchResult) error {
+	for _, attr := range sr.Entries[0].Attributes {
+		switch attr.Name {
+		case "shadowExpire":
+			// -1 启用, 1 停用, >1 从1970-01-01至到期日的天数
+			val, _ := strconv.ParseInt(attr.Values[0], 10, 64)
+			if val == -1 {
+				return nil
+			}
+			if val == 1 {
+				return fmt.Errorf("账号已停用")
+			}
+			if val > 1 {
+				expireTime := time.Unix(val*86400, 0)
+				t := time.Date(expireTime.Year(), expireTime.Month(), expireTime.Day(), 23, 59, 59, 0, time.Local)
+				if t.Before(time.Now()) {
+					return fmt.Errorf("账号已过期(过期日期: %s)", t.Format("2006-01-02"))
+				}
+				return nil
+			}
+			return fmt.Errorf("账号shadowExpire值异常: %d", val)
+		}
+	}
+	return nil
+}
+
 func ValidateDomainPort(addr string) bool {
 	re := regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+[A-Za-z]{2,18}\:([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$`)
 	return re.MatchString(addr)

+ 53 - 24
anylink/server/go.mod

@@ -1,75 +1,104 @@
 module github.com/bjdgyc/anylink
 
-go 1.18
+go 1.19
 
 require (
 	github.com/arl/statsviz v0.5.1
+	github.com/deckarep/golang-set v1.8.0
+	github.com/go-acme/lego/v4 v4.10.2
 	github.com/go-co-op/gocron v1.17.0
 	github.com/go-ldap/ldap v3.0.3+incompatible
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570
-	github.com/golang-jwt/jwt/v4 v4.0.0
+	github.com/golang-jwt/jwt/v4 v4.2.0
 	github.com/google/gopacket v1.1.19
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
 	github.com/ivpusic/grpool v1.0.0
+	github.com/lanrenwo/lzsgo v0.0.2
 	github.com/lib/pq v1.10.2
-	github.com/mattn/go-sqlite3 v1.14.8
+	github.com/mattn/go-sqlite3 v1.14.9
 	github.com/orcaman/concurrent-map v1.0.0
-	github.com/pion/dtls/v2 v2.1.5
+	github.com/pion/dtls/v2 v2.2.6
 	github.com/pion/logging v0.2.2
+	github.com/pires/go-proxyproto v0.6.2
 	github.com/shirou/gopsutil v3.21.7+incompatible
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
 	github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
+	github.com/spf13/cast v1.3.1
 	github.com/spf13/cobra v1.2.1
 	github.com/spf13/viper v1.8.1
-	github.com/stretchr/testify v1.8.0
+	github.com/stretchr/testify v1.8.1
 	github.com/xhit/go-simple-mail/v2 v2.10.0
 	github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
+	github.com/xuri/excelize/v2 v2.6.1
 	go.uber.org/atomic v1.10.0
-	golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
-	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
-	golang.org/x/text v0.3.7
-	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
+	golang.org/x/crypto v0.5.0
+	golang.org/x/net v0.7.0
+	golang.org/x/text v0.7.0
+	golang.org/x/time v0.3.0
 	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
-	xorm.io/xorm v1.2.2
+	xorm.io/xorm v1.3.2
+)
+
+require (
+	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.0 // indirect
+	github.com/cloudflare/cloudflare-go v0.49.0 // indirect
+	github.com/felixge/httpsnoop v1.0.1 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/kr/text v0.2.0 // indirect
+	github.com/miekg/dns v1.1.50 // indirect
+	github.com/pion/transport/v2 v2.0.2 // indirect
+	github.com/pion/udp/v2 v2.0.1 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/tools v0.1.12 // indirect
 )
 
 require (
 	github.com/StackExchange/wmi v1.2.1 // indirect
+	github.com/coreos/go-iptables v0.6.0
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/felixge/httpsnoop v1.0.1 // indirect
-	github.com/fsnotify/fsnotify v1.4.9 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
-	github.com/goccy/go-json v0.7.4 // indirect
-	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
+	github.com/goccy/go-json v0.8.1 // indirect
+	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gorilla/websocket v1.4.2 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
-	github.com/json-iterator/go v1.1.11 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
-	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.1 // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/pelletier/go-toml v1.9.3 // indirect
-	github.com/pion/transport v0.13.0 // indirect
-	github.com/pion/udp v0.1.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.3 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
-	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/tklauser/go-sysconf v0.3.7 // indirect
 	github.com/tklauser/numcpus v0.2.3 // indirect
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
+	github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
+	github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
+	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
+	golang.org/x/sys v0.5.0 // indirect
 	gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
-	gopkg.in/ini.v1 v1.62.0 // indirect
+	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	xorm.io/builder v0.3.9 // indirect
+	xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect
 )

+ 228 - 59
anylink/server/go.sum

@@ -39,6 +39,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
 gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
+gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -53,6 +54,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 h1:J45/QHgrzUdqe/Vco/Vxk0wRvdS2nKUxmf/zLgvfass=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -72,6 +75,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
+github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -79,12 +84,16 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/cloudflare-go v0.49.0 h1:KqJYk/YQ5ZhmyYz1oa4kGDskfF1gVuZfqesaJ/XDLto=
+github.com/cloudflare/cloudflare-go v0.49.0/go.mod h1:h0QgcIZ3qEXwFiwfBO8sQxjVdYsLX+PfD7NFEnANaKg=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
+github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -95,9 +104,12 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
+github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
 github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -115,19 +127,25 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-acme/lego/v4 v4.10.2 h1:5eW3qmda5v/LP21v1Hj70edKY1jeFZQwO617tdkwp6Q=
+github.com/go-acme/lego/v4 v4.10.2/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U=
 github.com/go-co-op/gocron v1.17.0 h1:IixLXsti+Qo0wMvmn6Kmjp2csk2ykpkcL+EmHmST18w=
 github.com/go-co-op/gocron v1.17.0/go.mod h1:IpDBSaJOVfFw7hXZuTag3SCSkqazXBBUkbQ1m1aesBs=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@@ -144,8 +162,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570 h1:n4E8KiBgNvYdtjgJbAqKov2IFv7tDkULV/2Ld3wj5Hg=
 github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
-github.com/goccy/go-json v0.7.4 h1:B44qRUFwz/vxPKPISQ1KhvzRi9kZ28RAf6YtjriBZ5k=
-github.com/goccy/go-json v0.7.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
+github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -154,8 +172,9 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
-github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
+github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -187,8 +206,10 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -202,6 +223,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
 github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
@@ -222,9 +246,10 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
@@ -246,9 +271,15 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
+github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -322,15 +353,20 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
 github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -342,12 +378,15 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lanrenwo/lzsgo v0.0.2 h1:FA30LAaJFYLoaM17b+H32gA+5H+abjoomNLSA9HCbrI=
+github.com/lanrenwo/lzsgo v0.0.2/go.mod h1:oxDZy2vgi6VBGIdvL80ayRMtIyXV+TbjavVuINXZY2k=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -363,20 +402,22 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
-github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
+github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -384,14 +425,18 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
 github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
@@ -427,17 +472,20 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
-github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
+github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
+github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
-github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
-github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
-github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
+github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
+github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
+github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
+github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
+github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
+github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -462,11 +510,17 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
+github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@@ -486,9 +540,7 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=
@@ -517,6 +569,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -524,12 +577,17 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ=
 github.com/tklauser/go-sysconf v0.3.7 h1:HT7h4+536gjqeq1ZIJPgOl1rg1XFatQGVZWp7Py53eg=
 github.com/tklauser/go-sysconf v0.3.7/go.mod h1:JZIdXh4RmBvZDBZ41ld2bGxRV3n4daiiqA3skYhAoQ4=
 github.com/tklauser/numcpus v0.2.3 h1:nQ0QYpiritP6ViFhrKYsiv6VVxOpum2Gks5GhnJbS/8=
@@ -542,11 +600,18 @@ github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkug
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4=
 github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8=
+github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
+github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.6.1 h1:ICBdtw803rmhLN3zfvyEGH3cwSmZv+kde7LhTDT659k=
+github.com/xuri/excelize/v2 v2.6.1/go.mod h1:tL+0m6DNwSXj/sILHbQTYsLi9IF4TW59H2EF3Yrx1AU=
+github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
+github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -596,8 +661,10 @@ golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -610,6 +677,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
+golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -632,8 +701,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -671,16 +741,18 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -703,8 +775,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -754,7 +827,6 @@ golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -765,11 +837,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 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.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+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=
@@ -778,14 +859,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -844,14 +927,15 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -967,16 +1051,18 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
-gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
+gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@@ -990,6 +1076,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1004,37 +1091,119 @@ layeh.com/radius v0.0.0-20210819152912-ad72663a72ab h1:05KeMI4s7jEdIfHb7QCjUr5X2
 layeh.com/radius v0.0.0-20210819152912-ad72663a72ab/go.mod h1:pFWM9De99EY9TPVyHIyA56QmoRViVck/x41WFkUlc9A=
 lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
 lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-modernc.org/cc/v3 v3.33.6 h1:r63dgSzVzRxUpAJFPQWHy1QeZeY1ydNENUDaBx1GqYc=
 modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
-modernc.org/ccgo/v3 v3.9.5 h1:dEuUSf8WN51rDkprFuAqjfchKEzN0WttP/Py3enBwjk=
+modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
+modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
 modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
+modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
+modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
+modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
+modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
+modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
+modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
+modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
+modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
+modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
+modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
+modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
+modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
+modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
+modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
+modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
+modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
+modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
+modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
+modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
+modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
+modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
+modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
+modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
+modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
+modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
+modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
+modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
+modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
+modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
+modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
+modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
+modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
+modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
+modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
+modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
+modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
 modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
-modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
 modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
-modernc.org/libc v1.9.11 h1:QUxZMs48Ahg2F7SN41aERvMfGLY2HU/ADnB9DC4Yts8=
 modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
+modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
+modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
+modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
+modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
+modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
+modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
+modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
+modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
+modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
+modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
+modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
+modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
+modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
+modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
+modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
+modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
+modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
+modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
+modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
+modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
+modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
+modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
+modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
+modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
+modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
+modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
+modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
+modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
+modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
+modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
+modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
+modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
+modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
+modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
 modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/mathutil v1.4.0 h1:GCjoRaBew8ECCKINQA2nYjzvufFW9YiEuuB+rQ9bn2E=
 modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM=
+modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
+modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
+modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
+modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
 modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
 modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sqlite v1.11.2 h1:ShWQpeD3ag/bmx6TqidBlIWonWmQaSQKls3aenCbt+w=
-modernc.org/sqlite v1.11.2/go.mod h1:+mhs/P1ONd+6G7hcAs6irwDi/bjTQ7nLW6LHRBsEa3A=
+modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
+modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
 modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
 modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
-modernc.org/tcl v1.5.5/go.mod h1:ADkaTUuwukkrlhqwERyq0SM8OvyXo7+TjFz7yAF56EI=
+modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
 modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
 modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
+modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
-xorm.io/builder v0.3.9 h1:Sd65/LdWyO7LR8+Cbd+e7mm3sK/7U9k0jS3999IDHMc=
-xorm.io/builder v0.3.9/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
-xorm.io/xorm v1.2.2 h1:FFBOEvJ++8fYBA9cywf2sxDVmFktl1SpJzTAG1ab06Y=
-xorm.io/xorm v1.2.2/go.mod h1:fTG8tSjk6O1BYxwuohZUK+S1glnRycsCF05L1qQyEU0=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
+xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=

+ 7 - 3
anylink/server/handler/dtls.go

@@ -66,9 +66,13 @@ func startDtls() {
 		go func() {
 			// time.Sleep(1 * time.Second)
 			cc := conn.(*dtls.Conn)
-			sessid := hex.EncodeToString(cc.ConnectionState().SessionID)
-			sess := sessdata.Dtls2Sess(sessid)
-			LinkDtls(conn, sess.CSess)
+			did := hex.EncodeToString(cc.ConnectionState().SessionID)
+			cSess := sessdata.Dtls2CSess(did)
+			if cSess == nil {
+				conn.Close()
+				return
+			}
+			LinkDtls(conn, cSess)
 		}()
 	}
 }

+ 38 - 8
anylink/server/handler/link_auth.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"net/http/httputil"
 	"strings"
 	"text/template"
 
@@ -18,11 +19,16 @@ import (
 var profileHash = ""
 
 func LinkAuth(w http.ResponseWriter, r *http.Request) {
+	// TODO 调试信息输出
+	if base.GetLogLevel() == base.LogLevelTrace {
+		hd, _ := httputil.DumpRequest(r, true)
+		base.Trace("LinkAuth: ", string(hd))
+	}
 	// 判断anyconnect客户端
 	userAgent := strings.ToLower(r.UserAgent())
 	xAggregateAuth := r.Header.Get("X-Aggregate-Auth")
 	xTranscendVersion := r.Header.Get("X-Transcend-Version")
-	if !((strings.Contains(userAgent, "anyconnect") || strings.Contains(userAgent, "openconnect")) &&
+	if !((strings.Contains(userAgent, "anyconnect") || strings.Contains(userAgent, "openconnect") || strings.Contains(userAgent, "anylink")) &&
 		xAggregateAuth == "1" && xTranscendVersion == "1") {
 		w.WriteHeader(http.StatusForbidden)
 		fmt.Fprintf(w, "error request")
@@ -43,7 +49,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// fmt.Printf("%+v \n", cr)
-
 	setCommonHeader(w)
 	if cr.Type == "logout" {
 		// 退出删除session信息
@@ -56,7 +61,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
 
 	if cr.Type == "init" {
 		w.WriteHeader(http.StatusOK)
-		data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames()}
+		data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNamesNormal()}
 		tplRequest(tpl_request, w, data)
 		return
 	}
@@ -66,16 +71,32 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
-
+	// 用户活动日志
+	ua := dbdata.UserActLog{
+		Username:        cr.Auth.Username,
+		GroupName:       cr.GroupSelect,
+		RemoteAddr:      r.RemoteAddr,
+		Status:          dbdata.UserAuthSuccess,
+		DeviceType:      cr.DeviceId.DeviceType,
+		PlatformVersion: cr.DeviceId.PlatformVersion,
+	}
 	// TODO 用户密码校验
 	err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
 	if err != nil {
 		base.Warn(err)
+		ua.Info = err.Error()
+		ua.Status = dbdata.UserAuthFail
+		dbdata.UserActLogIns.Add(ua, userAgent)
+
 		w.WriteHeader(http.StatusOK)
-		data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames(), Error: "用户名或密码错误"}
+		data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNamesNormal(), Error: "用户名或密码错误"}
+		if base.Cfg.DisplayError {
+			data.Error = err.Error()
+		}
 		tplRequest(tpl_request, w, data)
 		return
 	}
+	dbdata.UserActLogIns.Add(ua, userAgent)
 	// if !ok {
 	//	w.WriteHeader(http.StatusOK)
 	//	data := RequestData{Group: cr.GroupSelect, Groups: base.Cfg.UserGroups, Error: "请先激活用户"}
@@ -87,29 +108,38 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
 	sess := sessdata.NewSession("")
 	sess.Username = cr.Auth.Username
 	sess.Group = cr.GroupSelect
-	sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
+	oriMac := cr.MacAddressList.MacAddress
 	sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal
+	sess.UserAgent = userAgent
+	sess.DeviceType = ua.DeviceType
+	sess.PlatformVersion = ua.PlatformVersion
+	sess.RemoteAddr = r.RemoteAddr
 	// 获取客户端mac地址
-	macHw, err := net.ParseMAC(sess.MacAddr)
+	sess.UniqueMac = true
+	macHw, err := net.ParseMAC(oriMac)
 	if err != nil {
 		var sum [16]byte
 		if sess.UniqueIdGlobal != "" {
 			sum = md5.Sum([]byte(sess.UniqueIdGlobal))
 		} else {
 			sum = md5.Sum([]byte(sess.Token))
+			sess.UniqueMac = false
 		}
 		macHw = sum[0:5] // 5个byte
 		macHw = append([]byte{0x02}, macHw...)
 		sess.MacAddr = macHw.String()
 	}
 	sess.MacHw = macHw
+	// 统一macAddr的格式
+	sess.MacAddr = macHw.String()
+
 	other := &dbdata.SettingOther{}
 	_ = dbdata.SettingGet(other)
 	rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
 		Banner: other.Banner, ProfileHash: profileHash}
 	w.WriteHeader(http.StatusOK)
 	tplRequest(tpl_complete, w, rd)
-	base.Debug("login", cr.Auth.Username)
+	base.Debug("login", cr.Auth.Username, userAgent)
 }
 
 const (

+ 1 - 0
anylink/server/handler/link_base.go

@@ -44,6 +44,7 @@ type macAddressList struct {
 
 func setCommonHeader(w http.ResponseWriter) {
 	// Content-Length Date 默认已经存在
+	w.Header().Set("Server", "AnyLink")
 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 	w.Header().Set("Cache-Control", "no-store,no-cache")
 	w.Header().Set("Pragma", "no-cache")

+ 50 - 18
anylink/server/handler/link_cstp.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
 	"github.com/bjdgyc/anylink/pkg/utils"
 	"github.com/bjdgyc/anylink/sessdata"
 )
@@ -14,7 +15,7 @@ import (
 func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSession) {
 	base.Debug("LinkCstp connect ip:", cSess.IpAddr, "user:", cSess.Username, "rip:", conn.RemoteAddr())
 	defer func() {
-		base.Debug("LinkCstp return", cSess.IpAddr)
+		base.Debug("LinkCstp return", cSess.Username, cSess.IpAddr)
 		_ = conn.Close()
 		cSess.Close()
 	}()
@@ -33,14 +34,14 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
 		// 设置超时限制
 		err = conn.SetReadDeadline(utils.NowSec().Add(dead))
 		if err != nil {
-			base.Error("SetDeadline: ", err)
+			base.Error("SetDeadline: ", cSess.Username, err)
 			return
 		}
 		// hdata := make([]byte, BufferSize)
 		pl := getPayload()
 		n, err = bufRW.Read(pl.Data)
 		if err != nil {
-			base.Error("read hdata: ", err)
+			base.Error("read hdata: ", cSess.Username, err)
 			return
 		}
 
@@ -55,7 +56,8 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
 			// do nothing
 			// base.Debug("recv keepalive", cSess.IpAddr)
 		case 0x05: // DISCONNECT
-			base.Debug("DISCONNECT", cSess.IpAddr)
+			cSess.UserLogoutCode = dbdata.UserLogoutClient
+			base.Debug("DISCONNECT", cSess.Username, cSess.IpAddr)
 			return
 		case 0x03: // DPD-REQ
 			// base.Debug("recv DPD-REQ", cSess.IpAddr)
@@ -64,13 +66,28 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
 				return
 			}
 		case 0x04:
-			// log.Println("recv DPD-RESP")
+		// log.Println("recv DPD-RESP")
+		case 0x08: // decompress
+			if cSess.CstpPickCmp == nil {
+				continue
+			}
+			dst := getByteFull()
+			nn, err := cSess.CstpPickCmp.Uncompress(pl.Data[8:], *dst)
+			if err != nil {
+				putByte(dst)
+				base.Error("cstp decompress error", err, nn)
+				continue
+			}
+			binary.BigEndian.PutUint16(pl.Data[4:6], uint16(nn))
+			pl.Data = append(pl.Data[:8], (*dst)[:nn]...)
+			putByte(dst)
+			fallthrough
 		case 0x00: // DATA
 			// 获取数据长度
 			dataLen = binary.BigEndian.Uint16(pl.Data[4:6]) // 4,5
 			// 修复 cstp 数据长度溢出报错
 			if 8+dataLen > BufferSize {
-				base.Error("recv error dataLen", dataLen)
+				base.Error("recv error dataLen", cSess.Username, dataLen)
 				continue
 			}
 			// 去除数据头
@@ -87,7 +104,7 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
 
 func cstpWrite(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSession) {
 	defer func() {
-		base.Debug("cstpWrite return", cSess.IpAddr)
+		base.Debug("cstpWrite return", cSess.Username, cSess.IpAddr)
 		_ = conn.Close()
 		cSess.Close()
 	}()
@@ -110,16 +127,31 @@ func cstpWrite(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessi
 		}
 
 		if pl.PType == 0x00 {
-			// 获取数据长度
-			l := len(pl.Data)
-			// 先扩容 +8
-			pl.Data = pl.Data[:l+8]
-			// 数据后移
-			copy(pl.Data[8:], pl.Data)
-			// 添加头信息
-			copy(pl.Data[:8], plHeader)
-			// 更新头长度
-			binary.BigEndian.PutUint16(pl.Data[4:6], uint16(l))
+			isCompress := false
+			if cSess.CstpPickCmp != nil && len(pl.Data) > base.Cfg.NoCompressLimit {
+				dst := getByteFull()
+				size, err := cSess.CstpPickCmp.Compress(pl.Data, (*dst)[8:])
+				if err == nil && size < len(pl.Data) {
+					copy((*dst)[:8], plHeader)
+					binary.BigEndian.PutUint16((*dst)[4:6], uint16(size))
+					(*dst)[6] = 0x08
+					pl.Data = append(pl.Data[:0], (*dst)[:size+8]...)
+					isCompress = true
+				}
+				putByte(dst)
+			}
+			if !isCompress {
+				// 获取数据长度
+				l := len(pl.Data)
+				// 先扩容 +8
+				pl.Data = pl.Data[:l+8]
+				// 数据后移
+				copy(pl.Data[8:], pl.Data)
+				// 添加头信息
+				copy(pl.Data[:8], plHeader)
+				// 更新头长度
+				binary.BigEndian.PutUint16(pl.Data[4:6], uint16(l))
+			}
 		} else {
 			pl.Data = append(pl.Data[:0], plHeader...)
 			// 设置头类型
@@ -128,7 +160,7 @@ func cstpWrite(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessi
 
 		n, err = conn.Write(pl.Data)
 		if err != nil {
-			base.Error("write err", err)
+			base.Error("write err", cSess.Username, err)
 			return
 		}
 

+ 46 - 15
anylink/server/handler/link_dtls.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/bjdgyc/anylink/base"
+	"github.com/bjdgyc/anylink/dbdata"
 	"github.com/bjdgyc/anylink/pkg/utils"
 	"github.com/bjdgyc/anylink/sessdata"
 )
@@ -19,7 +20,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
 	}
 
 	defer func() {
-		base.Debug("LinkDtls return", cSess.IpAddr)
+		base.Debug("LinkDtls return", cSess.Username, cSess.IpAddr)
 		_ = conn.Close()
 		dSess.Close()
 	}()
@@ -35,14 +36,14 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
 	for {
 		err = conn.SetReadDeadline(utils.NowSec().Add(dead))
 		if err != nil {
-			base.Error("SetDeadline: ", err)
+			base.Error("SetDeadline: ", cSess.Username, err)
 			return
 		}
 
 		pl := getPayload()
 		n, err = conn.Read(pl.Data)
 		if err != nil {
-			base.Error("read hdata: ", err)
+			base.Error("read hdata: ", cSess.Username, err)
 			return
 		}
 
@@ -57,7 +58,8 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
 			// do nothing
 			// base.Debug("recv keepalive", cSess.IpAddr)
 		case 0x05: // DISCONNECT
-			base.Debug("DISCONNECT DTLS", cSess.IpAddr)
+			cSess.UserLogoutCode = dbdata.UserLogoutClient
+			base.Debug("DISCONNECT DTLS", cSess.Username, cSess.IpAddr)
 			return
 		case 0x03: // DPD-REQ
 			// base.Debug("recv DPD-REQ", cSess.IpAddr)
@@ -66,7 +68,22 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
 				return
 			}
 		case 0x04:
-			// base.Debug("recv DPD-RESP", cSess.IpAddr)
+		// base.Debug("recv DPD-RESP", cSess.IpAddr)
+		case 0x08: // decompress
+			if cSess.DtlsPickCmp == nil {
+				continue
+			}
+			dst := getByteFull()
+			nn, err := cSess.DtlsPickCmp.Uncompress(pl.Data[1:], *dst)
+			if err != nil {
+				putByte(dst)
+				base.Error("dtls decompress error", err, n)
+				continue
+			}
+			pl.Data = append(pl.Data[:1], (*dst)[:nn]...)
+			putByte(dst)
+			n = nn + 1
+			fallthrough
 		case 0x00: // DATA
 			// 去除数据头
 			// copy(pl.Data, pl.Data[1:n])
@@ -83,7 +100,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
 
 func dtlsWrite(conn net.Conn, dSess *sessdata.DtlsSession, cSess *sessdata.ConnSession) {
 	defer func() {
-		base.Debug("dtlsWrite return", cSess.IpAddr)
+		base.Debug("dtlsWrite return", cSess.Username, cSess.IpAddr)
 		_ = conn.Close()
 		dSess.Close()
 	}()
@@ -106,21 +123,35 @@ func dtlsWrite(conn net.Conn, dSess *sessdata.DtlsSession, cSess *sessdata.ConnS
 
 		// header = []byte{payload.PType}
 		if pl.PType == 0x00 { // data
-			// 获取数据长度
-			l := len(pl.Data)
-			// 先扩容 +1
-			pl.Data = pl.Data[:l+1]
-			// 数据后移
-			copy(pl.Data[1:], pl.Data)
-			// 添加头信息
-			pl.Data[0] = pl.PType
+			isCompress := false
+			if cSess.DtlsPickCmp != nil && len(pl.Data) > base.Cfg.NoCompressLimit {
+				dst := getByteFull()
+				size, err := cSess.DtlsPickCmp.Compress(pl.Data, (*dst)[1:])
+				if err == nil && size < len(pl.Data) {
+					(*dst)[0] = 0x08
+					pl.Data = append(pl.Data[:0], (*dst)[:size+1]...)
+					isCompress = true
+				}
+				putByte(dst)
+			}
+			// 未压缩
+			if !isCompress {
+				// 获取数据长度
+				l := len(pl.Data)
+				// 先扩容 +1
+				pl.Data = pl.Data[:l+1]
+				// 数据后移
+				copy(pl.Data[1:], pl.Data)
+				// 添加头信息
+				pl.Data[0] = pl.PType
+			}
 		} else {
 			// 设置头类型
 			pl.Data = append(pl.Data[:0], pl.PType)
 		}
 		n, err := conn.Write(pl.Data)
 		if err != nil {
-			base.Error("write err", err)
+			base.Error("write err", cSess.Username, err)
 			return
 		}
 

+ 5 - 2
anylink/server/handler/link_home.go

@@ -13,6 +13,7 @@ func LinkHome(w http.ResponseWriter, r *http.Request) {
 	// fmt.Println(r.RemoteAddr)
 	// hu, _ := httputil.DumpRequest(r, true)
 	// fmt.Println("DumpHome: ", string(hu))
+	w.Header().Set("Server", "AnyLinkOpenSource")
 	connection := strings.ToLower(r.Header.Get("Connection"))
 	userAgent := strings.ToLower(r.UserAgent())
 	if connection == "close" && (strings.Contains(userAgent, "anyconnect") || strings.Contains(userAgent, "openconnect")) {
@@ -21,11 +22,13 @@ func LinkHome(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	index := &dbdata.SettingOther{}
-	dbdata.SettingGet(index)
+	if err := dbdata.SettingGet(index); err != nil {
+		return
+	}
 	w.WriteHeader(http.StatusOK)
 	if index.Homeindex == "" {
 		index.Homeindex = "AnyLink 是一个企业级远程办公 SSL VPN 软件,可以支持多人同时在线使用。"
-	}	
+	}
 	fmt.Fprintln(w, index.Homeindex)
 }
 

+ 1 - 1
anylink/server/handler/link_tap.go

@@ -175,7 +175,7 @@ func allTapWrite(ifce LinkDriver, cSess *sessdata.ConnSession) {
 			return
 		}
 
-		putPayload(pl)
+		putPayloadInBefore(cSess, pl)
 	}
 }
 

+ 24 - 1
anylink/server/handler/link_tun.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/bjdgyc/anylink/base"
 	"github.com/bjdgyc/anylink/sessdata"
+	"github.com/coreos/go-iptables/iptables"
 	"github.com/songgao/water"
 )
 
@@ -26,6 +27,28 @@ func checkTun() {
 	if err != nil {
 		base.Fatal("testTun err: ", err)
 	}
+	//开启服务器转发
+	if err := execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"}); err != nil {
+		base.Error(err)
+	}
+	if base.Cfg.IptablesNat {
+		//添加NAT转发规则
+		ipt, err := iptables.New()
+		if err != nil {
+			base.Error(err)
+			return
+		}
+		natRule := []string{"-s", base.Cfg.Ipv4CIDR, "-o", base.Cfg.Ipv4Master, "-j", "MASQUERADE"}
+		forwardRule := []string{"-j", "ACCEPT"}
+		if natExists, _ := ipt.Exists("nat", "POSTROUTING", natRule...); !natExists {
+			ipt.Insert("nat", "POSTROUTING", 1, natRule...)
+		}
+		if forwardExists, _ := ipt.Exists("filter", "FORWARD", forwardRule...); !forwardExists {
+			ipt.Insert("filter", "FORWARD", 1, forwardRule...)
+		}
+		base.Info(ipt.List("nat", "POSTROUTING"))
+		base.Info(ipt.List("filter", "FORWARD"))
+	}
 }
 
 // 创建tun网卡
@@ -85,7 +108,7 @@ func tunWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
 			return
 		}
 
-		putPayload(pl)
+		putPayloadInBefore(cSess, pl)
 	}
 }
 

+ 28 - 6
anylink/server/handler/link_tunnel.go

@@ -6,6 +6,7 @@ import (
 	"log"
 	"net"
 	"net/http"
+	"net/http/httputil"
 	"os"
 	"strings"
 	"text/template"
@@ -34,9 +35,10 @@ func HttpAddHeader(w http.ResponseWriter, key string, value string) {
 
 func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 	// TODO 调试信息输出
-	// hd, _ := httputil.DumpRequest(r, true)
-	// fmt.Println("DumpRequest: ", string(hd))
-	// fmt.Println("LinkTunnel", r.RemoteAddr)
+	if base.GetLogLevel() == base.LogLevelTrace {
+		hd, _ := httputil.DumpRequest(r, true)
+		base.Trace("LinkTunnel: ", string(hd))
+	}
 
 	// 判断session-token的值
 	cookie, err := r.Cookie("webvpn")
@@ -69,6 +71,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 	cSess.SetMtu(cstpMtu)
 	cSess.MasterSecret = masterSecret
 	cSess.RemoteAddr = r.RemoteAddr
+	cSess.UserAgent = strings.ToLower(r.UserAgent())
 	cSess.LocalIp = net.ParseIP(localIp)
 	cstpKeepalive := base.Cfg.CstpKeepalive
 	cstpDpd := base.Cfg.CstpDpd
@@ -89,6 +92,14 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 
 	base.Debug(cSess.IpAddr, cSess.MacHw, sess.Username, mobile)
 
+	// 压缩
+	if cmpName, ok := cSess.SetPickCmp("cstp", r.Header.Get("X-Cstp-Accept-Encoding")); ok {
+		HttpSetHeader(w, "X-CSTP-Content-Encoding", cmpName)
+	}
+	if cmpName, ok := cSess.SetPickCmp("dtls", r.Header.Get("X-Dtls-Accept-Encoding")); ok {
+		HttpSetHeader(w, "X-DTLS-Content-Encoding", cmpName)
+	}
+
 	// 返回客户端数据
 	HttpSetHeader(w, "Server", fmt.Sprintf("%s %s", base.APP_NAME, base.APP_VER))
 	HttpSetHeader(w, "X-CSTP-Version", "1")
@@ -125,7 +136,8 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 	for _, v := range cSess.Group.RouteExclude {
 		HttpAddHeader(w, "X-CSTP-Split-Exclude", v.IpMask)
 	}
-	HttpSetHeader(w, "X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.IpLease)) // ip地址租期
+
+	HttpSetHeader(w, "X-CSTP-Lease-Duration", "1209600") // ip地址租期
 	HttpSetHeader(w, "X-CSTP-Session-Timeout", "none")
 	HttpSetHeader(w, "X-CSTP-Session-Timeout-Alert-Interval", "60")
 	HttpSetHeader(w, "X-CSTP-Session-Timeout-Remaining", "none")
@@ -134,8 +146,10 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 	HttpSetHeader(w, "X-CSTP-Keep", "true")
 	HttpSetHeader(w, "X-CSTP-Tunnel-All-DNS", "false")
 
-	HttpSetHeader(w, "X-CSTP-Rekey-Time", "172800")
+	HttpSetHeader(w, "X-CSTP-Rekey-Time", "43200") // 172800
 	HttpSetHeader(w, "X-CSTP-Rekey-Method", "new-tunnel")
+	HttpSetHeader(w, "X-DTLS-Rekey-Time", "43200")
+	HttpSetHeader(w, "X-DTLS-Rekey-Method", "new-tunnel")
 
 	HttpSetHeader(w, "X-CSTP-DPD", fmt.Sprintf("%d", cstpDpd))
 	HttpSetHeader(w, "X-CSTP-Keepalive", fmt.Sprintf("%d", cstpKeepalive))
@@ -150,7 +164,6 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 	HttpSetHeader(w, "X-DTLS-Port", dtlsPort)
 	HttpSetHeader(w, "X-DTLS-DPD", fmt.Sprintf("%d", cstpDpd))
 	HttpSetHeader(w, "X-DTLS-Keepalive", fmt.Sprintf("%d", cstpKeepalive))
-	HttpSetHeader(w, "X-DTLS-Rekey-Time", "5400")
 	HttpSetHeader(w, "X-DTLS12-CipherSuite", "ECDHE-ECDSA-AES128-GCM-SHA256")
 
 	HttpSetHeader(w, "X-CSTP-License", "accept")
@@ -194,6 +207,15 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
 		base.Error(err)
 		return
 	}
+	dbdata.UserActLogIns.Add(dbdata.UserActLog{
+		Username:        sess.Username,
+		GroupName:       sess.Group,
+		IpAddr:          cSess.IpAddr.String(),
+		RemoteAddr:      cSess.RemoteAddr,
+		DeviceType:      sess.DeviceType,
+		PlatformVersion: sess.PlatformVersion,
+		Status:          dbdata.UserConnected,
+	}, cSess.UserAgent)
 
 	go LinkCstp(conn, bufRW, cSess)
 }

+ 9 - 5
anylink/server/handler/payload.go

@@ -15,11 +15,6 @@ func payloadIn(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool {
 			// 校验不通过直接丢弃
 			return false
 		}
-		if base.Cfg.AuditInterval >= 0 {
-			cSess.IpAuditPool.JobQueue <- func() {
-				logAudit(cSess, pl)
-			}
-		}
 	}
 
 	closed := false
@@ -32,6 +27,15 @@ func payloadIn(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool {
 	return closed
 }
 
+func putPayloadInBefore(cSess *sessdata.ConnSession, pl *sessdata.Payload) {
+	// 异步审计日志
+	if base.Cfg.AuditInterval >= 0 {
+		auditPayload.Add(cSess.Username, pl)
+		return
+	}
+	putPayload(pl)
+}
+
 func payloadOut(cSess *sessdata.ConnSession, pl *sessdata.Payload) bool {
 	dSess := cSess.GetDtlsSession()
 	if dSess == nil {

+ 90 - 96
anylink/server/handler/payload_access_audit.go

@@ -3,13 +3,13 @@ package handler
 import (
 	"crypto/md5"
 	"encoding/binary"
-	"encoding/hex"
 	"time"
 
 	"github.com/bjdgyc/anylink/base"
 	"github.com/bjdgyc/anylink/dbdata"
 	"github.com/bjdgyc/anylink/pkg/utils"
 	"github.com/bjdgyc/anylink/sessdata"
+	"github.com/ivpusic/grpool"
 	"github.com/songgao/water/waterutil"
 )
 
@@ -20,73 +20,92 @@ const (
 	acc_proto_http
 )
 
-// 保存批量的审计日志
+var (
+	auditPayload *AuditPayload
+	logBatch     *LogBatch
+)
+
+// 分析审计日志
+type AuditPayload struct {
+	Pool       *grpool.Pool
+	IpAuditMap utils.IMaps
+}
+
+// 保存审计日志
 type LogBatch struct {
-	Logs []dbdata.AccessAudit
+	Logs    []dbdata.AccessAudit
+	LogChan chan dbdata.AccessAudit
 }
 
-// 日志池
-type LogSink struct {
-	logChan        chan dbdata.AccessAudit
-	autoCommitChan chan *LogBatch // 超时通知
+// 异步写入pool
+func (p *AuditPayload) Add(userName string, pl *sessdata.Payload) {
+	select {
+	case p.Pool.JobQueue <- func() {
+		logAudit(userName, pl)
+	}:
+	default:
+		putPayload(pl)
+		base.Error("AccessAudit: AuditPayload channel is full")
+	}
 }
 
-var logAuditSink *LogSink
+// 数据落盘
+func (l *LogBatch) Write() {
+	if len(l.Logs) == 0 {
+		return
+	}
+	_ = dbdata.AddBatch(l.Logs)
+	l.Reset()
+}
 
-// 写入日志通道
-func logAuditWrite(aa dbdata.AccessAudit) {
-	logAuditSink.logChan <- aa
+// 清空数据
+func (l *LogBatch) Reset() {
+	l.Logs = []dbdata.AccessAudit{}
 }
 
-// 批量写入数据表
+// 开启批量写入数据功能
 func logAuditBatch() {
 	if base.Cfg.AuditInterval < 0 {
 		return
 	}
-	logAuditSink = &LogSink{
-		logChan:        make(chan dbdata.AccessAudit, 1000),
-		autoCommitChan: make(chan *LogBatch, 10),
+	auditPayload = &AuditPayload{
+		Pool:       grpool.NewPool(10, 10240),
+		IpAuditMap: utils.NewMap("cmap", 0),
+	}
+	logBatch = &LogBatch{
+		LogChan: make(chan dbdata.AccessAudit, 10240),
 	}
 	var (
-		limit        = 100 // 超过上限批量写入数据表
-		logAudit     dbdata.AccessAudit
-		logBatch     *LogBatch
-		commitTimer  *time.Timer // 超时自动提交
-		timeOutBatch *LogBatch
+		limit       = 100 // 超过上限批量写入数据表
+		outTime     = time.NewTimer(time.Second)
+		accessAudit = dbdata.AccessAudit{}
 	)
+
 	for {
+		// 重置超时 时间
+		outTime.Reset(time.Second * 1)
 		select {
-		case logAudit = <-logAuditSink.logChan:
-			if logBatch == nil {
-				logBatch = &LogBatch{}
-				commitTimer = time.AfterFunc(
-					1*time.Second, func(logBatch *LogBatch) func() {
-						return func() {
-							logAuditSink.autoCommitChan <- logBatch
-						}
-					}(logBatch),
-				)
-			}
-			logBatch.Logs = append(logBatch.Logs, logAudit)
+		case accessAudit = <-logBatch.LogChan:
+			logBatch.Logs = append(logBatch.Logs, accessAudit)
 			if len(logBatch.Logs) >= limit {
-				commitTimer.Stop()
-				_ = dbdata.AddBatch(logBatch.Logs)
-				logBatch = nil
-			}
-		case timeOutBatch = <-logAuditSink.autoCommitChan:
-			if timeOutBatch != logBatch {
-				continue
+				if !outTime.Stop() {
+					<-outTime.C
+				}
+				logBatch.Write()
 			}
-			if logBatch != nil {
-				_ = dbdata.AddBatch(logBatch.Logs)
-			}
-			logBatch = nil
+		case <-outTime.C:
+			logBatch.Write()
 		}
 	}
 }
 
 // 解析IP包的数据
-func logAudit(cSess *sessdata.ConnSession, pl *sessdata.Payload) {
+func logAudit(userName string, pl *sessdata.Payload) {
+	defer putPayload(pl)
+
+	if !(pl.LType == sessdata.LTypeIPData && pl.PType == 0x00) {
+		return
+	}
 	ipProto := waterutil.IPv4Protocol(pl.Data)
 	// 访问协议
 	var accessProto uint8
@@ -109,79 +128,48 @@ func logAudit(cSess *sessdata.ConnSession, pl *sessdata.Payload) {
 	copy(key[:16], ipSrc)
 	copy(key[16:32], ipDst)
 	binary.BigEndian.PutUint16(key[32:34], ipPort)
+	key[34] = byte(accessProto)
+	copy(key[35:51], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
 
 	info := ""
 	nu := utils.NowSec().Unix()
 	if ipProto == waterutil.TCP {
-		plData := waterutil.IPv4Payload(pl.Data)
-		if len(plData) < 14 {
+		tcpPlData := waterutil.IPv4Payload(pl.Data)
+		// 24 (ACK PSH)
+		if len(tcpPlData) < 14 || tcpPlData[13] != 24 {
 			return
 		}
-		flags := plData[13]
-		switch flags {
-		case flags & 0x20:
-			// URG
-			return
-		case flags & 0x14:
-			// RST ACK
-			return
-		case flags & 0x12:
-			// SYN ACK
-			return
-		case flags & 0x11:
-			// Client FIN
-			return
-		case flags & 0x10:
-			// ACK
-			return
-		case flags & 0x08:
-			// PSH
-			return
-		case flags & 0x04:
-			// RST
-			return
-		case flags & 0x02:
-			// SYN
-			return
-		case flags & 0x01:
-			// FIN
-			return
-		case flags & 0x18:
-			// PSH ACK
-			accessProto, info = onTCP(plData)
+		accessProto, info = onTCP(tcpPlData)
+		// HTTPS or HTTP
+		if accessProto != acc_proto_tcp {
+			// 提前存储只含ip数据的key, 避免即记录域名又记录一笔IP数据的记录
+			ipKey := make([]byte, 51)
+			copy(ipKey, key)
+			ipS := utils.BytesToString(ipKey)
+			auditPayload.IpAuditMap.Set(ipS, nu)
+
+			key[34] = byte(accessProto)
+			// 存储含域名的key
 			if info != "" {
-				// 提前存储只含ip数据的key, 避免即记录域名又记录一笔IP数据的记录
-				ipKey := make([]byte, 51)
-				copy(ipKey, key)
-				ipS := utils.BytesToString(ipKey)
-				cSess.IpAuditMap.Set(ipS, nu)
-				// 存储含域名的key
-				key[34] = byte(accessProto)
 				md5Sum := md5.Sum([]byte(info))
-				copy(key[35:51], hex.EncodeToString(md5Sum[:]))
+				copy(key[35:51], md5Sum[:])
 			}
-		case flags & 0x19:
-			// URG
-			return
-		case flags & 0xC2:
-			// SYN-ECE-CWR
-			return
 		}
 	}
 	s := utils.BytesToString(key)
 
 	// 判断已经存在,并且没有过期
-	v, ok := cSess.IpAuditMap.Get(s)
+	v, ok := auditPayload.IpAuditMap.Get(s)
 	if ok && nu-v.(int64) < int64(base.Cfg.AuditInterval) {
 		// 回收byte对象
 		putByte51(b)
 		return
 	}
 
-	cSess.IpAuditMap.Set(s, nu)
+	auditPayload.IpAuditMap.Set(s, nu)
 
 	audit := dbdata.AccessAudit{
-		Username:    cSess.Username,
+		Username:    userName,
 		Protocol:    uint8(ipProto),
 		Src:         ipSrc.String(),
 		Dst:         ipDst.String(),
@@ -190,5 +178,11 @@ func logAudit(cSess *sessdata.ConnSession, pl *sessdata.Payload) {
 		AccessProto: accessProto,
 		Info:        info,
 	}
-	logAuditWrite(audit)
+
+	select {
+	case logBatch.LogChan <- audit:
+	default:
+		base.Error("AccessAudit: LogChan channel is full")
+		return
+	}
 }

+ 28 - 15
anylink/server/handler/payload_tcp_parser.go

@@ -21,7 +21,7 @@ func onTCP(payload []byte) (uint8, string) {
 	}
 	data := payload[ihl:]
 	for _, parser := range tcpParsers {
-		if proto, info := parser(data); info != "" {
+		if proto, info := parser(data); proto != acc_proto_tcp {
 			return proto, info
 		}
 	}
@@ -29,8 +29,7 @@ func onTCP(payload []byte) (uint8, string) {
 }
 
 func sniNewParser(b []byte) (uint8, string) {
-	dataSize := len(b)
-	if dataSize < 2 || b[0] != 0x16 || b[1] != 0x03 {
+	if len(b) < 2 || b[0] != 0x16 || b[1] != 0x03 {
 		return acc_proto_tcp, ""
 	}
 	rest := b[5:]
@@ -51,27 +50,27 @@ func sniNewParser(b []byte) (uint8, string) {
 	// Skip over random number
 	current += 4 + 28
 	if current >= restLen {
-		return acc_proto_tcp, ""
+		return acc_proto_https, ""
 	}
 	// Skip over session ID
 	sessionIDLength := int(rest[current])
 	current += 1
 	current += sessionIDLength
-	if current >= restLen {
-		return acc_proto_tcp, ""
+	if current+1 >= restLen {
+		return acc_proto_https, ""
 	}
 	cipherSuiteLength := (int(rest[current]) << 8) + int(rest[current+1])
 	current += 2
 	current += cipherSuiteLength
 	if current >= restLen {
-		return acc_proto_tcp, ""
+		return acc_proto_https, ""
 	}
 	compressionMethodLength := int(rest[current])
 	current += 1
 	current += compressionMethodLength
 
 	if current >= restLen {
-		return acc_proto_tcp, ""
+		return acc_proto_https, ""
 	}
 	current += 2
 	hostname := ""
@@ -84,27 +83,30 @@ func sniNewParser(b []byte) (uint8, string) {
 			// Skip over number of names as we're assuming there's just one
 			current += 2
 			if current >= restLen {
-				return acc_proto_tcp, ""
+				return acc_proto_https, ""
 			}
 			nameType := rest[current]
 			current += 1
 			if nameType != 0 {
-				return acc_proto_tcp, ""
+				return acc_proto_https, ""
 			}
 			if current+1 >= restLen {
-				return acc_proto_tcp, ""
+				return acc_proto_https, ""
 			}
 			nameLen := (int(rest[current]) << 8) + int(rest[current+1])
 			current += 2
 			if current+nameLen >= restLen {
-				return acc_proto_tcp, ""
+				return acc_proto_https, ""
 			}
 			hostname = string(rest[current : current+nameLen])
 		}
 		current += extensionDataLength
 	}
 	if hostname == "" {
-		return acc_proto_tcp, ""
+		return acc_proto_https, ""
+	}
+	if !validDomainChar(hostname) {
+		return acc_proto_https, ""
 	}
 	return acc_proto_https, hostname
 }
@@ -150,8 +152,7 @@ func httpNewParser(data []byte) (uint8, string) {
 }
 
 func sniParser(data []byte) (uint8, string) {
-	dataSize := len(data)
-	if dataSize < 2 || data[0] != 0x16 || data[1] != 0x03 {
+	if len(data) < 2 || data[0] != 0x16 || data[1] != 0x03 {
 		return acc_proto_tcp, ""
 	}
 	sniRe := regexp.MustCompile("\x00\x00.{4}\x00.{2}([a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,6})\x00")
@@ -169,3 +170,15 @@ func httpParser(data []byte) (uint8, string) {
 	}
 	return acc_proto_tcp, ""
 }
+
+// 校验域名的合法字符, 处理乱码问题
+func validDomainChar(addr string) bool {
+	// Allow a-z A-Z . - 0-9
+	for i := 0; i < len(addr); i++ {
+		c := addr[i]
+		if !((c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 45 && c <= 46) || (c >= 48 && c <= 57)) {
+			return false
+		}
+	}
+	return true
+}

+ 8 - 4
anylink/server/handler/payload_test.go

@@ -51,22 +51,26 @@ func BenchmarkNewHttpParser(b *testing.B) {
 func TestNewSniParser(t *testing.T) {
 	ast := assert.New(t)
 	data := handlerTcpPayload(httpsPacket)
-	_, sni := sniNewParser(data)
+	proto, sni := sniNewParser(data)
 	ast.Equal(sni, httpsSni)
+	ast.Equal(int(proto), acc_proto_https)
 }
 
 func TestNewHttpParser(t *testing.T) {
 	ast := assert.New(t)
 	// Host
 	data := handlerTcpPayload(httpPacket)
-	_, hostname := httpNewParser(data)
+	proto, hostname := httpNewParser(data)
 	ast.Equal(hostname, httpHost)
+	ast.Equal(int(proto), acc_proto_http)
 	// HOST
 	data = handlerTcpPayload(httpPacket2)
-	_, hostname = httpNewParser(data)
+	proto, hostname = httpNewParser(data)
 	ast.Equal(hostname, httpHost)
+	ast.Equal(int(proto), acc_proto_http)
 	// GET http://www.google.com/index.html HTTP/1.1
 	data = handlerTcpPayload(httpPacket3)
-	_, hostname = httpNewParser(data)
+	proto, hostname = httpNewParser(data)
 	ast.Equal(hostname, httpHost)
+	ast.Equal(int(proto), acc_proto_http)
 }

+ 22 - 7
anylink/server/handler/server.go

@@ -3,6 +3,7 @@ package handler
 import (
 	"crypto/tls"
 	"fmt"
+	"io"
 	"log"
 	"net"
 	"net/http"
@@ -10,8 +11,9 @@ import (
 	"time"
 
 	"github.com/bjdgyc/anylink/base"
-	"github.com/bjdgyc/anylink/pkg/proxyproto"
+	"github.com/bjdgyc/anylink/dbdata"
 	"github.com/gorilla/mux"
+	"github.com/pires/go-proxyproto"
 )
 
 func startTls() {
@@ -47,13 +49,19 @@ func startTls() {
 		NextProtos:   []string{"http/1.1"},
 		MinVersion:   tls.VersionTLS12,
 		CipherSuites: selectedCipherSuites,
+		GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
+			base.Trace("GetCertificate", chi.ServerName)
+			return dbdata.GetCertificateBySNI(chi.ServerName)
+		},
 		// InsecureSkipVerify: true,
 	}
 	srv := &http.Server{
-		Addr:      addr,
-		Handler:   initRoute(),
-		TLSConfig: tlsConfig,
-		ErrorLog:  base.GetBaseLog(),
+		Addr:         addr,
+		Handler:      initRoute(),
+		TLSConfig:    tlsConfig,
+		ErrorLog:     base.GetBaseLog(),
+		ReadTimeout:  60 * time.Second,
+		WriteTimeout: 60 * time.Second,
 	}
 
 	ln, err = net.Listen("tcp", addr)
@@ -63,11 +71,14 @@ func startTls() {
 	defer ln.Close()
 
 	if base.Cfg.ProxyProtocol {
-		ln = &proxyproto.Listener{Listener: ln, ProxyHeaderTimeout: time.Second * 5}
+		ln = &proxyproto.Listener{
+			Listener:          ln,
+			ReadHeaderTimeout: 30 * time.Second,
+		}
 	}
 
 	base.Info("listen server", addr)
-	err = srv.ServeTLS(ln, base.Cfg.CertFile, base.Cfg.CertKey)
+	err = srv.ServeTLS(ln, "", "")
 	if err != nil {
 		base.Fatal(err)
 	}
@@ -88,6 +99,10 @@ func initRoute() http.Handler {
 			http.FileServer(http.Dir(base.Cfg.FilesPath)),
 		),
 	)
+	// 健康检测
+	r.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
+		io.WriteString(w, "ok")
+	}).Methods(http.MethodGet)
 	r.NotFoundHandler = http.HandlerFunc(notFound)
 	return r
 }

+ 1 - 0
anylink/server/main.go

@@ -1,5 +1,6 @@
 // AnyLink 是一个企业级远程办公vpn软件,可以支持多人同时在线使用。
 
+//go:build !windows
 // +build !windows
 
 package main

+ 0 - 290
anylink/server/pkg/proxyproto/protocol.go

@@ -1,290 +0,0 @@
-// copy from: https://github.com/armon/go-proxyproto/blob/master/protocol.go
-// design: http://www.haproxy.org/download/2.2/doc/proxy-protocol.txt
-
-// HAProxy proxy proto v1
-package proxyproto
-
-import (
-	"bufio"
-	"bytes"
-	"errors"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-)
-
-var (
-	// prefix is the string we look for at the start of a connection
-	// to check if this connection is using the proxy protocol
-	prefix    = []byte("PROXY ")
-	prefixLen = len(prefix)
-
-	ErrInvalidUpstream = errors.New("upstream connection address not trusted for PROXY information")
-)
-
-// SourceChecker can be used to decide whether to trust the PROXY info or pass
-// the original connection address through. If set, the connecting address is
-// passed in as an argument. If the function returns an error due to the source
-// being disallowed, it should return ErrInvalidUpstream.
-//
-// If error is not nil, the call to Accept() will fail. If the reason for
-// triggering this failure is due to a disallowed source, it should return
-// ErrInvalidUpstream.
-//
-// If bool is true, the PROXY-set address is used.
-//
-// If bool is false, the connection's remote address is used, rather than the
-// address claimed in the PROXY info.
-type SourceChecker func(net.Addr) (bool, error)
-
-// Listener is used to wrap an underlying listener,
-// whose connections may be using the HAProxy Proxy Protocol (version 1).
-// If the connection is using the protocol, the RemoteAddr() will return
-// the correct client address.
-//
-// Optionally define ProxyHeaderTimeout to set a maximum time to
-// receive the Proxy Protocol Header. Zero means no timeout.
-type Listener struct {
-	Listener           net.Listener
-	ProxyHeaderTimeout time.Duration
-	SourceCheck        SourceChecker
-	UnknownOK          bool // allow PROXY UNKNOWN
-}
-
-// Conn is used to wrap and underlying connection which
-// may be speaking the Proxy Protocol. If it is, the RemoteAddr() will
-// return the address of the client instead of the proxy address.
-type Conn struct {
-	bufReader          *bufio.Reader
-	conn               net.Conn
-	dstAddr            *net.TCPAddr
-	srcAddr            *net.TCPAddr
-	useConnAddr        bool
-	once               sync.Once
-	proxyHeaderTimeout time.Duration
-	unknownOK          bool
-}
-
-// Accept waits for and returns the next connection to the listener.
-func (p *Listener) Accept() (net.Conn, error) {
-	// Get the underlying connection
-	conn, err := p.Listener.Accept()
-	if err != nil {
-		return nil, err
-	}
-	var useConnAddr bool
-	if p.SourceCheck != nil {
-		allowed, err := p.SourceCheck(conn.RemoteAddr())
-		if err != nil {
-			return nil, err
-		}
-		if !allowed {
-			useConnAddr = true
-		}
-	}
-	newConn := NewConn(conn, p.ProxyHeaderTimeout)
-	newConn.useConnAddr = useConnAddr
-	newConn.unknownOK = p.UnknownOK
-	return newConn, nil
-}
-
-// Close closes the underlying listener.
-func (p *Listener) Close() error {
-	return p.Listener.Close()
-}
-
-// Addr returns the underlying listener's network address.
-func (p *Listener) Addr() net.Addr {
-	return p.Listener.Addr()
-}
-
-// NewConn is used to wrap a net.Conn that may be speaking
-// the proxy protocol into a proxyproto.Conn
-func NewConn(conn net.Conn, timeout time.Duration) *Conn {
-	pConn := &Conn{
-		bufReader:          bufio.NewReader(conn),
-		conn:               conn,
-		proxyHeaderTimeout: timeout,
-	}
-	return pConn
-}
-
-// Read is check for the proxy protocol header when doing
-// the initial scan. If there is an error parsing the header,
-// it is returned and the socket is closed.
-func (p *Conn) Read(b []byte) (int, error) {
-	var err error
-	p.once.Do(func() { err = p.checkPrefix() })
-	if err != nil {
-		return 0, err
-	}
-	return p.bufReader.Read(b)
-}
-
-func (p *Conn) ReadFrom(r io.Reader) (int64, error) {
-	if rf, ok := p.conn.(io.ReaderFrom); ok {
-		return rf.ReadFrom(r)
-	}
-	return io.Copy(p.conn, r)
-}
-
-func (p *Conn) WriteTo(w io.Writer) (int64, error) {
-	var err error
-	p.once.Do(func() { err = p.checkPrefix() })
-	if err != nil {
-		return 0, err
-	}
-	return p.bufReader.WriteTo(w)
-}
-
-func (p *Conn) Write(b []byte) (int, error) {
-	return p.conn.Write(b)
-}
-
-func (p *Conn) Close() error {
-	return p.conn.Close()
-}
-
-func (p *Conn) LocalAddr() net.Addr {
-	p.checkPrefixOnce()
-	if p.dstAddr != nil && !p.useConnAddr {
-		return p.dstAddr
-	}
-	return p.conn.LocalAddr()
-}
-
-// RemoteAddr returns the address of the client if the proxy
-// protocol is being used, otherwise just returns the address of
-// the socket peer. If there is an error parsing the header, the
-// address of the client is not returned, and the socket is closed.
-// Once implication of this is that the call could block if the
-// client is slow. Using a Deadline is recommended if this is called
-// before Read()
-func (p *Conn) RemoteAddr() net.Addr {
-	p.checkPrefixOnce()
-	if p.srcAddr != nil && !p.useConnAddr {
-		return p.srcAddr
-	}
-	return p.conn.RemoteAddr()
-}
-
-func (p *Conn) SetDeadline(t time.Time) error {
-	return p.conn.SetDeadline(t)
-}
-
-func (p *Conn) SetReadDeadline(t time.Time) error {
-	return p.conn.SetReadDeadline(t)
-}
-
-func (p *Conn) SetWriteDeadline(t time.Time) error {
-	return p.conn.SetWriteDeadline(t)
-}
-
-func (p *Conn) checkPrefixOnce() {
-	p.once.Do(func() {
-		if err := p.checkPrefix(); err != nil && err != io.EOF {
-			log.Printf("[ERR] Failed to read proxy prefix: %v", err)
-			p.Close()
-			p.bufReader = bufio.NewReader(p.conn)
-		}
-	})
-}
-
-func (p *Conn) checkPrefix() error {
-	if p.proxyHeaderTimeout != 0 {
-		readDeadLine := time.Now().Add(p.proxyHeaderTimeout)
-		_ = p.conn.SetReadDeadline(readDeadLine)
-		defer func() {
-			_ = p.conn.SetReadDeadline(time.Time{})
-		}()
-	}
-
-	// Incrementally check each byte of the prefix
-	for i := 1; i <= prefixLen; i++ {
-		inp, err := p.bufReader.Peek(i)
-
-		if err != nil {
-			if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
-				return nil
-			} else {
-				return err
-			}
-		}
-
-		// Check for a prefix mis-match, quit early
-		if !bytes.Equal(inp, prefix[:i]) {
-			return nil
-		}
-	}
-
-	// Read the header line
-	header, err := p.bufReader.ReadString('\n')
-	if err != nil {
-		p.conn.Close()
-		return err
-	}
-
-	// Strip the carriage return and new line
-	header = header[:len(header)-2]
-
-	// Split on spaces, should be (PROXY <type> <src addr> <dst addr> <src port> <dst port>)
-	parts := strings.Split(header, " ")
-	if len(parts) < 2 {
-		p.conn.Close()
-		return fmt.Errorf("Invalid header line: %s", header)
-	}
-
-	// Verify the type is known
-	switch parts[1] {
-	case "UNKNOWN":
-		if !p.unknownOK || len(parts) != 2 {
-			p.conn.Close()
-			return fmt.Errorf("Invalid UNKNOWN header line: %s", header)
-		}
-		p.useConnAddr = true
-		return nil
-	case "TCP4":
-	case "TCP6":
-	default:
-		p.conn.Close()
-		return fmt.Errorf("Unhandled address type: %s", parts[1])
-	}
-
-	if len(parts) != 6 {
-		p.conn.Close()
-		return fmt.Errorf("Invalid header line: %s", header)
-	}
-
-	// Parse out the source address
-	ip := net.ParseIP(parts[2])
-	if ip == nil {
-		p.conn.Close()
-		return fmt.Errorf("Invalid source ip: %s", parts[2])
-	}
-	port, err := strconv.Atoi(parts[4])
-	if err != nil {
-		p.conn.Close()
-		return fmt.Errorf("Invalid source port: %s", parts[4])
-	}
-	p.srcAddr = &net.TCPAddr{IP: ip, Port: port}
-
-	// Parse out the destination address
-	ip = net.ParseIP(parts[3])
-	if ip == nil {
-		p.conn.Close()
-		return fmt.Errorf("Invalid destination ip: %s", parts[3])
-	}
-	port, err = strconv.Atoi(parts[5])
-	if err != nil {
-		p.conn.Close()
-		return fmt.Errorf("Invalid destination port: %s", parts[5])
-	}
-	p.dstAddr = &net.TCPAddr{IP: ip, Port: port}
-
-	return nil
-}

+ 0 - 486
anylink/server/pkg/proxyproto/protocol_test.go

@@ -1,486 +0,0 @@
-// copy from: https://github.com/armon/go-proxyproto/blob/master/protocol_test.go
-package proxyproto
-
-import (
-	"bytes"
-	"io"
-	"net"
-	"testing"
-	"time"
-)
-
-const (
-	goodAddr = "127.0.0.1"
-	badAddr  = "127.0.0.2"
-	errAddr  = "9999.0.0.2"
-)
-
-var (
-	checkAddr string
-)
-
-func TestPassthrough(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	pl := &Listener{Listener: l}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-}
-
-func TestTimeout(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	clientWriteDelay := 200 * time.Millisecond
-	proxyHeaderTimeout := 50 * time.Millisecond
-	pl := &Listener{Listener: l, ProxyHeaderTimeout: proxyHeaderTimeout}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Do not send data for a while
-		time.Sleep(clientWriteDelay)
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	// Check the remote addr is the original 127.0.0.1
-	remoteAddrStartTime := time.Now()
-	addr := conn.RemoteAddr().(*net.TCPAddr)
-	if addr.IP.String() != "127.0.0.1" {
-		t.Fatalf("bad: %v", addr)
-	}
-	remoteAddrDuration := time.Since(remoteAddrStartTime)
-
-	// Check RemoteAddr() call did timeout
-	if remoteAddrDuration >= clientWriteDelay {
-		t.Fatalf("RemoteAddr() took longer than the specified timeout: %v < %v", proxyHeaderTimeout, remoteAddrDuration)
-	}
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-}
-
-func TestParse_ipv4(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	pl := &Listener{Listener: l}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Write out the header!
-		header := "PROXY TCP4 10.1.1.1 20.2.2.2 1000 2000\r\n"
-		conn.Write([]byte(header))
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	// Check the remote addr
-	addr := conn.RemoteAddr().(*net.TCPAddr)
-	if addr.IP.String() != "10.1.1.1" {
-		t.Fatalf("bad: %v", addr)
-	}
-	if addr.Port != 1000 {
-		t.Fatalf("bad: %v", addr)
-	}
-}
-
-func TestParse_ipv6(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	pl := &Listener{Listener: l}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Write out the header!
-		header := "PROXY TCP6 ffff::ffff ffff::ffff 1000 2000\r\n"
-		conn.Write([]byte(header))
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	// Check the remote addr
-	addr := conn.RemoteAddr().(*net.TCPAddr)
-	if addr.IP.String() != "ffff::ffff" {
-		t.Fatalf("bad: %v", addr)
-	}
-	if addr.Port != 1000 {
-		t.Fatalf("bad: %v", addr)
-	}
-}
-
-func TestParse_Unknown(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	pl := &Listener{Listener: l, UnknownOK: true}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Write out the header!
-		header := "PROXY UNKNOWN\r\n"
-		conn.Write([]byte(header))
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-}
-
-func TestParse_BadHeader(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	pl := &Listener{Listener: l}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Write out the header!
-		header := "PROXY TCP4 what 127.0.0.1 1000 2000\r\n"
-		conn.Write([]byte(header))
-
-		conn.Write([]byte("ping"))
-
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err == nil {
-			t.Fatalf("err: %v", err)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	// Check the remote addr, should be the local addr
-	addr := conn.RemoteAddr().(*net.TCPAddr)
-	if addr.IP.String() != "127.0.0.1" {
-		t.Fatalf("bad: %v", addr)
-	}
-
-	// Read should fail
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err == nil {
-		t.Fatalf("err: %v", err)
-	}
-}
-
-func TestParse_ipv4_checkfunc(t *testing.T) {
-	checkAddr = goodAddr
-	testParse_ipv4_checkfunc(t)
-	checkAddr = badAddr
-	testParse_ipv4_checkfunc(t)
-	checkAddr = errAddr
-	testParse_ipv4_checkfunc(t)
-}
-
-func testParse_ipv4_checkfunc(t *testing.T) {
-	l, err := net.Listen("tcp", "127.0.0.1:0")
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	checkFunc := func(addr net.Addr) (bool, error) {
-		tcpAddr := addr.(*net.TCPAddr)
-		if tcpAddr.IP.String() == checkAddr {
-			return true, nil
-		}
-		return false, nil
-	}
-
-	pl := &Listener{Listener: l, SourceCheck: checkFunc}
-
-	go func() {
-		conn, err := net.Dial("tcp", pl.Addr().String())
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		defer conn.Close()
-
-		// Write out the header!
-		header := "PROXY TCP4 10.1.1.1 20.2.2.2 1000 2000\r\n"
-		conn.Write([]byte(header))
-
-		conn.Write([]byte("ping"))
-		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
-		}
-		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
-		}
-	}()
-
-	conn, err := pl.Accept()
-	if err != nil {
-		if checkAddr == badAddr {
-			return
-		}
-		t.Fatalf("err: %v", err)
-	}
-	defer conn.Close()
-
-	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
-		t.Fatalf("err: %v", err)
-	}
-	if !bytes.Equal(recv, []byte("ping")) {
-		t.Fatalf("bad: %v", recv)
-	}
-
-	if _, err := conn.Write([]byte("pong")); err != nil {
-		t.Fatalf("err: %v", err)
-	}
-
-	// Check the remote addr
-	addr := conn.RemoteAddr().(*net.TCPAddr)
-	switch checkAddr {
-	case goodAddr:
-		if addr.IP.String() != "10.1.1.1" {
-			t.Fatalf("bad: %v", addr)
-		}
-		if addr.Port != 1000 {
-			t.Fatalf("bad: %v", addr)
-		}
-	case badAddr:
-		if addr.IP.String() != "127.0.0.1" {
-			t.Fatalf("bad: %v", addr)
-		}
-		if addr.Port == 1000 {
-			t.Fatalf("bad: %v", addr)
-		}
-	}
-}
-
-type testConn struct {
-	readFromCalledWith io.Reader
-	net.Conn           // nil; crash on any unexpected use
-}
-
-func (c *testConn) ReadFrom(r io.Reader) (int64, error) {
-	c.readFromCalledWith = r
-	return 0, nil
-}
-func (c *testConn) Write(p []byte) (int, error) {
-	return len(p), nil
-}
-func (c *testConn) Read(p []byte) (int, error) {
-	return 1, nil
-}
-
-func TestCopyToWrappedConnection(t *testing.T) {
-	innerConn := &testConn{}
-	wrappedConn := NewConn(innerConn, 0)
-	dummySrc := &testConn{}
-
-	io.Copy(wrappedConn, dummySrc)
-	if innerConn.readFromCalledWith != dummySrc {
-		t.Error("Expected io.Copy to delegate to ReadFrom function of inner destination connection")
-	}
-}
-
-func TestCopyFromWrappedConnection(t *testing.T) {
-	wrappedConn := NewConn(&testConn{}, 0)
-	dummyDst := &testConn{}
-
-	io.Copy(dummyDst, wrappedConn)
-	if dummyDst.readFromCalledWith != wrappedConn.conn {
-		t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom method of destination")
-	}
-}
-
-func TestCopyFromWrappedConnectionToWrappedConnection(t *testing.T) {
-	innerConn1 := &testConn{}
-	wrappedConn1 := NewConn(innerConn1, 0)
-	innerConn2 := &testConn{}
-	wrappedConn2 := NewConn(innerConn2, 0)
-
-	io.Copy(wrappedConn1, wrappedConn2)
-	if innerConn1.readFromCalledWith != innerConn2 {
-		t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom of inner destination connection")
-	}
-
-}

+ 35 - 0
anylink/server/sessdata/compress.go

@@ -0,0 +1,35 @@
+package sessdata
+
+import (
+	"github.com/lanrenwo/lzsgo"
+)
+
+type CmpEncoding interface {
+	Compress(src []byte, dst []byte) (int, error)
+	Uncompress(src []byte, dst []byte) (int, error)
+}
+
+type LzsgoCmp struct {
+}
+
+func (l LzsgoCmp) Compress(src []byte, dst []byte) (int, error) {
+	n, err := lzsgo.Compress(src, dst)
+	return n, err
+}
+
+func (l LzsgoCmp) Uncompress(src []byte, dst []byte) (int, error) {
+	n, err := lzsgo.Uncompress(src, dst)
+	return n, err
+}
+
+// type Lz4Cmp struct {
+// 	c lz4.Compressor
+// }
+
+// func (l Lz4Cmp) Compress(src []byte, dst []byte) (int, error) {
+// 	return l.c.CompressBlock(src, dst)
+// }
+
+// func (l Lz4Cmp) Uncompress(src []byte, dst []byte) (int, error) {
+// 	return lz4.UncompressBlock(src, dst)
+// }

+ 28 - 0
anylink/server/sessdata/compress_test.go

@@ -0,0 +1,28 @@
+package sessdata
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLzsCompress(t *testing.T) {
+	var (
+		n   int
+		err error
+	)
+	assert := assert.New(t)
+	c := LzsgoCmp{}
+	s := "hello anylink, you are best!"
+	src := []byte(strings.Repeat(s, 50))
+
+	comprBuf := make([]byte, 2048)
+	n, err = c.Compress(src, comprBuf)
+	assert.Nil(err)
+
+	unprBuf := make([]byte, 2048)
+	n, err = c.Uncompress(comprBuf[:n], unprBuf)
+	assert.Nil(err)
+	assert.Equal(src, unprBuf[:n])
+}

+ 171 - 58
anylink/server/sessdata/ip_pool.go

@@ -14,8 +14,10 @@ var (
 	IpPool   = &ipPoolConfig{}
 	ipActive = map[string]bool{}
 	// ipKeep and ipLease  ipAddr => type
-	ipLease   = map[string]bool{}
+	// ipLease   = map[string]bool{}
 	ipPoolMux sync.Mutex
+	// 记录循环点
+	loopCurIp uint32
 )
 
 type ipPoolConfig struct {
@@ -36,7 +38,19 @@ func initIpPool() {
 	}
 	IpPool.Ipv4IPNet = ipNet
 	IpPool.Ipv4Mask = net.IP(ipNet.Mask)
-	IpPool.Ipv4Gateway = net.ParseIP(base.Cfg.Ipv4Gateway)
+
+	ipv4Gateway := net.ParseIP(base.Cfg.Ipv4Gateway)
+	ipStart := net.ParseIP(base.Cfg.Ipv4Start)
+	ipEnd := net.ParseIP(base.Cfg.Ipv4End)
+	if !ipNet.Contains(ipv4Gateway) || !ipNet.Contains(ipStart) || !ipNet.Contains(ipEnd) {
+		panic("ip段 设置错误")
+	}
+	// ip地址池
+	IpPool.Ipv4Gateway = ipv4Gateway
+	IpPool.IpLongMin = utils.Ip2long(ipStart)
+	IpPool.IpLongMax = utils.Ip2long(ipEnd)
+
+	loopCurIp = IpPool.IpLongMin
 
 	// 网络地址零值
 	// zero := binary.BigEndian.Uint32(ip.Mask(mask))
@@ -44,71 +58,160 @@ func initIpPool() {
 	// one, _ := ipNet.Mask.Size()
 	// max := min | uint32(math.Pow(2, float64(32-one))-1)
 
-	// ip地址池
-	IpPool.IpLongMin = utils.Ip2long(net.ParseIP(base.Cfg.Ipv4Start))
-	IpPool.IpLongMax = utils.Ip2long(net.ParseIP(base.Cfg.Ipv4End))
-
 	// 获取IpLease数据
-	go cronIpLease()
+	// go cronIpLease()
 }
 
-func cronIpLease() {
-	getIpLease()
-	tick := time.NewTicker(time.Minute * 30)
-	for range tick.C {
-		getIpLease()
-	}
-}
-
-func getIpLease() {
-	xdb := dbdata.GetXdb()
-	keepIpMaps := []dbdata.IpMap{}
-	sNow := time.Now().Add(-1 * time.Duration(base.Cfg.IpLease) * time.Second)
-	err := xdb.Cols("ip_addr").Where("keep=?", true).Or("last_login>?", sNow).Find(&keepIpMaps)
-	if err != nil {
-		base.Error(err)
-	}
-	// fmt.Println(keepIpMaps)
-	ipPoolMux.Lock()
-	ipLease = map[string]bool{}
-	for _, v := range keepIpMaps {
-		ipLease[v.IpAddr] = true
-	}
-	ipPoolMux.Unlock()
-}
+// func cronIpLease() {
+// 	getIpLease()
+// 	tick := time.NewTicker(time.Minute * 30)
+// 	for range tick.C {
+// 		getIpLease()
+// 	}
+// }
+//
+// func getIpLease() {
+// 	xdb := dbdata.GetXdb()
+// 	keepIpMaps := []dbdata.IpMap{}
+// 	sNow := time.Now().Add(-1 * time.Duration(base.Cfg.IpLease) * time.Second)
+// 	err := xdb.Cols("ip_addr").Where("keep=?", true).
+// 		Or("unique_mac=? and last_login>?", true, sNow).Find(&keepIpMaps)
+// 	if err != nil {
+// 		base.Error(err)
+// 	}
+// 	// fmt.Println(keepIpMaps)
+// 	ipPoolMux.Lock()
+// 	ipLease = map[string]bool{}
+// 	for _, v := range keepIpMaps {
+// 		ipLease[v.IpAddr] = true
+// 	}
+// 	ipPoolMux.Unlock()
+// }
 
 // AcquireIp 获取动态ip
-func AcquireIp(username, macAddr string) net.IP {
+func AcquireIp(username, macAddr string, uniqueMac bool) net.IP {
+	base.Trace("AcquireIp:", username, macAddr, uniqueMac)
 	ipPoolMux.Lock()
 	defer ipPoolMux.Unlock()
 
-	tNow := time.Now()
+	var (
+		err  error
+		tNow = time.Now()
+	)
 
-	// 判断是否已经分配过
-	mi := &dbdata.IpMap{}
-	err := dbdata.One("mac_addr", macAddr, mi)
-	// 存在ip记录
-	if err == nil {
+	if uniqueMac {
+		// 判断是否已经分配过
+		mi := &dbdata.IpMap{}
+		err = dbdata.One("mac_addr", macAddr, mi)
+		if err != nil {
+			// 没有查询到数据
+			if dbdata.CheckErrNotFound(err) {
+				return loopIp(username, macAddr, uniqueMac)
+			}
+			// 查询报错
+			base.Error(err)
+			return nil
+		}
+
+		// 存在ip记录
+		base.Trace("uniqueMac:", username, mi)
 		ipStr := mi.IpAddr
 		ip := net.ParseIP(ipStr)
 		// 跳过活跃连接
 		_, ok := ipActive[ipStr]
 		// 检测原有ip是否在新的ip池内
-		if IpPool.Ipv4IPNet.Contains(ip) && !ok &&
+		// IpPool.Ipv4IPNet.Contains(ip) &&
+		if !ok &&
 			utils.Ip2long(ip) >= IpPool.IpLongMin &&
 			utils.Ip2long(ip) <= IpPool.IpLongMax {
 			mi.Username = username
 			mi.LastLogin = tNow
+			mi.UniqueMac = uniqueMac
 			// 回写db数据
 			_ = dbdata.Set(mi)
 			ipActive[ipStr] = true
 			return ip
 		}
+		// 删除当前macAddr
+		mi = &dbdata.IpMap{MacAddr: macAddr}
 		_ = dbdata.Del(mi)
+
+	} else {
+		// 没有获取到mac的情况
+		ipMaps := []dbdata.IpMap{}
+		err = dbdata.FindWhere(&ipMaps, 50, 1, "username=? and unique_mac=?", username, false)
+		if err != nil {
+			// 没有查询到数据
+			if dbdata.CheckErrNotFound(err) {
+				return loopIp(username, macAddr, uniqueMac)
+			}
+			// 查询报错
+			base.Error(err)
+			return nil
+		}
+
+		// 遍历mac记录
+		for _, mi := range ipMaps {
+			ipStr := mi.IpAddr
+			ip := net.ParseIP(ipStr)
+
+			// 跳过活跃连接
+			if _, ok := ipActive[ipStr]; ok {
+				continue
+			}
+			// 跳过保留ip
+			if mi.Keep {
+				continue
+			}
+			// 没有mac的 不需要验证租期
+			// mi.LastLogin.Before(leaseTime) &&
+			if utils.Ip2long(ip) >= IpPool.IpLongMin &&
+				utils.Ip2long(ip) <= IpPool.IpLongMax {
+				mi.LastLogin = tNow
+				mi.MacAddr = macAddr
+				mi.UniqueMac = uniqueMac
+				// 回写db数据
+				_ = dbdata.Set(mi)
+				ipActive[ipStr] = true
+				return ip
+			}
+		}
 	}
 
+	return loopIp(username, macAddr, uniqueMac)
+}
+
+func loopIp(username, macAddr string, uniqueMac bool) net.IP {
+	var (
+		i  uint32
+		ip net.IP
+	)
+
+	i, ip = loopLong(loopCurIp, IpPool.IpLongMax, username, macAddr, uniqueMac)
+	if ip != nil {
+		loopCurIp = i
+		return ip
+	}
+
+	i, ip = loopLong(IpPool.IpLongMin, loopCurIp, username, macAddr, uniqueMac)
+	if ip != nil {
+		loopCurIp = i
+		return ip
+	}
+
+	base.Warn("no ip available, please see ip_map table row", username, macAddr)
+	return nil
+}
+
+func loopLong(start, end uint32, username, macAddr string, uniqueMac bool) (uint32, net.IP) {
+	var (
+		err       error
+		tNow      = time.Now()
+		leaseTime = time.Now().Add(-1 * time.Duration(base.Cfg.IpLease) * time.Second)
+	)
+
 	// 全局遍历超过租期和未保留的ip
-	for i := IpPool.IpLongMin; i <= IpPool.IpLongMax; i++ {
+	for i := start; i <= end; i++ {
 		ip := utils.Long2ip(i)
 		ipStr := ip.String()
 
@@ -116,32 +219,42 @@ func AcquireIp(username, macAddr string) net.IP {
 		if _, ok := ipActive[ipStr]; ok {
 			continue
 		}
-		// 跳过ip租期内数据
-		if _, ok := ipLease[ipStr]; ok {
-			continue
+
+		mi := &dbdata.IpMap{}
+		err = dbdata.One("ip_addr", ipStr, mi)
+		if err != nil {
+			// 没有查询到数据
+			if dbdata.CheckErrNotFound(err) {
+				// 该ip没有被使用
+				mi = &dbdata.IpMap{IpAddr: ipStr, MacAddr: macAddr, UniqueMac: uniqueMac, Username: username, LastLogin: tNow}
+				_ = dbdata.Add(mi)
+				ipActive[ipStr] = true
+				return i, ip
+			}
+			// 查询报错
+			base.Error(err)
+			return 0, nil
 		}
 
-		v := &dbdata.IpMap{}
-		err = dbdata.One("ip_addr", ipStr, v)
-		if err == nil {
-			// 存在记录直接跳过
+		// 查询到已经使用的ip
+		// 跳过保留ip
+		if mi.Keep {
 			continue
 		}
-
-		if dbdata.CheckErrNotFound(err) {
-			// 该ip没有被使用
-			mi = &dbdata.IpMap{IpAddr: ipStr, MacAddr: macAddr, Username: username, LastLogin: tNow}
-			_ = dbdata.Add(mi)
+		// 判断租期
+		if mi.LastLogin.Before(leaseTime) {
+			// 存在记录,说明已经超过租期,可以直接使用
+			mi.LastLogin = tNow
+			mi.MacAddr = macAddr
+			mi.UniqueMac = uniqueMac
+			// 回写db数据
+			_ = dbdata.Set(mi)
 			ipActive[ipStr] = true
-			return ip
+			return i, ip
 		}
-		// 查询报错
-		base.Error(err)
-		return nil
 	}
 
-	base.Warn("no ip available, please see ip_map table row")
-	return nil
+	return 0, nil
 }
 
 // 回收ip

+ 32 - 17
anylink/server/sessdata/ip_pool_test.go

@@ -6,6 +6,7 @@ import (
 	"os"
 	"path"
 	"testing"
+	"time"
 
 	"github.com/bjdgyc/anylink/base"
 	"github.com/bjdgyc/anylink/dbdata"
@@ -18,10 +19,12 @@ func preData(tmpDir string) {
 	base.Cfg.DbType = "sqlite3"
 	base.Cfg.DbSource = tmpDb
 	base.Cfg.Ipv4CIDR = "192.168.3.0/24"
-	base.Cfg.Ipv4Start = "192.168.3.1"
-	base.Cfg.Ipv4End = "192.168.3.199"
+	base.Cfg.Ipv4Gateway = "192.168.3.1"
+	base.Cfg.Ipv4Start = "192.168.3.100"
+	base.Cfg.Ipv4End = "192.168.3.150"
 	base.Cfg.MaxClient = 100
 	base.Cfg.MaxUserClient = 3
+	base.Cfg.IpLease = 5
 
 	dbdata.Start()
 	group := dbdata.Group{
@@ -46,22 +49,34 @@ func TestIpPool(t *testing.T) {
 
 	var ip net.IP
 
-	for i := 1; i <= 100; i++ {
-		_ = AcquireIp("user", fmt.Sprintf("mac-%d", i))
+	for i := 100; i <= 150; i++ {
+		_ = AcquireIp(getTestUser(i), getTestMacAddr(i), true)
 	}
-	ip = AcquireIp("user", "mac-new")
-	assert.True(net.IPv4(192, 168, 3, 101).Equal(ip))
-	for i := 102; i <= 199; i++ {
-		ip = AcquireIp("user", fmt.Sprintf("mac-%d", i))
-	}
-	assert.True(net.IPv4(192, 168, 3, 199).Equal(ip))
-	ip = AcquireIp("user", "mac-nil")
-	assert.Nil(ip)
 
-	ReleaseIp(net.IPv4(192, 168, 3, 88), "mac-88")
-	ReleaseIp(net.IPv4(192, 168, 3, 188), "mac-188")
+	// 回收
+	ReleaseIp(net.IPv4(192, 168, 3, 140), getTestMacAddr(140))
+	time.Sleep(time.Second * 6)
+
 	// 从头循环获取可用ip
-	ip = AcquireIp("user", "mac-188")
-	t.Log("mac-188", ip)
-	assert.True(net.IPv4(192, 168, 3, 188).Equal(ip))
+	user_new := getTestUser(210)
+	mac_new := getTestMacAddr(210)
+	ip = AcquireIp(user_new, mac_new, true)
+	t.Log("mac_new", ip)
+	assert.NotNil(ip)
+	assert.True(net.IPv4(192, 168, 3, 140).Equal(ip))
+
+	// 回收全部
+	for i := 100; i <= 150; i++ {
+		ReleaseIp(net.IPv4(192, 168, 3, byte(i)), getTestMacAddr(i))
+	}
+}
+
+func getTestUser(i int) string {
+	return fmt.Sprintf("user-%d", i)
+}
+
+func getTestMacAddr(i int) string {
+	// 前缀mac
+	macAddr := "02:00:00:00:00"
+	return fmt.Sprintf("%s:%x", macAddr, i)
 }

+ 2 - 0
anylink/server/sessdata/online.go

@@ -14,6 +14,7 @@ type Online struct {
 	Username         string    `json:"username"`
 	Group            string    `json:"group"`
 	MacAddr          string    `json:"mac_addr"`
+	UniqueMac        bool      `json:"unique_mac"`
 	Ip               net.IP    `json:"ip"`
 	RemoteAddr       string    `json:"remote_addr"`
 	TunName          string    `json:"tun_name"`
@@ -52,6 +53,7 @@ func OnlineSess() []Online {
 				Username:         v.Username,
 				Group:            v.Group,
 				MacAddr:          v.MacAddr,
+				UniqueMac:        v.UniqueMac,
 				RemoteAddr:       v.CSess.RemoteAddr,
 				TunName:          v.CSess.IfName,
 				Mtu:              v.CSess.Mtu,

+ 136 - 33
anylink/server/sessdata/session.go

@@ -12,8 +12,7 @@ import (
 
 	"github.com/bjdgyc/anylink/base"
 	"github.com/bjdgyc/anylink/dbdata"
-	"github.com/bjdgyc/anylink/pkg/utils"
-	"github.com/ivpusic/grpool"
+	mapset "github.com/deckarep/golang-set"
 	atomic2 "go.uber.org/atomic"
 )
 
@@ -37,6 +36,8 @@ type ConnSession struct {
 	Mtu                 int
 	IfName              string
 	Client              string // 客户端  mobile pc
+	UserAgent           string // 客户端信息
+	UserLogoutCode      uint8  // 用户/客户端主动登出
 	CstpDpd             int
 	Group               *dbdata.Group
 	Limit               *LimitRater
@@ -51,10 +52,11 @@ type ConnSession struct {
 	PayloadIn           chan *Payload
 	PayloadOutCstp      chan *Payload // Cstp的数据
 	PayloadOutDtls      chan *Payload // Dtls的数据
-	IpAuditMap          utils.IMaps   // 审计的ip数据
-	IpAuditPool         *grpool.Pool  // 审计的IP包解析池
 	// dSess *DtlsSession
 	dSess *atomic.Value
+	// compress
+	CstpPickCmp CmpEncoding
+	DtlsPickCmp CmpEncoding
 }
 
 type DtlsSession struct {
@@ -65,17 +67,22 @@ type DtlsSession struct {
 }
 
 type Session struct {
-	mux            sync.RWMutex
-	Sid            string // auth返回的 session-id
-	Token          string // session信息的唯一token
-	DtlsSid        string // dtls协议的 session_id
-	MacAddr        string // 客户端mac地址
-	UniqueIdGlobal string // 客户端唯一标示
-	MacHw          net.HardwareAddr
-	Username       string // 用户名
-	Group          string
-	AuthStep       string
-	AuthPass       string
+	mux             sync.RWMutex
+	Sid             string // auth返回的 session-id
+	Token           string // session信息的唯一token
+	DtlsSid         string // dtls协议的 session_id
+	MacAddr         string // 客户端mac地址
+	UniqueIdGlobal  string // 客户端唯一标示
+	MacHw           net.HardwareAddr
+	UniqueMac       bool   // 客户端获取到真实设备mac
+	Username        string // 用户名
+	Group           string
+	AuthStep        string
+	AuthPass        string
+	RemoteAddr      string
+	UserAgent       string
+	DeviceType      string
+	PlatformVersion string
 
 	LastLogin time.Time
 	IsActive  bool
@@ -97,22 +104,46 @@ func checkSession() {
 		timeout := time.Duration(base.Cfg.SessionTimeout) * time.Second
 		tick := time.NewTicker(time.Second * 60)
 		for range tick.C {
-			sessMux.Lock()
+			outToken := []string{}
+			sessMux.RLock()
 			t := time.Now()
 			for k, v := range sessions {
-				v.mux.Lock()
+				v.mux.RLock()
 				if !v.IsActive {
 					if t.Sub(v.LastLogin) > timeout {
-						delete(sessions, k)
+						outToken = append(outToken, k)
 					}
 				}
-				v.mux.Unlock()
+				v.mux.RUnlock()
+			}
+			sessMux.RUnlock()
+
+			// 删除过期session
+			for _, v := range outToken {
+				CloseSess(v, dbdata.UserLogoutTimeout)
 			}
-			sessMux.Unlock()
 		}
 	}()
 }
 
+// 状态为过期的用户踢下线
+func CloseUserLimittimeSession() {
+	s := mapset.NewSetFromSlice(dbdata.CheckUserlimittime())
+	limitTimeToken := []string{}
+	sessMux.RLock()
+	for _, v := range sessions {
+		v.mux.RLock()
+		if v.IsActive && s.Contains(v.Username) {
+			limitTimeToken = append(limitTimeToken, v.Token)
+		}
+		v.mux.RUnlock()
+	}
+	sessMux.RUnlock()
+	for _, v := range limitTimeToken {
+		CloseSess(v, dbdata.UserLogoutExpire)
+	}
+}
+
 func GenToken() string {
 	// 生成32位的 token
 	bToken := make([]byte, 32)
@@ -151,6 +182,7 @@ func (s *Session) NewConn() *ConnSession {
 	macAddr := s.MacAddr
 	macHw := s.MacHw
 	username := s.Username
+	uniqueMac := s.UniqueMac
 	s.mux.RUnlock()
 	if active {
 		s.CSess.Close()
@@ -158,9 +190,10 @@ func (s *Session) NewConn() *ConnSession {
 
 	limit := LimitClient(username, false)
 	if !limit {
+		base.Warn("limit is full", username)
 		return nil
 	}
-	ip := AcquireIp(username, macAddr)
+	ip := AcquireIp(username, macAddr, uniqueMac)
 	if ip == nil {
 		LimitClient(username, true)
 		return nil
@@ -187,12 +220,6 @@ func (s *Session) NewConn() *ConnSession {
 		dSess:          &atomic.Value{},
 	}
 
-	// ip 审计
-	if base.Cfg.AuditInterval >= 0 {
-		cSess.IpAuditMap = utils.NewMap("cmap", 0)
-		cSess.IpAuditPool = grpool.NewPool(1, 600)
-	}
-
 	dSess := &DtlsSession{
 		isActive: -1,
 	}
@@ -232,6 +259,7 @@ func (cs *ConnSession) Close() {
 
 		ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
 		LimitClient(cs.Username, true)
+		AddUserActLog(cs)
 	})
 }
 
@@ -335,6 +363,30 @@ func (cs *ConnSession) RateLimit(byt int, isUp bool) error {
 	return cs.Limit.Wait(byt)
 }
 
+func (cs *ConnSession) SetPickCmp(cate, encoding string) (string, bool) {
+	var cmpName string
+	if !base.Cfg.Compression {
+		return cmpName, false
+	}
+	var cmp CmpEncoding
+	switch {
+	// case strings.Contains(encoding, "oc-lz4"):
+	// 	cmpName = "oc-lz4"
+	// 	cmp = Lz4Cmp{}
+	case strings.Contains(encoding, "lzs"):
+		cmpName = "lzs"
+		cmp = LzsgoCmp{}
+	default:
+		return cmpName, false
+	}
+	if cate == "cstp" {
+		cs.CstpPickCmp = cmp
+	} else {
+		cs.DtlsPickCmp = cmp
+	}
+	return cmpName, true
+}
+
 func SToken2Sess(stoken string) *Session {
 	stoken = strings.TrimSpace(stoken)
 	sarr := strings.Split(stoken, "@")
@@ -356,6 +408,20 @@ func Dtls2Sess(did string) *Session {
 	return sessions[token]
 }
 
+func Dtls2CSess(did string) *ConnSession {
+	sessMux.RLock()
+	defer sessMux.RUnlock()
+	token := dtlsIds[did]
+	sess := sessions[token]
+	if sess == nil {
+		return nil
+	}
+
+	sess.mux.RLock()
+	defer sess.mux.RUnlock()
+	return sess.CSess
+}
+
 func Dtls2MasterSecret(did string) string {
 	sessMux.RLock()
 	token := dtlsIds[did]
@@ -378,7 +444,7 @@ func DelSess(token string) {
 	// sessions.Delete(token)
 }
 
-func CloseSess(token string) {
+func CloseSess(token string, code ...uint8) {
 	sessMux.Lock()
 	defer sessMux.Unlock()
 	sess, ok := sessions[token]
@@ -387,7 +453,16 @@ func CloseSess(token string) {
 	}
 
 	delete(sessions, token)
-	sess.CSess.Close()
+	delete(dtlsIds, sess.DtlsSid)
+
+	if sess.CSess != nil {
+		if len(code) > 0 {
+			sess.CSess.UserLogoutCode = code[0]
+		}
+		sess.CSess.Close()
+		return
+	}
+	AddUserActLogBySess(sess)
 }
 
 func CloseCSess(token string) {
@@ -398,14 +473,42 @@ func CloseCSess(token string) {
 		return
 	}
 
-	sess.CSess.Close()
+	if sess.CSess != nil {
+		sess.CSess.Close()
+	}
 }
 
 func DelSessByStoken(stoken string) {
 	stoken = strings.TrimSpace(stoken)
 	sarr := strings.Split(stoken, "@")
 	token := sarr[1]
-	sessMux.Lock()
-	delete(sessions, token)
-	sessMux.Unlock()
+	CloseSess(token, dbdata.UserLogoutBanner)
+}
+
+func AddUserActLog(cs *ConnSession) {
+	ua := dbdata.UserActLog{
+		Username:        cs.Sess.Username,
+		GroupName:       cs.Sess.Group,
+		IpAddr:          cs.IpAddr.String(),
+		RemoteAddr:      cs.RemoteAddr,
+		DeviceType:      cs.Sess.DeviceType,
+		PlatformVersion: cs.Sess.PlatformVersion,
+		Status:          dbdata.UserLogout,
+	}
+	ua.Info = dbdata.UserActLogIns.GetInfoOpsById(cs.UserLogoutCode)
+	dbdata.UserActLogIns.Add(ua, cs.UserAgent)
+}
+
+func AddUserActLogBySess(sess *Session) {
+	ua := dbdata.UserActLog{
+		Username:        sess.Username,
+		GroupName:       sess.Group,
+		IpAddr:          "",
+		RemoteAddr:      sess.RemoteAddr,
+		DeviceType:      sess.DeviceType,
+		PlatformVersion: sess.PlatformVersion,
+		Status:          dbdata.UserLogout,
+	}
+	ua.Info = dbdata.UserActLogIns.GetInfoOpsById(dbdata.UserLogoutBanner)
+	dbdata.UserActLogIns.Add(ua, sess.UserAgent)
 }

+ 25 - 0
anylink/server/sessdata/session_test.go

@@ -1,8 +1,11 @@
 package sessdata
 
 import (
+	"fmt"
 	"testing"
+	"time"
 
+	"github.com/bjdgyc/anylink/base"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -22,11 +25,15 @@ func TestConnSession(t *testing.T) {
 	preData(tmp)
 	defer cleardata(tmp)
 
+	time.Sleep(time.Second * 10)
+
 	sess := NewSession("")
+	sess.Username = "user-test"
 	sess.Group = "group1"
 	sess.MacAddr = "00:15:5d:50:14:43"
 
 	cSess := sess.NewConn()
+	base.Info("cSess", cSess)
 
 	err := cSess.RateLimit(100, true)
 	ast.Nil(err)
@@ -34,5 +41,23 @@ func TestConnSession(t *testing.T) {
 	err = cSess.RateLimit(200, false)
 	ast.Nil(err)
 	ast.Equal(cSess.BandwidthDown.Load(), uint32(200))
+
+	var (
+		cmpName string
+		ok      bool
+	)
+	base.Cfg.Compression = true
+
+	cmpName, ok = cSess.SetPickCmp("cstp", "oc-lz4,lzs")
+	fmt.Println(cmpName, ok)
+	ast.True(ok)
+	ast.Equal(cmpName, "lzs")
+	cmpName, ok = cSess.SetPickCmp("dtls", "lzs")
+	ast.True(ok)
+	ast.Equal(cmpName, "lzs")
+	cmpName, ok = cSess.SetPickCmp("dtls", "test")
+	ast.False(ok)
+	ast.Equal(cmpName, "")
+
 	cSess.Close()
 }

+ 1 - 0
anylink/server/sessdata/start.go

@@ -4,4 +4,5 @@ func Start() {
 	initIpPool()
 	checkSession()
 	saveStatsInfo()
+	CloseUserLimittimeSession()
 }

+ 7 - 0
anylink/systemd/anylink.service

@@ -11,5 +11,12 @@ Restart=on-failure
 RestartSec=5s
 ExecStart=/usr/local/anylink-deploy/anylink --conf=/usr/local/anylink-deploy/conf/server.toml
 
+# systemd older than v236
+# ExecStart=/bin/bash -c 'exec /usr/local/anylink-deploy/anylink --conf=/usr/local/anylink-deploy/conf/server.toml >> /usr/local/anylink-deploy/log/anylink.log 2>&1'
+
+
+StandardOutput=file:/usr/local/anylink-deploy/log/anylink.log
+StandardError=file:/usr/local/anylink-deploy/log/anylink.log
+
 [Install]
 WantedBy=multi-user.target

+ 1 - 0
anylink/web/package.json

@@ -12,6 +12,7 @@
     "core-js": "^3.6.5",
     "echarts": "^4.9.0",
     "element-ui": "^2.4.5",
+    "qs": "^6.11.1",
     "vue": "^2.6.11",
     "vue-count-to": "^1.0.13",
     "vue-router": "^3.5.2"

BIN
anylink/web/public/批量添加用户模版.xlsx


+ 321 - 0
anylink/web/src/components/audit/Access.vue

@@ -0,0 +1,321 @@
+<template>
+  <div>
+<el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="search-form">
+        <el-form-item label="用户名:" prop="username">
+          <el-input size="mini" v-model="searchForm.username" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item>
+        <el-form-item label="源IP地址:" prop="src">
+          <el-input size="mini" v-model="searchForm.src" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item>    
+        <el-form-item label="目的IP地址:" prop="dst">
+          <el-input size="mini" v-model="searchForm.dst" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item> 
+        <el-form-item label="目的端口:" prop="dst_port">
+          <el-input size="mini" v-model="searchForm.dst_port" clearable style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item> 
+        <el-form-item label="访问协议:">
+            <el-select size="mini" v-model="searchForm.access_proto" clearable placeholder="请选择" style="width: 100px">
+                    <el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value">
+                    </el-option>
+            </el-select>           
+        </el-form-item>  
+        <el-form-item label="日期范围:">
+            <el-date-picker
+                v-model="searchForm.date"
+                type="datetimerange"
+                value-format="yyyy-MM-dd HH:mm:ss"
+                size="mini"
+                align="left"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="['00:00:00', '23:59:59']">
+            </el-date-picker>
+        </el-form-item>
+        <el-form-item label="详情:">
+          <el-input size="mini" v-model="searchForm.info" placeholder="请输入关键字" clearable style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item>         
+        <el-form-item>
+          <el-button
+              size="mini"
+              type="primary"
+              icon="el-icon-search"
+              @click="handleSearch">搜索
+          </el-button>
+          <el-button
+              size="mini"
+              icon="el-icon-refresh"
+              @click="rest">重置搜索
+          </el-button>
+          <el-button
+              size="mini"
+              icon="el-icon-download"
+              @click="handleExport">导出
+          </el-button>          
+        </el-form-item>
+      </el-form>
+
+      <el-table
+          ref="multipleTable"
+          :data="tableData"
+          v-loading="loading"
+          element-loading-text="玩命加载中"
+          element-loading-spinner="el-icon-loading"
+          :default-sort="{ prop: 'id', order: 'descending' }"
+          @sort-change="sortChange" 
+          :header-cell-style="{backgroundColor:'#fcfcfc'}"
+          border>
+
+        <el-table-column
+            prop="id"
+            label="ID"
+            sortable="custom"
+            width="100">
+        </el-table-column>
+
+        <el-table-column
+            prop="username"
+            label="用户名"
+            width="140">
+        </el-table-column>
+
+        <el-table-column
+            prop="src"
+            label="源IP地址"
+            width="140">
+        </el-table-column>
+
+        <el-table-column
+            prop="dst"
+            label="目的IP地址"
+            width="140">
+        </el-table-column>
+
+        <el-table-column
+            prop="dst_port"
+            label="目的端口"
+            width="85">
+        </el-table-column>
+
+        <el-table-column
+            prop="access_proto"
+            label="访问协议"
+            width="80"
+            :formatter="protoFormat">
+        </el-table-column>
+
+        <el-table-column
+            prop="info"
+            label="详情">
+        </el-table-column>        
+
+        <el-table-column
+            prop="created_at"
+            label="创建时间"
+            width="160"
+            :formatter="tableDateFormat">
+        </el-table-column>
+      </el-table>
+
+      <div class="sh-20"></div>
+
+      <el-pagination
+          background
+          layout="prev, pager, next"
+          :pager-count="11"
+          :current-page.sync="currentPage"
+          @current-change="pageChange"
+          :total="count">
+      </el-pagination>
+  </div>
+</template>
+
+<script>
+import axios from "axios";
+
+export default {    
+  name: "auditAccess",
+  mixins: [],
+  data() {
+    return {
+      tableData: [],
+      count: 10,
+      currentPage: 1,
+      idSort: 1,
+      activeName: "first",
+      accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"], 
+      defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]},
+      searchForm: {},
+      access_proto: [
+            { text: 'UDP', value: '1' },
+            { text: 'TCP', value: '2' },
+            { text: 'HTTPS', value: '3' },
+            { text: 'HTTP', value: '4' },
+      ],
+      maxExportNum: 1000000,
+      loading: false,
+      rules: {
+        username: [
+          {max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
+        ],
+        src: [
+          {  message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
+        ],        
+        dst: [
+          { message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
+        ],               
+      },            
+    }
+  },
+  watch: {
+    idSort: {
+        handler(newValue, oldValue) {
+            if (newValue != oldValue) {
+                this.getData(1);
+            }
+        },
+    },
+  },  
+  methods: {
+    setSearchData() {
+        this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm));
+    },    
+    handleSearch() {
+      this.$refs["searchForm"].validate((valid) => {
+        if (!valid) {
+          console.log('error submit!!');
+          return false;
+        }
+        this.getData(1)
+      })          
+    },
+    searchEnterFun(e) {
+        var keyCode = window.event ? e.keyCode : e.which;
+        if (keyCode == 13) {
+            this.handleSearch()
+        }
+    },        
+    getData(p) {
+      this.loading = true
+      if (! this.searchForm.date) {
+        this.searchForm.date = ["", ""];
+      }
+      this.searchForm.sort = this.idSort     
+      axios.get('/set/audit/list', {
+        params: {
+          page: p,
+          search: this.searchForm,
+        }
+      }).then(resp => {
+        var data = resp.data.data
+        console.log(data);        
+        this.tableData = data.datas;
+        this.count = data.count
+        this.loading = false
+        this.currentPage = p;
+      }).catch(error => {
+        this.$message.error('哦,请求出错');
+        console.log(error);
+      });
+    },
+    pageChange(p) {     
+        this.getData(p)
+    },
+    handleExport() {
+      if (this.count > this.maxExportNum) {
+        var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
+           return s+','
+        })
+        this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出");
+        return ;
+      }
+      if (! this.searchForm.date) {
+        this.searchForm.date = ["", ""];
+      }
+      const exporting = this.$loading({
+            lock: true,
+            text: '玩命导出中,请稍等片刻...',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+      });
+      axios.get('/set/audit/export', {
+        params: {
+          search: this.searchForm,
+        }
+      }).then(resp => {
+        var rdata = resp.data
+        if (rdata.code && rdata.code != 0) {
+            exporting.close();
+            this.$message.error(rdata.msg);
+            return ;
+        }
+        exporting.close();
+        this.$message.success("成功导出CSV文件")
+        let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata
+        this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`)
+      }).catch(error => {
+        exporting.close();
+        this.$message.error('哦,请求出错');
+        console.log(error);
+      });
+    },
+    createDownLoadClick(content, fileName) {
+        const link = document.createElement('a')
+        link.href = encodeURI(content)
+        link.download = fileName
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+    },    
+    protoFormat(row) {
+        var access_proto = row.access_proto
+        if (row.access_proto == 0) {
+            switch (row.protocol) {
+                case 6: access_proto = 2; break;
+                case 17: access_proto = 1; break;
+            }
+        }
+        return this.accessProtoArr[access_proto]
+    },
+    rest() {
+        console.log("rest");
+        this.setSearchData();
+        this.handleSearch();
+    }, 
+    validateIP(rule, value, callback) {
+        if (value === '' || typeof value === 'undefined' || value == null) {
+            callback()
+        } else {
+            const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
+            if ((!reg.test(value)) && value !== '') {
+            callback(new Error('请输入正确的IP地址'))
+            } else {
+            callback()
+            }
+        }
+    }, 
+    sortChange(column) {
+        let { order } = column;
+        if (order === 'ascending') {
+            this.idSort = 2;
+        } else {
+            this.idSort = 1;
+        }
+    },            
+  },
+}
+</script>
+
+<style scoped>
+.el-form-item {
+    margin-bottom: 5px;
+}
+.el-table {
+    font-size: 12px;
+}
+.search-form >>> .el-form-item__label {
+  font-size: 12px;
+}
+/deep/ .el-table th {
+    padding: 5px 0;
+}
+</style>

+ 263 - 0
anylink/web/src/components/audit/ActLog.vue

@@ -0,0 +1,263 @@
+<template>
+  <div>
+      <el-form  :model="searchForm" ref="searchForm" :inline="true" class="search-form">
+        <el-form-item>
+          <el-input size="mini" v-model="searchForm.username" clearable placeholder="请输入用户名" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
+        </el-form-item>
+        <el-form-item>
+                <el-date-picker
+                    v-model="searchForm.sdate"
+                    type="date"
+                    size="mini"
+                    placeholder="开始日期"
+                    format="yyyy-MM-dd"
+                    value-format="yyyy-MM-dd"  
+                    style="width: 130px"
+                >
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item>    
+                <el-date-picker
+                    v-model="searchForm.edate"
+                    type="date"
+                    size="mini"
+                    placeholder="结束日期"
+                    format="yyyy-MM-dd"
+                    value-format="yyyy-MM-dd"  
+                    style="width: 130px"              
+                >
+            </el-date-picker>
+        </el-form-item>
+        <el-form-item >
+            <el-select size="mini" v-model="searchForm.status" clearable placeholder="操作类型" style="width: 130px">
+                    <el-option v-for="(item,index) in statusOps" :key="index" :label="item.value" :value="item.key+1">
+                    </el-option>
+            </el-select>           
+        </el-form-item>
+        <el-form-item>
+            <el-select size="mini" v-model="searchForm.os" clearable placeholder="操作系统" style="width: 130px">
+                    <el-option v-for="(value,item,index) in osOps" :key="index" :label="value" :value="item+1">
+                    </el-option>
+            </el-select>           
+        </el-form-item>        
+        <el-form-item>
+          <el-button
+              size="mini"
+              type="primary"
+              icon="el-icon-search"
+              @click="handleSearch">搜索
+          </el-button>
+          <el-button
+              size="mini"
+              icon="el-icon-refresh"
+              @click="rest">重置搜索
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table
+          ref="multipleTable"
+          :data="tableData"
+          :default-sort="{ prop: 'id', order: 'descending' }"
+          @sort-change="sortChange"
+          :header-cell-style="{backgroundColor:'#fcfcfc'}"
+          border>
+
+        <el-table-column
+            prop="id"
+            label="ID"
+            sortable="custom"
+            width="100">
+        </el-table-column>
+        <el-table-column
+            prop="username"
+            label="用户名"
+            width="140">
+        </el-table-column>
+        <el-table-column
+            prop="group_name"
+            label="登陆组"
+            width="100">
+        </el-table-column>
+        <el-table-column
+            prop="status"
+            label="操作类型"
+            width="92">
+                <template slot-scope="{ row }">
+                    <span v-for="(item, index) in statusOps" :key="index">
+                        <el-tag size="small" v-if="row.status == item.key" disable-transitions :type="item.tag">{{item.value}}</el-tag>
+                    </span>
+                </template>            
+        </el-table-column> 
+        <el-table-column
+            prop="info"
+            label="操作详情"
+            min-width="200">
+        </el-table-column> 
+        <el-table-column
+            prop="created_at"
+            label="操作时间"
+            width="150"
+            :formatter="tableDateFormat">
+        </el-table-column>                                          
+        <el-table-column
+            prop="os"
+            label="操作系统"
+            min-width="210">
+                <template slot-scope="{ row }">
+                    <span v-for="(value, item, index) in osOps" :key="index">
+                    {{ row.os == item? value: "" }}
+                    </span>
+                    <div class="sub_txt">型号: 
+                        <span v-if="row.device_type != ''">{{ row.device_type }} / {{ row.platform_version }}</span>
+                        <span v-else> - </span>
+                    </div>
+                </template>             
+        </el-table-column>         
+        <el-table-column
+            prop="client"
+            label="客户端"
+            width="150">
+                <template slot-scope="{ row }">
+                    <span v-for="(value, item, index) in clientOps" :key="index">
+                    {{ row.client == item? value: "" }}
+                    </span>
+                    {{ row.version }} 
+                </template>                           
+        </el-table-column>  
+        <el-table-column
+            prop="ip_addr"
+            label="内网IP"
+            width="120">
+        </el-table-column>
+        <el-table-column
+            prop="remote_addr"
+            label="外网IP"
+            width="120">
+        </el-table-column>                                                  
+      </el-table>
+      <div class="sh-20"></div>
+        <el-pagination
+            background
+            layout="prev, pager, next"  
+            :pager-count="11"
+            @current-change="pageChange"
+            :current-page="page"
+            :total="count">
+        </el-pagination>
+</div>
+</template>
+
+<script>
+import axios from "axios";
+
+export default {
+  name: "List",
+  components: {},
+  mixins: [],
+  created() {
+    this.$emit('update:route_path', this.$route.path)
+    this.$emit('update:route_name', ['用户信息', '登入日志'])
+  },
+  data() {
+    return {
+      page: 1,
+      grouNames: [],
+      tableData: [],
+      idSort: 1,
+      count: 10,
+      searchForm: {username:'', sdate:'', edate:'', status:'', os:''},
+      statusOps:[],
+      osOps:[],
+      clientOps:[],                  
+    }
+  },
+  watch: {
+    idSort: {
+        handler(newValue, oldValue) {
+            if (newValue != oldValue) {
+                this.getData(1);
+            }
+        },
+    },
+  },
+  methods: {    
+    handleSearch() {
+      this.getData(1)
+    },
+    pageChange(p) {
+      this.getData(p)
+    },
+    searchEnterFun(e) {
+        var keyCode = window.event ? e.keyCode : e.which;
+        if (keyCode == 13) {
+            this.handleSearch()
+        }
+    },    
+    getData(page) {
+      console.log(this.searchForm)
+      this.page = page
+      axios.get('/set/audit/act_log_list', {
+        params: {
+          page: page,
+          username: this.searchForm.username || '',
+          sdate: this.searchForm.sdate || '',
+          edate: this.searchForm.edate || '',
+          status: this.searchForm.status || '',
+          os: this.searchForm.os || '',
+          sort: this.idSort,
+        }
+      }).then(resp => {
+        var data = resp.data.data
+        console.log(data);
+        this.tableData = data.datas;
+        this.count = data.count
+        this.statusOps = data.statusOps
+        this.osOps = data.osOps
+        this.clientOps = data.clientOps
+      }).catch(error => {
+        this.$message.error('哦,请求出错');
+        console.log(error);
+      });
+    },
+    rest() {
+        console.log("rest");
+        this.searchForm.username = "";
+        this.searchForm.sdate = "";
+        this.searchForm.edate = "";
+        this.searchForm.status = "";
+        this.searchForm.os = "";
+        this.handleSearch();
+    },
+    sortChange(column) {
+        let { order } = column;
+        if (order === 'ascending') {
+            this.idSort = 2;
+        } else {
+            this.idSort = 1;
+        }
+    },    
+  }
+}
+</script>
+
+<style scoped>
+.el-form-item {
+    margin-bottom: 8px;
+}
+.el-table {
+    font-size: 12px;
+}
+.search-form >>> .el-form-item__label {
+  font-size: 12px;
+}
+/deep/ .el-table th {
+    padding: 5px 0;
+}
+/deep/ .el-table td {
+    padding: 5px 0;
+}
+.sub_txt {
+    color: #88909B;
+}
+</style>

+ 6 - 1
anylink/web/src/layout/LayoutAside.vue

@@ -56,7 +56,7 @@
       <el-menu-item index="/admin/group/list">用户组列表</el-menu-item>
     </el-submenu>
 
-    <el-submenu index="3">
+    <el-submenu index="4">
       <template slot="title">
         <i class="el-icon-s-order"></i>
         <span slot="title">调试信息</span>
@@ -91,4 +91,9 @@ export default {
 .layout-menu {
   height: 100%;
 }
+
+.el-menu-item a {
+  display: block;
+  color: #fff;
+}
 </style>

+ 3 - 0
anylink/web/src/pages/Home.vue

@@ -230,6 +230,9 @@ export default {
                 case "mem": this.formatMem(data); break;
             }
         }).catch(error => {
+            if (error.response.status === 401) {
+               return ;
+            }            
             this.$message.error('哦,请求出错');
             console.log(error);
         });

+ 116 - 13
anylink/web/src/pages/group/List.vue

@@ -191,9 +191,10 @@
                     <template slot="append">BYTE/S</template>
                 </el-input>
                 </el-form-item>
-                <el-form-item label="本地网络" prop="allow_lan">
+                <el-form-item label="排除本地网络" prop="allow_lan">
                 <el-switch
-                    v-model="ruleForm.allow_lan">
+                    v-model="ruleForm.allow_lan"
+                    active-text="开启后 用户本地所在网段将不通过anylink加密传输">
                 </el-switch>
                 </el-form-item>
 
@@ -235,23 +236,23 @@
                         <el-radio label="ldap" border>LDAP</el-radio>
                     </el-radio-group>
                 </el-form-item>   
-                <templete v-if="ruleForm.auth.type == 'radius'">
+                <template v-if="ruleForm.auth.type == 'radius'">
                   <el-form-item label="服务器地址" prop="auth.radius.addr" :rules="this.ruleForm.auth.type== 'radius' ? this.rules['auth.radius.addr'] : [{ required: false }]">
                       <el-input v-model="ruleForm.auth.radius.addr" placeholder="例如 ip:1812"></el-input>
                   </el-form-item>                
                   <el-form-item label="密钥" prop="auth.radius.secret" :rules="this.ruleForm.auth.type== 'radius' ? this.rules['auth.radius.secret'] : [{ required: false }]">
                       <el-input v-model="ruleForm.auth.radius.secret" placeholder=""></el-input>
                   </el-form-item>               
-                </templete>
+                </template>
 
-                <templete v-if="ruleForm.auth.type == 'ldap'">
+                <template v-if="ruleForm.auth.type == 'ldap'">
                   <el-form-item label="服务器地址" prop="auth.ldap.addr" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.addr'] : [{ required: false }]">
                       <el-input v-model="ruleForm.auth.ldap.addr" placeholder="例如 ip:389 / 域名:389"></el-input>    
                   </el-form-item> 
                   <el-form-item label="开启TLS" prop="auth.ldap.tls">
                     <el-switch v-model="ruleForm.auth.ldap.tls"></el-switch>                      
                   </el-form-item>
-                  <el-form-item label="管理员账号" prop="auth.ldap.bind_name" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.bind_name'] : [{ required: false }]">
+                  <el-form-item label="管理员 DN" prop="auth.ldap.bind_name" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.bind_name'] : [{ required: false }]">
                     <el-input v-model="ruleForm.auth.ldap.bind_name" placeholder="例如 CN=bindadmin,DC=abc,DC=COM"></el-input>
                   </el-form-item>
                   <el-form-item label="管理员密码" prop="auth.ldap.bind_pwd" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.bind_pwd'] : [{ required: false }]">
@@ -259,14 +260,17 @@
                   </el-form-item>                                                
                   <el-form-item label="Base DN" prop="auth.ldap.base_dn" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.base_dn'] : [{ required: false }]">
                     <el-input v-model="ruleForm.auth.ldap.base_dn" placeholder="例如 DC=abc,DC=com"></el-input>
-                  </el-form-item>  
+                  </el-form-item>
+                  <el-form-item label="用户对象类" prop="auth.ldap.object_class" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.object_class'] : [{ required: false }]">
+                    <el-input v-model="ruleForm.auth.ldap.object_class" placeholder="例如 person / user / posixAccount"></el-input>
+                  </el-form-item>                  
                   <el-form-item label="用户唯一ID" prop="auth.ldap.search_attr" :rules="this.ruleForm.auth.type== 'ldap' ? this.rules['auth.ldap.search_attr'] : [{ required: false }]">
-                    <el-input v-model="ruleForm.auth.ldap.search_attr" placeholder="例如 sAMAccountName 或 uid"></el-input>
+                    <el-input v-model="ruleForm.auth.ldap.search_attr" placeholder="例如 sAMAccountName / uid / cn"></el-input>
                   </el-form-item>    
                   <el-form-item label="受限用户组" prop="auth.ldap.member_of">
                     <el-input v-model="ruleForm.auth.ldap.member_of" placeholder="选填, 只允许指定组登入, 例如 CN=HomeWork,DC=abc,DC=com"></el-input>
                   </el-form-item>                                                                      
-                </templete>                 
+                </template>                 
             </el-tab-pane>  
 
             <el-tab-pane label="路由设置" name="route">
@@ -359,13 +363,36 @@
                 </el-form-item>
             </el-tab-pane>
             <el-form-item>
-            <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
-            <el-button @click="closeDialog">取消</el-button>
+                <templete v-if="activeTab == 'authtype' && ruleForm.auth.type != 'local'">
+                    <el-button @click="openAuthLoginDialog()" style="margin-right:10px">测试登录</el-button>
+                </templete>
+                <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
+                <el-button @click="closeDialog">取消</el-button>
             </el-form-item>
           </el-tabs>
         </el-form> 
     </el-dialog>
-
+    <!--测试用户登录弹出框-->
+    <el-dialog
+        :close-on-click-modal="false"
+        title="测试用户登录"
+        :visible.sync="authLoginDialog"
+        width="600px"
+        custom-class="valgin-dialog"
+        center>
+        <el-form :model="authLoginForm" :rules="authLoginRules" ref="authLoginForm" label-width="100px">
+            <el-form-item label="账号" prop="name">
+                <el-input v-model="authLoginForm.name" ref="authLoginFormName" @keydown.enter.native="testAuthLogin"></el-input>
+            </el-form-item>
+            <el-form-item label="密码" prop="pwd">
+                <el-input type="password" v-model="authLoginForm.pwd" @keydown.enter.native="testAuthLogin"></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="testAuthLogin()" :loading="authLoginLoading">登录</el-button>
+                <el-button @click="authLoginDialog = false">取 消</el-button>
+            </el-form-item>
+        </el-form>
+    </el-dialog> 
   </div>
 </template>
 
@@ -399,6 +426,7 @@ export default {
                       addr:"", 
                       tls:false,
                       base_dn:"",
+                      object_class:"person",
                       search_attr:"sAMAccountName",
                       member_of:"",
                       bind_name:"",
@@ -415,6 +443,21 @@ export default {
         link_acl: [],
         auth : {},
       },
+      authLoginDialog : false,
+      authLoginLoading : false,
+      authLoginForm : {
+        name : "",
+        pwd : "",
+      },   
+      authLoginRules: {
+        name: [
+          {required: true, message: '请输入账号', trigger: 'blur'},
+        ],
+        pwd: [
+          {required: true, message: '请输入密码', trigger: 'blur'},
+          {min: 6, message: '长度至少 6 个字符', trigger: 'blur'}
+        ],
+      },         
       rules: {
         name: [
           {required: true, message: '请输入组名', trigger: 'blur'},
@@ -437,7 +480,7 @@ export default {
           {required: true, message: '请输入服务器地址(含端口)', trigger: 'blur'}
         ],  
         "auth.ldap.bind_name": [
-          {required: true, message: '请输入管理员账号', trigger: 'blur'}
+          {required: true, message: '请输入管理员 DN', trigger: 'blur'}
         ],
         "auth.ldap.bind_pwd": [
           {required: true, message: '请输入管理员密码', trigger: 'blur'}
@@ -445,6 +488,9 @@ export default {
         "auth.ldap.base_dn": [
           {required: true, message: '请输入Base DN值', trigger: 'blur'}
         ],
+        "auth.ldap.object_class": [
+          {required: true, message: '请输入用户对象类', trigger: 'blur'}
+        ],        
         "auth.ldap.search_attr": [
           {required: true, message: '请输入用户唯一ID', trigger: 'blur'}
         ],                                       
@@ -457,6 +503,9 @@ export default {
         this.ruleForm.auth = JSON.parse(JSON.stringify(this.defAuth));
         return ;
       }
+      if (row.auth.type == "ldap" && ! row.auth.ldap.object_class) {
+        row.auth.ldap.object_class = this.defAuth.ldap.object_class;
+      }      
       this.ruleForm.auth = Object.assign(JSON.parse(JSON.stringify(this.defAuth)), row.auth);
     },
     handleDel(row) {
@@ -549,6 +598,44 @@ export default {
         });
       });
     },
+    testAuthLogin() {
+        this.$refs["authLoginForm"].validate((valid) => {
+            if (!valid) {
+                console.log('error submit!!');
+                return false;
+            }        
+            this.authLoginLoading = true;
+            axios.post('/group/auth_login', {name:this.authLoginForm.name,
+                                            pwd:this.authLoginForm.pwd,
+                                            auth:this.ruleForm.auth}).then(resp => {
+                    const rdata = resp.data;
+                    if (rdata.code === 0) {
+                        this.$message.success("登录成功");
+                    } else {
+                        this.$message.error(rdata.msg);                
+                    }
+                    this.authLoginLoading = false;
+                    console.log(rdata);
+                }).catch(error => {
+                    this.$message.error('哦,请求出错');
+                    console.log(error);
+                    this.authLoginLoading = false;
+            });
+        });
+    },
+    openAuthLoginDialog() {
+      this.$refs["ruleForm"].validate((valid) => {
+        if (!valid) {
+          console.log('error submit!!');
+          return false;
+        }        
+        this.authLoginDialog = true;
+        // set authLoginFormName focus
+        this.$nextTick(() => {
+            this.$refs['authLoginFormName'].focus();
+        });
+      });
+    },
     resetForm(formName) {
       this.$refs[formName].resetFields();
     },
@@ -598,4 +685,20 @@ export default {
 .el-select {
   width: 80px;
 }
+
+::v-deep .valgin-dialog{
+    display: flex;
+    flex-direction: column;
+    margin:0 !important;
+    position:absolute;
+    top:50%;
+    left:50%;
+    transform:translate(-50%,-50%);
+    max-height:calc(100% - 30px);
+    max-width:calc(100% - 30px);
+}
+::v-deep  .valgin-dialog .el-dialog__body{
+    flex:1;
+    overflow: auto;
+}
 </style>

+ 39 - 278
anylink/web/src/pages/set/Audit.vue

@@ -1,300 +1,61 @@
 <template>
   <div>
-    <el-card>
-      <el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="form-inner-error">
-        <el-form-item label="用户名:" prop="username">
-          <el-input size="small" v-model="searchForm.username" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
-        </el-form-item>
-        <el-form-item label="源IP地址:" prop="src">
-          <el-input size="small" v-model="searchForm.src" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
-        </el-form-item>    
-        <el-form-item label="目的IP地址:" prop="dst">
-          <el-input size="small" v-model="searchForm.dst" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
-        </el-form-item> 
-        <el-form-item label="目的端口:" prop="dst_port">
-          <el-input size="small" v-model="searchForm.dst_port" style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input>
-        </el-form-item> 
-        <el-form-item label="访问协议:">
-            <el-select size="small" v-model="searchForm.access_proto" style="width: 100px">
-                    <el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value">
-                    </el-option>
-            </el-select>           
-        </el-form-item>  
-        <div>
-        <el-form-item label="日期范围:">
-            <el-date-picker
-                v-model="searchForm.date"
-                type="datetimerange"
-                size="small"
-                value-format="yyyy-MM-dd HH:mm:ss"
-                range-separator="~"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期">
-            >
-            </el-date-picker>
-        </el-form-item>
-        <el-form-item label="详情:">
-          <el-input size="small" v-model="searchForm.info" placeholder="请输入关键字" style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input>
-        </el-form-item>         
-        <el-form-item>
-          <el-button
-              size="small"
-              type="primary"
-              icon="el-icon-search"
-              @click="handleSearch">搜索
-          </el-button>
-          <el-button
-              size="small"
-              icon="el-icon-refresh"
-              @click="rest">重置搜索
-          </el-button>
-          <el-button
-              size="small"
-              icon="el-icon-download"
-              @click="handleExport">导出
-          </el-button>          
-        </el-form-item>
-        </div>
-      </el-form>
-
-      <el-table
-          ref="multipleTable"
-          :data="tableData"
-          v-loading="loading"
-          element-loading-text="玩命加载中"
-          element-loading-spinner="el-icon-loading"
-          border>
-
-        <el-table-column
-            prop="id"
-            label="ID"
-            width="100">
-        </el-table-column>
-
-        <el-table-column
-            prop="username"
-            label="用户名"
-            width="140">
-        </el-table-column>
-
-        <el-table-column
-            prop="src"
-            label="源IP地址"
-            width="140">
-        </el-table-column>
-
-        <el-table-column
-            prop="dst"
-            label="目的IP地址"
-            width="140">
-        </el-table-column>
-
-        <el-table-column
-            prop="dst_port"
-            label="目的端口"
-            width="85">
-        </el-table-column>
-
-        <el-table-column
-            prop="access_proto"
-            label="访问协议"
-            width="80"
-            :formatter="protoFormat">
-        </el-table-column>
-
-        <el-table-column
-            prop="info"
-            label="详情">
-        </el-table-column>        
-
-        <el-table-column
-            prop="created_at"
-            label="创建时间"
-            width="150"
-            :formatter="tableDateFormat">
-        </el-table-column>
-      </el-table>
-
-      <div class="sh-20"></div>
-
-      <el-pagination
-          background
-          layout="prev, pager, next"
-          :pager-count="11"
-          @current-change="pageChange"
-          :total="count">
-      </el-pagination>
-
-    </el-card>
-
+    <el-card>    
+    <el-tabs v-model="activeName" @tab-click="handleClick">
+        <el-tab-pane label="用户活动日志" name="act_log">
+            <AuditActLog ref="auditActLog"></AuditActLog>
+        </el-tab-pane>        
+        <el-tab-pane label="用户访问日志" name="access_audit">
+            <AuditAccess ref="auditAccess"></AuditAccess>
+        </el-tab-pane>
+    </el-tabs>
+    </el-card>      
   </div>
 </template>
 
 <script>
-import axios from "axios";
+import AuditAccess from "../../components/audit/Access";
+import AuditActLog from "../../components/audit/ActLog";
 
 export default {
   name: "Audit",
-  components: {},
+  components:{
+    AuditAccess,
+    AuditActLog
+  },
   mixins: [],
+  mounted() {    
+    this.upTab();
+  },  
   created() {
     this.$emit('update:route_path', this.$route.path)
-    this.$emit('update:route_name', ['基础信息', '审计日志'])
-  },
-  mounted() {    
-    this.getData(1)
-    this.setSearchData()
+    this.$emit('update:route_name', ['基础信息', '审计日志'])        
   },
   data() {
     return {
-      tableData: [],
-      count: 10,
-      nowIndex: 0,
-      accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"], 
-      defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]},
-      searchForm: {},
-      access_proto: [
-            { text: '请选择', value: '' },
-            { text: 'UDP', value: '1' },
-            { text: 'TCP', value: '2' },
-            { text: 'HTTPS', value: '3' },
-            { text: 'HTTP', value: '4' },
-      ],
-      maxExportNum: 1000000,
-      loading: false,
-      rules: {
-        username: [
-          {max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
-        ],
-        src: [
-          {  message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
-        ],        
-        dst: [
-          { message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
-        ],               
-      },            
+      activeName: "act_log",
     }
   },
-  methods: {
-    setSearchData() {
-        this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm));
-    },    
-    handleSearch() {
-      this.$refs["searchForm"].validate((valid) => {
-        if (!valid) {
-          console.log('error submit!!');
-          return false;
-        }
-        this.getData(1)
-      })          
-    },
-    searchEnterFun(e) {
-        var keyCode = window.event ? e.keyCode : e.which;
-        if (keyCode == 13) {
-            this.handleSearch()
-        }
-    },        
-    getData(p) {
-      this.loading = true
-      if (! this.searchForm.date) {
-        this.searchForm.date = ["", ""];
-      }        
-      axios.get('/set/audit/list', {
-        params: {
-          page: p,
-          search: this.searchForm,
-        }
-      }).then(resp => {
-        var data = resp.data.data
-        console.log(data);
-        this.tableData = data.datas;
-        this.count = data.count
-        this.loading = false
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
-    },
-    pageChange(p) {
-      this.getData(p)
-    },
-    handleExport() {
-      if (this.count > this.maxExportNum) {
-        var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
-           return s+','
-        })
-        this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出");
-        return ;
+  methods: {  
+    upTab() {
+      var tabname = this.$route.query.tabname
+      if (tabname) {
+        this.activeName = tabname
       }
-      if (! this.searchForm.date) {
-        this.searchForm.date = ["", ""];
-      }
-      const exporting = this.$loading({
-            lock: true,
-            text: '玩命导出中,请稍等片刻...',
-            spinner: 'el-icon-loading',
-            background: 'rgba(0, 0, 0, 0.7)'
-      });
-      axios.get('/set/audit/export', {
-        params: {
-          search: this.searchForm,
-        }
-      }).then(resp => {
-        var rdata = resp.data
-        if (rdata.code && rdata.code != 0) {
-            exporting.close();
-            this.$message.error(rdata.msg);
-            return ;
-        }
-        exporting.close();
-        this.$message.success("成功导出CSV文件")
-        let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata
-        this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`)
-      }).catch(error => {
-        exporting.close();
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
-    },
-    createDownLoadClick(content, fileName) {
-        const link = document.createElement('a')
-        link.href = encodeURI(content)
-        link.download = fileName
-        document.body.appendChild(link)
-        link.click()
-        document.body.removeChild(link)
-    },    
-    protoFormat(row) {
-        var access_proto = row.access_proto
-        if (row.access_proto == 0) {
-            switch (row.protocol) {
-                case 6: access_proto = 2; break;
-                case 17: access_proto = 1; break;
-            }
-        }
-        return this.accessProtoArr[access_proto]
+      this.handleClick(this.activeName)      
     },
-    rest() {
-        console.log("rest");
-        this.setSearchData();
-        this.handleSearch();
-    }, 
-    validateIP(rule, value, callback) {
-        if (value === '' || typeof value === 'undefined' || value == null) {
-            callback()
-        } else {
-            const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
-            if ((!reg.test(value)) && value !== '') {
-            callback(new Error('请输入正确的IP地址'))
-            } else {
-            callback()
-            }
+    handleClick() {
+        switch (this.activeName) {
+        case "access_audit":
+            this.$refs.auditAccess.setSearchData()
+            this.$refs.auditAccess.getData(1)            
+            break
+        case "act_log":
+            this.$refs.auditActLog.getData(1)
+            break          
         }
-    },        
-  },
+        this.$router.push({path: this.$route.path, query: {tabname: this.activeName}})
+    },    
+  }
 }
 </script>
-
-<style scoped>
-
-</style>

+ 456 - 112
anylink/web/src/pages/set/Other.vue

@@ -2,7 +2,13 @@
   <el-card>
     <el-tabs v-model="activeName" @tab-click="handleClick">
       <el-tab-pane label="邮件配置" name="dataSmtp">
-        <el-form :model="dataSmtp" ref="dataSmtp" :rules="rules" label-width="100px" class="tab-one">
+        <el-form
+          :model="dataSmtp"
+          ref="dataSmtp"
+          :rules="rules"
+          label-width="100px"
+          class="tab-one"
+        >
           <el-form-item label="服务器地址" prop="host">
             <el-input v-model="dataSmtp.host"></el-input>
           </el-form-item>
@@ -13,7 +19,11 @@
             <el-input v-model="dataSmtp.username"></el-input>
           </el-form-item>
           <el-form-item label="密码" prop="password">
-            <el-input type="password" v-model="dataSmtp.password" placeholder="密码为空则不修改"></el-input>
+            <el-input
+              type="password"
+              v-model="dataSmtp.password"
+              placeholder="密码为空则不修改"
+            ></el-input>
           </el-form-item>
           <el-form-item label="加密类型" prop="encryption">
             <el-radio-group v-model="dataSmtp.encryption">
@@ -26,90 +36,252 @@
             <el-input v-model="dataSmtp.from"></el-input>
           </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="submitForm('dataSmtp')">保存</el-button>
+            <el-button type="primary" @click="submitForm('dataSmtp')"
+              >保存</el-button
+            >
             <el-button @click="resetForm('dataSmtp')">重置</el-button>
           </el-form-item>
         </el-form>
       </el-tab-pane>
 
       <el-tab-pane label="审计日志" name="dataAuditLog">
-        <el-form :model="dataAuditLog" ref="dataAuditLog" :rules="rules" label-width="100px" class="tab-one">
+        <el-form
+          :model="dataAuditLog"
+          ref="dataAuditLog"
+          :rules="rules"
+          label-width="100px"
+          class="tab-one"
+        >
+          <el-form-item label="审计去重间隔" prop="audit_interval">
+            <el-input-number
+              v-model="dataAuditLog.audit_interval"
+              :min="-1"
+              size="small"
+              label="秒"
+              :disabled="true"
+            ></el-input-number>
+            秒
+            <p class="input_tip">
+              请手动修改配置文件中的 audit_interval 参数后,再重启服务,
+              <strong style="color: #ea3323">-1 代表关闭审计日志</strong>
+            </p>
+          </el-form-item>
           <el-form-item label="存储时长" prop="life_day">
-                <el-input-number v-model="dataAuditLog.life_day" :min="0" :max="365" size="small" label="天数"></el-input-number>  天
-                <p class="input_tip">范围: 0 ~ 365天 , <strong style="color:#EA3323;">0 代表永久保存</strong></p>
+            <el-input-number
+              v-model="dataAuditLog.life_day"
+              :min="0"
+              :max="365"
+              size="small"
+              label="天数"
+            ></el-input-number>
+            天
+            <p class="input_tip">
+              范围: 0 ~ 365天 ,
+              <strong style="color: #ea3323">0 代表永久保存</strong>
+            </p>
           </el-form-item>
           <el-form-item label="清理时间" prop="clear_time">
             <el-time-select
-                v-model="dataAuditLog.clear_time"
-                :picker-options="{
-                    start: '00:00',
-                    step: '01:00',
-                    end: '23:00'
-                }"
-                editable=false,
-                size="small"
-                placeholder="请选择"
-                style="width:130px;">
-                </el-time-select>  
-            </el-form-item>
+              v-model="dataAuditLog.clear_time"
+              :picker-options="{
+                start: '00:00',
+                step: '01:00',
+                end: '23:00',
+              }"
+              editable="false,"
+              size="small"
+              placeholder="请选择"
+              style="width: 130px"
+            >
+            </el-time-select>
+          </el-form-item>
           <el-form-item>
-            <el-button type="primary" @click="submitForm('dataAuditLog')">保存</el-button>
+            <el-button type="primary" @click="submitForm('dataAuditLog')"
+              >保存</el-button
+            >
             <el-button @click="resetForm('dataAuditLog')">重置</el-button>
-          </el-form-item>          
+          </el-form-item>
         </el-form>
-      </el-tab-pane>     
+      </el-tab-pane>
+      <el-tab-pane label="证书设置" name="datacertManage">
+        <el-tabs
+          tab-position="left"
+          v-model="datacertManage"
+          @tab-click="handleClick"
+        >
+          <el-tab-pane label="自定义证书" name="customCert">
+            <el-form
+              ref="customCert"
+              :model="customCert"
+              label-width="100px"
+              size="small"
+              class="tab-one"
+            >
+              <el-form-item>
+                <el-upload
+                  class="uploadCert"
+                  :before-upload="beforeCertUpload"
+                  :action="certUpload"
+                  :limit="1"
+                >
+                  <el-button size="mini" icon="el-icon-plus" slot="trigger"
+                    >证书文件</el-button
+                  >
+                  <el-tooltip
+                    class="item"
+                    effect="dark"
+                    content="请上传 .pem 格式的 cert 文件"
+                    placement="top"
+                  >
+                    <i class="el-icon-info"></i>
+                  </el-tooltip>
+                </el-upload>
+              </el-form-item>
+              <el-form-item>
+                <el-upload
+                  class="uploadCert"
+                  :before-upload="beforeKeyUpload"
+                  :action="certUpload"
+                  :limit="1"
+                >
+                  <el-button size="mini" icon="el-icon-plus" slot="trigger"
+                    >私钥文件</el-button
+                  >
+                  <el-tooltip
+                    class="item"
+                    effect="dark"
+                    content="请上传 .pem 格式的 key 文件"
+                    placement="top"
+                  >
+                    <i class="el-icon-info"></i>
+                  </el-tooltip>
+                </el-upload>
+              </el-form-item>
+              <el-form-item>
+                <el-button
+                  size="small"
+                  icon="el-icon-upload"
+                  type="primary"
+                  @click="submitForm('customCert')"
+                  >上传</el-button
+                >
+              </el-form-item>
+            </el-form>
+          </el-tab-pane>
+          <el-tab-pane label="Let's Encrypt证书" name="letsCert">
+            <el-form
+              :model="letsCert"
+              ref="letsCert"
+              :rules="rules"
+              label-width="120px"
+              size="small"
+              class="tab-one"
+            >
+              <el-form-item label="域名" prop="domain">
+                <el-input v-model="letsCert.domain"></el-input>
+              </el-form-item>
+              <el-form-item label="邮箱" prop="legomail">
+                <el-input v-model="letsCert.legomail"></el-input>
+              </el-form-item>
+              <el-form-item label="域名服务商" prop="name">
+                <el-radio-group v-model="letsCert.name">
+                  <el-radio label="aliyun">阿里云</el-radio>
+                  <el-radio label="txcloud">腾讯云</el-radio>
+                  <el-radio label="cfcloud">cloudflare</el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item
+                v-for="component in dnsProvider[letsCert.name]"
+                :key="component.prop"
+                :label="component.label"
+                :rules="component.rules"
+              >
+                <component
+                  :is="component.component"
+                  :type="component.type"
+                  v-model="letsCert[letsCert.name][component.prop]"
+                ></component>
+              </el-form-item>
+              <el-form-item>
+                <el-switch
+                  style="display: block"
+                  v-model="letsCert.renew"
+                  active-color="#13ce66"
+                  inactive-color="#ff4949"
+                  inactive-text="自动续期"
+                >
+                </el-switch>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" @click="submitForm('letsCert')"
+                  >申请</el-button
+                >
+                <el-button @click="resetForm('letsCert')">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </el-tab-pane>
+        </el-tabs>
+      </el-tab-pane>
       <el-tab-pane label="其他设置" name="dataOther">
-        <el-form :model="dataOther" ref="dataOther" :rules="rules" label-width="100px" class="tab-one">
-
+        <el-form
+          :model="dataOther"
+          ref="dataOther"
+          :rules="rules"
+          label-width="100px"
+          class="tab-one"
+        >
           <el-form-item label="vpn对外地址" prop="link_addr">
-            <el-input
-                placeholder="请输入内容"
-                v-model="dataOther.link_addr">
+            <el-input placeholder="请输入内容" v-model="dataOther.link_addr">
             </el-input>
           </el-form-item>
 
           <el-form-item label="Banner信息" prop="banner">
             <el-input
-                type="textarea"
-                :rows="5"
-                placeholder="请输入内容"
-                v-model="dataOther.banner">
+              type="textarea"
+              :rows="5"
+              placeholder="请输入内容"
+              v-model="dataOther.banner"
+            >
             </el-input>
           </el-form-item>
 
           <el-form-item label="自定义首页" prop="homeindex">
             <el-input
-                type="textarea"
-                :rows="5"
-                placeholder="请输入内容"
-                v-model="dataOther.homeindex">
+              type="textarea"
+              :rows="5"
+              placeholder="请输入内容"
+              v-model="dataOther.homeindex"
+            >
             </el-input>
           </el-form-item>
 
           <el-form-item label="账户开通邮件" prop="account_mail">
             <el-input
-                type="textarea"
-                :rows="10"
-                placeholder="请输入内容"
-                v-model="dataOther.account_mail">
+              type="textarea"
+              :rows="10"
+              placeholder="请输入内容"
+              v-model="dataOther.account_mail"
+            >
             </el-input>
           </el-form-item>
 
           <el-form-item label="邮件展示">
             <iframe
-                width="500px"
-                height="300px"
-                :srcdoc="dataOther.account_mail">
+              width="500px"
+              height="300px"
+              :srcdoc="dataOther.account_mail"
+            >
             </iframe>
           </el-form-item>
 
           <el-form-item>
-            <el-button type="primary" @click="submitForm('dataOther')">保存</el-button>
+            <el-button type="primary" @click="submitForm('dataOther')"
+              >保存</el-button
+            >
             <el-button @click="resetForm('dataOther')">重置</el-button>
           </el-form-item>
         </el-form>
       </el-tab-pane>
-
     </el-tabs>
   </el-card>
 </template>
@@ -120,25 +292,130 @@ import axios from "axios";
 export default {
   name: "Other",
   created() {
-    this.$emit('update:route_path', this.$route.path)
-    this.$emit('update:route_name', ['基础信息', '其他设置'])
+    this.$emit("update:route_path", this.$route.path);
+    this.$emit("update:route_name", ["基础信息", "其他设置"]);
   },
   mounted() {
-    this.getSmtp()
+    this.getSmtp();
   },
   data() {
     return {
-      activeName: 'dataSmtp',
+      activeName: "dataSmtp",
+      datacertManage: "customCert",
       dataSmtp: {},
       dataAuditLog: {},
+      letsCert: {
+        domain: ``,
+        legomail: ``,
+        name: "",
+        renew: "",
+        aliyun: {
+          apiKey: "",
+          secretKey: "",
+        },
+        txcloud: {
+          secretId: "",
+          secretKey: "",
+        },
+        cfcloud: {
+          authEmail: "",
+          authKey: "",
+        },
+      },
+      customCert: { cert: "", key: "" },
       dataOther: {},
       rules: {
-        host: {required: true, message: '请输入服务器地址', trigger: 'blur'},
+        host: { required: true, message: "请输入服务器地址", trigger: "blur" },
         port: [
-          {required: true, message: '请输入服务器端口', trigger: 'blur'},
-          {type: 'number', message: '请输入正确的服务器端口', trigger: ['blur', 'change']}
+          { required: true, message: "请输入服务器端口", trigger: "blur" },
+          {
+            type: "number",
+            message: "请输入正确的服务器端口",
+            trigger: ["blur", "change"],
+          },
+        ],
+        issuer: { required: true, message: "请输入系统名称", trigger: "blur" },
+        domain: {
+          required: true,
+          message: "请输入需要申请证书的域名",
+          trigger: "blur",
+        },
+        legomail: {
+          required: true,
+          message: "请输入申请证书的邮箱地址",
+          trigger: "blur",
+        },
+        name: { required: true, message: "请选择域名服务商", trigger: "blur" },
+      },
+      certUpload: "/set/other/customcert",
+      dnsProvider: {
+        aliyun: [
+          {
+            label: "APIKey",
+            prop: "apiKey",
+            component: "el-input",
+            type: "password",
+            rules: {
+              required: true,
+              message: "请输入正确的APIKey",
+              trigger: "blur",
+            },
+          },
+          {
+            label: "SecretKey",
+            prop: "secretKey",
+            component: "el-input",
+            type: "password",
+            rules: {
+              required: true,
+              message: "请输入正确的SecretKey",
+              trigger: "blur",
+            },
+          },
+        ],
+        txcloud: [
+          {
+            label: "SecretID",
+            prop: "secretId",
+            component: "el-input",
+            type: "password",
+            rules: {
+              required: true,
+              message: "请输入正确的APIKey",
+              trigger: "blur",
+            },
+          },
+          {
+            label: "SecretKey",
+            prop: "secretKey",
+            component: "el-input",
+            type: "password",
+            rules: {
+              required: true,
+              message: "请输入正确的APIKey",
+              trigger: "blur",
+            },
+          },
+        ],
+        cfcloud: [
+          {
+            label: "Email",
+            prop: "email",
+            component: "el-input",
+            type: "text",
+          },
+          {
+            label: "AuthKey",
+            prop: "authKey",
+            component: "el-input",
+            type: "password",
+            rules: {
+              required: true,
+              message: "请输入正确的APIKey",
+              trigger: "blur",
+            },
+          },
         ],
-        issuer: {required: true, message: '请输入系统名称', trigger: 'blur'},
       },
     };
   },
@@ -147,118 +424,185 @@ export default {
       window.console.log(tab.name, event);
       switch (tab.name) {
         case "dataSmtp":
-          this.getSmtp()
-          break
+          this.getSmtp();
+          break;
         case "dataAuditLog":
-          this.getAuditLog()
-          break          
+          this.getAuditLog();
+          break;
+        case "letsCert":
+          this.getletsCert();
+          break;
         case "dataOther":
-          this.getOther()
-          break          
+          this.getOther();
+          break;
       }
     },
+    beforeCertUpload(file) {
+      // if (file.type !== 'application/x-pem-file') {
+      //   this.$message.error('只能上传 .pem 格式的证书文件')
+      //   return false
+      // }
+      this.customCert.cert = file;
+    },
+    beforeKeyUpload(file) {
+      // if (file.type !== 'application/x-pem-file') {
+      //   this.$message.error('只能上传 .pem 格式的私钥文件')
+      //   return false
+      // }
+      this.customCert.key = file;
+    },
     getSmtp() {
-      axios.get('/set/other/smtp').then(resp => {
-        let rdata = resp.data
-        console.log(rdata)
-        if (rdata.code !== 0) {
-          this.$message.error(rdata.msg);
-          return;
-        }
-        this.dataSmtp = rdata.data
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
+      axios
+        .get("/set/other/smtp")
+        .then((resp) => {
+          let rdata = resp.data;
+          console.log(rdata);
+          if (rdata.code !== 0) {
+            this.$message.error(rdata.msg);
+            return;
+          }
+          this.dataSmtp = rdata.data;
+        })
+        .catch((error) => {
+          this.$message.error("哦,请求出错");
+          console.log(error);
+        });
     },
     getAuditLog() {
-      axios.get('/set/other/audit_log').then(resp => {
-        let rdata = resp.data
-        console.log(rdata)
-        if (rdata.code !== 0) {
-          this.$message.error(rdata.msg);
-          return;
-        }
-        this.dataAuditLog = rdata.data
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
-    },     
+      axios
+        .get("/set/other/audit_log")
+        .then((resp) => {
+          let rdata = resp.data;
+          console.log(rdata);
+          if (rdata.code !== 0) {
+            this.$message.error(rdata.msg);
+            return;
+          }
+          this.dataAuditLog = rdata.data;
+        })
+        .catch((error) => {
+          this.$message.error("哦,请求出错");
+          console.log(error);
+        });
+    },
+    getletsCert() {
+      axios
+        .get("/set/other/getcertset")
+        .then((resp) => {
+          let rdata = resp.data;
+          console.log(rdata);
+          if (rdata.code !== 0) {
+            this.$message.error(rdata.msg);
+            return;
+          }
+          this.letsCert = Object.assign({}, this.letsCert, rdata.data);
+        })
+        .catch((error) => {
+          this.$message.error("哦,请求出错");
+          console.log(error);
+        });
+    },
     getOther() {
-      axios.get('/set/other').then(resp => {
-        let rdata = resp.data
-        console.log(rdata)
-        if (rdata.code !== 0) {
-          this.$message.error(rdata.msg);
-          return;
-        }
-        this.dataOther = rdata.data
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
+      axios
+        .get("/set/other")
+        .then((resp) => {
+          let rdata = resp.data;
+          console.log(rdata);
+          if (rdata.code !== 0) {
+            this.$message.error(rdata.msg);
+            return;
+          }
+          this.dataOther = rdata.data;
+        })
+        .catch((error) => {
+          this.$message.error("哦,请求出错");
+          console.log(error);
+        });
     },
     submitForm(formName) {
       this.$refs[formName].validate((valid) => {
         if (!valid) {
-          alert('error submit!');
+          alert("error submit!");
         }
 
         switch (formName) {
           case "dataSmtp":
-            axios.post('/set/other/smtp/edit', this.dataSmtp).then(resp => {
-              var rdata = resp.data
+            axios.post("/set/other/smtp/edit", this.dataSmtp).then((resp) => {
+              var rdata = resp.data;
               console.log(rdata);
               if (rdata.code === 0) {
                 this.$message.success(rdata.msg);
               } else {
                 this.$message.error(rdata.msg);
               }
-
-            })
+            });
             break;
           case "dataAuditLog":
-            axios.post('/set/other/audit_log/edit', this.dataAuditLog).then(resp => {
-              var rdata = resp.data
+            axios
+              .post("/set/other/audit_log/edit", this.dataAuditLog)
+              .then((resp) => {
+                var rdata = resp.data;
+                console.log(rdata);
+                if (rdata.code === 0) {
+                  this.$message.success(rdata.msg);
+                } else {
+                  this.$message.error(rdata.msg);
+                }
+              });
+            break;
+          case "letsCert":
+            axios.post("/set/other/createcert", this.letsCert).then((resp) => {
+              var rdata = resp.data;
+              console.log(rdata);
+              if (rdata.code === 0) {
+                this.$message.success(rdata.msg);
+              } else {
+                this.$message.error(rdata.msg);
+              }
+            });
+            break;
+          case "customCert":
+            var formData = new FormData();
+            formData.append("cert", this.customCert.cert);
+            formData.append("key", this.customCert.key);
+            axios.post(this.certUpload, formData).then((resp) => {
+              var rdata = resp.data;
               console.log(rdata);
               if (rdata.code === 0) {
                 this.$message.success(rdata.msg);
               } else {
                 this.$message.error(rdata.msg);
               }
-            })
+            });
             break;
           case "dataOther":
-            axios.post('/set/other/edit', this.dataOther).then(resp => {
-              var rdata = resp.data
+            axios.post("/set/other/edit", this.dataOther).then((resp) => {
+              var rdata = resp.data;
               console.log(rdata);
               if (rdata.code === 0) {
                 this.$message.success(rdata.msg);
               } else {
                 this.$message.error(rdata.msg);
               }
-            })
+            });
             break;
         }
-
       });
     },
     resetForm(formName) {
       this.$refs[formName].resetFields();
-    }
+    },
   },
-}
+};
 </script>
 
 <style scoped>
 .tab-one {
-  width: 600px;
+  width: 700px;
 }
 
 .input_tip {
-    line-height: 1.428;    
-    margin:2px 0 0 0;
+  line-height: 1.428;
+  margin: 2px 0 0 0;
 }
-
 </style>

+ 246 - 230
anylink/web/src/pages/user/IpMap.vue

@@ -1,269 +1,285 @@
 <template>
-  <div>
-    <el-card>
+    <div>
+        <el-card>
 
-      <el-form :inline="true">
-        <el-form-item>
-          <el-button
-              size="small"
-              type="primary"
-              icon="el-icon-plus"
-              @click="handleEdit('')">添加
-          </el-button>
-        </el-form-item>
-      </el-form>
+            <el-form :inline="true">
+                <el-form-item>
+                    <el-button
+                            size="small"
+                            type="primary"
+                            icon="el-icon-plus"
+                            @click="handleEdit('')">添加
+                    </el-button>
+                </el-form-item>
+                <!--
+                <el-form-item>
+                    <el-alert
+                            title="直接操作数据库增删改数据后,请重启anylink服务"
+                            type="warning">
+                    </el-alert>
+                </el-form-item>
+                -->
+            </el-form>
 
-      <el-table
-          ref="multipleTable"
-          :data="tableData"
-          border>
+            <el-table
+                    ref="multipleTable"
+                    :data="tableData"
+                    border>
 
-        <el-table-column
-            sortable="true"
-            prop="id"
-            label="ID"
-            width="60">
-        </el-table-column>
+                <el-table-column
+                        sortable="true"
+                        prop="id"
+                        label="ID"
+                        width="60">
+                </el-table-column>
 
-        <el-table-column
-            prop="ip_addr"
-            label="IP地址">
-        </el-table-column>
+                <el-table-column
+                        prop="ip_addr"
+                        label="IP地址">
+                </el-table-column>
 
-        <el-table-column
-            prop="mac_addr"
-            label="MAC地址">
-        </el-table-column>
+                <el-table-column
+                        prop="mac_addr"
+                        label="MAC地址">
+                </el-table-column>
 
-        <el-table-column
-            prop="username"
-            label="用户名">
-        </el-table-column>
+                <el-table-column
+                        prop="unique_mac"
+                        label="唯一MAC">
+                    <template slot-scope="scope">
+                        <el-tag v-if="scope.row.unique_mac" type="success">是</el-tag>
+                    </template>
+                </el-table-column>
 
-        <el-table-column
-            prop="keep"
-            label="IP保留">
-          <template slot-scope="scope">
-            <!--            <el-tag v-if="scope.row.keep" type="success">保留</el-tag>-->
-            <el-switch
-                disabled
-                v-model="scope.row.keep"
-                active-color="#13ce66">
-            </el-switch>
-          </template>
-        </el-table-column>
+                <el-table-column
+                        prop="username"
+                        label="用户名">
+                </el-table-column>
 
-        <el-table-column
-            prop="note"
-            label="备注">
-        </el-table-column>
+                <el-table-column
+                        prop="keep"
+                        label="IP保留">
+                    <template slot-scope="scope">
+                        <!--            <el-tag v-if="scope.row.keep" type="success">保留</el-tag>-->
+                        <el-switch
+                                disabled
+                                v-model="scope.row.keep"
+                                active-color="#13ce66">
+                        </el-switch>
+                    </template>
+                </el-table-column>
 
-        <el-table-column
-            prop="last_login"
-            label="最后登陆时间"
-            :formatter="tableDateFormat">
-        </el-table-column>
+                <el-table-column
+                        prop="note"
+                        label="备注">
+                </el-table-column>
 
-        <el-table-column
-            label="操作"
-            width="150">
-          <template slot-scope="scope">
-            <el-button
-                size="mini"
-                type="primary"
-                @click="handleEdit(scope.row)">编辑
-            </el-button>
+                <el-table-column
+                        prop="last_login"
+                        label="最后登陆时间"
+                        :formatter="tableDateFormat">
+                </el-table-column>
 
-            <el-popconfirm
-                class="m-left-10"
-                @confirm="handleDel(scope.row)"
-                title="确定要删除IP映射吗?">
-              <el-button
-                  slot="reference"
-                  size="mini"
-                  type="danger">删除
-              </el-button>
-            </el-popconfirm>
+                <el-table-column
+                        label="操作"
+                        width="150">
+                    <template slot-scope="scope">
+                        <el-button
+                                size="mini"
+                                type="primary"
+                                @click="handleEdit(scope.row)">编辑
+                        </el-button>
 
-          </template>
-        </el-table-column>
-      </el-table>
+                        <el-popconfirm
+                                class="m-left-10"
+                                @confirm="handleDel(scope.row)"
+                                title="确定要删除IP映射吗?">
+                            <el-button
+                                    slot="reference"
+                                    size="mini"
+                                    type="danger">删除
+                            </el-button>
+                        </el-popconfirm>
 
-      <div class="sh-20"></div>
+                    </template>
+                </el-table-column>
+            </el-table>
 
-      <el-pagination
-          background
-          layout="prev, pager, next"
-          :pager-count="11"
-          @current-change="pageChange"
-          :total="count">
-      </el-pagination>
+            <div class="sh-20"></div>
 
-    </el-card>
+            <el-pagination
+                    background
+                    layout="prev, pager, next"
+                    :pager-count="11"
+                    @current-change="pageChange"
+                    :total="count">
+            </el-pagination>
 
-    <!--新增、修改弹出框-->
-    <el-dialog
-        title="提示"
-        :close-on-click-modal="false"
-        :visible="user_edit_dialog"
-        @close="disVisible"
-        width="600px"
-        center>
+        </el-card>
 
-      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
-        <el-form-item label="ID" prop="id">
-          <el-input v-model="ruleForm.id" disabled></el-input>
-        </el-form-item>
-        <el-form-item label="IP地址" prop="ip_addr">
-          <el-input v-model="ruleForm.ip_addr"></el-input>
-        </el-form-item>
-        <el-form-item label="MAC地址" prop="mac_addr">
-          <el-input v-model="ruleForm.mac_addr"></el-input>
-        </el-form-item>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="ruleForm.username"></el-input>
-        </el-form-item>
+        <!--新增、修改弹出框-->
+        <el-dialog
+                title="提示"
+                :close-on-click-modal="false"
+                :visible="user_edit_dialog"
+                @close="disVisible"
+                width="600px"
+                center>
 
-        <el-form-item label="备注" prop="note">
-          <el-input v-model="ruleForm.note"></el-input>
-        </el-form-item>
+            <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
+                <el-form-item label="ID" prop="id">
+                    <el-input v-model="ruleForm.id" disabled></el-input>
+                </el-form-item>
+                <el-form-item label="IP地址" prop="ip_addr">
+                    <el-input v-model="ruleForm.ip_addr"></el-input>
+                </el-form-item>
+                <el-form-item label="MAC地址" prop="mac_addr">
+                    <el-input v-model="ruleForm.mac_addr"></el-input>
+                </el-form-item>
+                <el-form-item label="用户名" prop="username">
+                    <el-input v-model="ruleForm.username"></el-input>
+                </el-form-item>
 
-        <el-form-item label="IP保留" prop="keep">
-          <el-switch
-              v-model="ruleForm.keep"
-              active-color="#13ce66">
-          </el-switch>
-        </el-form-item>
+                <el-form-item label="备注" prop="note">
+                    <el-input v-model="ruleForm.note"></el-input>
+                </el-form-item>
 
-        <el-form-item>
-          <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
-          <el-button @click="disVisible">取消</el-button>
-        </el-form-item>
-      </el-form>
+                <el-form-item label="IP保留" prop="keep">
+                    <el-switch
+                            v-model="ruleForm.keep"
+                            active-color="#13ce66">
+                    </el-switch>
+                </el-form-item>
 
-    </el-dialog>
+                <el-form-item>
+                    <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
+                    <el-button @click="disVisible">取消</el-button>
+                </el-form-item>
+            </el-form>
 
-  </div>
+        </el-dialog>
+
+    </div>
 </template>
 
 <script>
 import axios from "axios";
 
 export default {
-  name: "IpMap",
-  components: {},
-  mixins:[],
-  created() {
-    this.$emit('update:route_path', this.$route.path)
-    this.$emit('update:route_name', ['用户信息', 'IP映射'])
-  },
-  mounted() {
-    this.getData(1)
-  },
-  data() {
-    return {
-      tableData: [],
-      count: 10,
-      nowIndex: 0,
-      ruleForm: {
-        status: 1,
-        groups: [],
-      },
-      rules: {
-        username: [
-          {required: false, message: '请输入用户名', trigger: 'blur'},
-          {max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
-        ],
-        mac_addr: [
-          {required: true, message: '请输入mac地址', trigger: 'blur'}
-        ],
-        ip_addr: [
-          {required: true, message: '请输入ip地址', trigger: 'blur'}
-        ],
-
-        status: [
-          {required: true}
-        ],
-      },
-    }
-  },
-  methods: {
-    getData(p) {
-      axios.get('/user/ip_map/list', {
-        params: {
-          page: p,
-        }
-      }).then(resp => {
-        var data = resp.data.data
-        console.log(data);
-        this.tableData = data.datas;
-        this.count = data.count
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
+    name: "IpMap",
+    components: {},
+    mixins: [],
+    created() {
+        this.$emit('update:route_path', this.$route.path)
+        this.$emit('update:route_name', ['用户信息', 'IP映射'])
     },
-    pageChange(p) {
-      this.getData(p)
+    mounted() {
+        this.getData(1)
     },
-    handleEdit(row) {
-      !this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
-      console.log(row)
-      this.user_edit_dialog = true
-      if (!row) {
-        return;
-      }
+    data() {
+        return {
+            tableData: [],
+            count: 10,
+            nowIndex: 0,
+            ruleForm: {
+                status: 1,
+                groups: [],
+            },
+            rules: {
+                username: [
+                    {required: false, message: '请输入用户名', trigger: 'blur'},
+                    {max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
+                ],
+                mac_addr: [
+                    {required: true, message: '请输入mac地址', trigger: 'blur'}
+                ],
+                ip_addr: [
+                    {required: true, message: '请输入ip地址', trigger: 'blur'}
+                ],
 
-      axios.get('/user/ip_map/detail', {
-        params: {
-          id: row.id,
+                status: [
+                    {required: true}
+                ],
+            },
         }
-      }).then(resp => {
-        this.ruleForm = resp.data.data
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
     },
-    handleDel(row) {
-      axios.post('/user/ip_map/del?id=' + row.id).then(resp => {
-        var rdata = resp.data
-        if (rdata.code === 0) {
-          this.$message.success(rdata.msg);
-          this.getData(1);
-        } else {
-          this.$message.error(rdata.msg);
-        }
-        console.log(rdata);
-      }).catch(error => {
-        this.$message.error('哦,请求出错');
-        console.log(error);
-      });
-    },
-    submitForm(formName) {
-      this.$refs[formName].validate((valid) => {
-        if (!valid) {
-          console.log('error submit!!');
-          return false;
-        }
+    methods: {
+        getData(p) {
+            axios.get('/user/ip_map/list', {
+                params: {
+                    page: p,
+                }
+            }).then(resp => {
+                var data = resp.data.data
+                console.log(data);
+                this.tableData = data.datas;
+                this.count = data.count
+            }).catch(error => {
+                this.$message.error('哦,请求出错');
+                console.log(error);
+            });
+        },
+        pageChange(p) {
+            this.getData(p)
+        },
+        handleEdit(row) {
+            !this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
+            console.log(row)
+            this.user_edit_dialog = true
+            if (!row) {
+                return;
+            }
+
+            axios.get('/user/ip_map/detail', {
+                params: {
+                    id: row.id,
+                }
+            }).then(resp => {
+                this.ruleForm = resp.data.data
+            }).catch(error => {
+                this.$message.error('哦,请求出错');
+                console.log(error);
+            });
+        },
+        handleDel(row) {
+            axios.post('/user/ip_map/del?id=' + row.id).then(resp => {
+                var rdata = resp.data
+                if (rdata.code === 0) {
+                    this.$message.success(rdata.msg);
+                    this.getData(1);
+                } else {
+                    this.$message.error(rdata.msg);
+                }
+                console.log(rdata);
+            }).catch(error => {
+                this.$message.error('哦,请求出错');
+                console.log(error);
+            });
+        },
+        submitForm(formName) {
+            this.$refs[formName].validate((valid) => {
+                if (!valid) {
+                    console.log('error submit!!');
+                    return false;
+                }
 
-        // alert('submit!');
-        axios.post('/user/ip_map/set', this.ruleForm).then(resp => {
-          var rdata = resp.data
-          if (rdata.code === 0) {
-            this.$message.success(rdata.msg);
-            this.getData(1);
-          } else {
-            this.$message.error(rdata.msg);
-          }
-          console.log(rdata);
-        }).catch(error => {
-          this.$message.error('哦,请求出错');
-          console.log(error);
-        });
-      });
+                // alert('submit!');
+                axios.post('/user/ip_map/set', this.ruleForm).then(resp => {
+                    var rdata = resp.data
+                    if (rdata.code === 0) {
+                        this.$message.success(rdata.msg);
+                        this.getData(1);
+                    } else {
+                        this.$message.error(rdata.msg);
+                    }
+                    console.log(rdata);
+                }).catch(error => {
+                    this.$message.error('哦,请求出错');
+                    console.log(error);
+                });
+            });
+        },
     },
-  },
 }
 </script>
 

+ 56 - 3
anylink/web/src/pages/user/List.vue

@@ -10,7 +10,24 @@
               @click="handleEdit('')">添加
           </el-button>
         </el-form-item>
-
+        <el-form-item>
+          <el-dropdown size="small" placement="bottom">
+            <el-upload
+              class="uploaduser"
+              action="uploaduser"
+              accept=".xlsx, .xls"
+              :http-request="upLoadUser"
+              :limit="1"
+              :show-file-list="false">
+              <el-button size="small"  icon="el-icon-upload2" type="primary">批量添加</el-button>
+            </el-upload>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item>
+              <el-link style="font-size:12px;" type="success" href="批量添加用户模版.xlsx"><i class="el-icon-download"></i>下载模版</el-link>
+            </el-dropdown-item>
+          </el-dropdown-menu>
+          </el-dropdown>
+        </el-form-item>
         <el-form-item label="用户名:">
           <el-input size="small" v-model="searchData" placeholder="请输入内容" @keydown.enter.native="searchEnterFun"></el-input>
         </el-form-item>
@@ -87,7 +104,8 @@
             width="70">
           <template slot-scope="scope">
             <el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
-            <el-tag v-else type="danger">停用</el-tag>
+            <el-tag v-if="scope.row.status === 0" type="danger">停用</el-tag>
+            <el-tag v-if="scope.row.status === 2" >过期</el-tag>
           </template>
         </el-table-column>
 
@@ -182,6 +200,18 @@
           <el-input v-model="ruleForm.pin_code" placeholder="不填由系统自动生成"></el-input>
         </el-form-item>
 
+        <el-form-item label="过期时间" prop="limittime">
+          <el-date-picker
+            v-model="ruleForm.limittime"
+            type="date"
+            size="small"
+            align="center"
+            style="width:130px"
+            :picker-options="pickerOptions"
+            placeholder="选择日期">
+          </el-date-picker>
+        </el-form-item>
+        
         <el-form-item label="禁用OTP" prop="disable_otp">
           <el-switch
               v-model="ruleForm.disable_otp">
@@ -208,6 +238,7 @@
           <el-radio-group v-model="ruleForm.status">
             <el-radio :label="1" border>启用</el-radio>
             <el-radio :label="0" border>停用</el-radio>
+            <el-radio :label="2" border>过期</el-radio>
           </el-radio-group>
         </el-form-item>
 
@@ -245,6 +276,11 @@ export default {
       grouNames: [],
       tableData: [],
       count: 10,
+      pickerOptions: {
+        disabledDate(time) {
+            return time.getTime() < Date.now();
+        }
+      },
       searchData: '',
       otpImgData: {visible: false, username: '', nickname: '', base64Img: ''},
       ruleForm: {
@@ -264,7 +300,6 @@ export default {
           {required: true, message: '请输入用户邮箱', trigger: 'blur'},
           {type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']}
         ],
-
         password: [
           {min: 6, message: '长度大于 6 个字符', trigger: 'blur'}
         ],
@@ -285,6 +320,24 @@ export default {
   },
 
   methods: {
+    upLoadUser(item) {
+      const formData = new FormData();
+      formData.append("file", item.file);
+      axios.post('/user/uploaduser', formData, {
+         headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      }).then(resp => {
+        if (resp.data.code === 0) {
+          this.$message.success(resp.data.data);
+          this.getData(1);
+        } else {
+          this.$message.error(resp.data.msg);
+          this.getData(1);
+        }
+        console.log(resp.data);
+      })
+    },
     getOtpImg(row) {
       // this.base64Img = Buffer.from(data).toString('base64');
       this.otpImgData.visible = true

+ 9 - 0
anylink/web/src/pages/user/Online.vue

@@ -27,6 +27,15 @@
             prop="mac_addr"
             label="MAC地址">
         </el-table-column>
+        
+        <el-table-column
+            prop="unique_mac"
+            label="唯一MAC">
+            <template slot-scope="scope">
+                <el-tag v-if="scope.row.unique_mac" type="success">是</el-tag>
+            </template>
+        </el-table-column>
+
         <el-table-column
             prop="ip"
             label="IP地址"

+ 7 - 0
anylink/web/yarn.lock

@@ -6759,6 +6759,13 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
   integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
 
+qs@^6.11.1:
+  version "6.11.1"
+  resolved "https://registry.npmmirror.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f"
+  integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==
+  dependencies:
+    side-channel "^1.0.4"
+
 qs@~6.5.2:
   version "6.5.3"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"