mirror of
				https://github.com/bjdgyc/anylink.git
				synced 2025-10-31 16:43:28 +08:00 
			
		
		
		
	
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -60,11 +60,17 @@ AnyLink 服务端仅在 CentOS 7、CentOS 8、Ubuntu 18.04、Ubuntu 20.04 测试 | ||||
|  | ||||
| ### 自行编译安装 | ||||
|  | ||||
| > 需要提前安装好 golang >= 1.19 和 nodejs >= 14.x 和 yarn >= v1.22.x | ||||
| > 需要提前安装好 golang >= 1.19 和 nodejs >= 16.x 和 yarn >= v1.22.x | ||||
|  | ||||
| ```shell | ||||
| git clone https://github.com/bjdgyc/anylink.git | ||||
|  | ||||
| # 编译参考软件版本 | ||||
| # go 1.20.12 | ||||
| # node v16.20.2 | ||||
| # yarn 1.22.19 | ||||
|  | ||||
|  | ||||
| cd anylink | ||||
| sh build.sh | ||||
|  | ||||
| @@ -284,7 +290,7 @@ ipv4_end = "10.1.2.200" | ||||
|        -c=/etc/server.toml --ip_lease=1209600 # IP地址租约时长 | ||||
|    ``` | ||||
|  | ||||
| 7. 构建镜像 | ||||
| 7. 构建镜像 (非必需) | ||||
|  | ||||
|    ```bash | ||||
|    #获取仓库源码 | ||||
| @@ -301,6 +307,7 @@ ipv4_end = "10.1.2.200" | ||||
| ## Discussion | ||||
|  | ||||
| 添加QQ群(1): 567510628 | ||||
|  | ||||
| 添加QQ群(2): 739072205 | ||||
|  | ||||
| 群共享文件有相关软件下载 | ||||
|   | ||||
							
								
								
									
										3
									
								
								build.sh
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										3
									
								
								build.sh
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -36,7 +36,7 @@ cp -rf $cpath/web/ui . | ||||
| #国内可替换源加快速度 | ||||
| export GOPROXY=https://goproxy.io | ||||
| go mod tidy | ||||
| go build -v -o anylink -ldflags "-X main.CommitId=$(git rev-parse HEAD)" | ||||
| go build -v -o anylink -ldflags "-s -w -X main.CommitId=$(git rev-parse HEAD)" | ||||
| RETVAL $? | ||||
|  | ||||
| cd $cpath | ||||
| @@ -52,6 +52,7 @@ cp -r server/conf $deploy | ||||
|  | ||||
| cp -r systemd $deploy | ||||
| cp -r LICENSE $deploy | ||||
| cp -r home $deploy | ||||
|  | ||||
| tar zcvf ${deploy}.tar.gz $deploy | ||||
|  | ||||
|   | ||||
| @@ -6,10 +6,13 @@ echo $ver | ||||
| #docker login -u bjdgyc | ||||
|  | ||||
| #docker build -t bjdgyc/anylink . | ||||
| docker build -t bjdgyc/anylink -f docker/Dockerfile . | ||||
|  | ||||
| docker build -t bjdgyc/anylink --build-arg GitCommitId=$(git rev-parse HEAD) -f docker/Dockerfile . | ||||
|  | ||||
| docker tag bjdgyc/anylink:latest bjdgyc/anylink:$ver | ||||
|  | ||||
| exit 0 | ||||
|  | ||||
| docker push bjdgyc/anylink:$ver | ||||
| docker push bjdgyc/anylink:latest | ||||
|  | ||||
|   | ||||
| @@ -12,8 +12,8 @@ | ||||
| > | ||||
| > 需要展示主页的同学,可以在QQ群 直接联系我添加。 | ||||
|  | ||||
| | 昵称      | 主页                         | | ||||
| |---------| ---------------------------- | | ||||
| | 昵称        | 主页 / 留言                      | | ||||
| |-----------|------------------------------| | ||||
| | 代码 oo8    |                              | | ||||
| | 甘磊        | https://github.com/ganlei333 | | ||||
| | Oo@       | https://github.com/chooop    | | ||||
| @@ -40,6 +40,8 @@ | ||||
| | 悲鸣        |                              | | ||||
| | 谢谢        |                              | | ||||
| | 云思科技      |                              | | ||||
| | 哆啦A伟(张佳伟) |                              | | ||||
| | nobody    | 开源不易,感谢分享                    | | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -46,6 +46,34 @@ stream { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| > nginx实现 共用443端口 示例 | ||||
|  | ||||
| ```conf | ||||
| stream { | ||||
|     map $ssl_preread_server_name $name { | ||||
|         vpn.xx.com        myvpn; | ||||
|         default     defaultpage; | ||||
|     } | ||||
|      | ||||
|     # upstream pool | ||||
|     upstream myvpn { | ||||
|         server 127.0.0.1:8443; | ||||
|     } | ||||
|     upstream defaultpage { | ||||
|         server 127.0.0.1:8080; | ||||
|     } | ||||
|      | ||||
|     server { | ||||
|         listen 443 so_keepalive=on; | ||||
|         ssl_preread on; | ||||
|         #接收端也需要设置 proxy_protocol | ||||
|         #proxy_protocol on; | ||||
|         proxy_pass $name; | ||||
|     } | ||||
| } | ||||
|  | ||||
| ``` | ||||
|  | ||||
| ### 性能问题 | ||||
| ``` | ||||
| 内网环境测试数据 | ||||
|   | ||||
| @@ -1,40 +1,49 @@ | ||||
| #node:16-bullseye | ||||
| #golang:1.20-bullseye | ||||
| #debian:bullseye-slim | ||||
| #sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list | ||||
|  | ||||
|  | ||||
| # web | ||||
| FROM node:16.17.1-alpine3.15 as builder_node | ||||
| FROM node:16-alpine3.18 as builder_node | ||||
| WORKDIR /web | ||||
| COPY ./web /web | ||||
| RUN yarn install \ | ||||
|     && yarn run build \ | ||||
|     && ls /web/ui | ||||
|  | ||||
|  | ||||
| # server | ||||
| FROM golang:1.19-alpine as builder_golang | ||||
| FROM golang:1.20-alpine3.18 as builder_golang | ||||
| #TODO 本地打包时使用镜像 | ||||
| ENV GOPROXY=https://goproxy.io | ||||
| ENV GOPROXY=https://goproxy.cn | ||||
| ENV GOOS=linux | ||||
| ARG GitCommitId="gitCommitId" | ||||
|  | ||||
| WORKDIR /anylink | ||||
| COPY . /anylink | ||||
| COPY --from=builder_node /web/ui  /anylink/server/ui | ||||
| COPY server /anylink | ||||
| COPY --from=builder_node /web/ui  /anylink/ui | ||||
|  | ||||
| #TODO 本地打包时使用镜像 | ||||
| RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories | ||||
| RUN apk add --no-cache git gcc musl-dev | ||||
| RUN cd /anylink/server;go mod tidy;go build -o anylink -ldflags "-X main.CommitId=$(git rev-parse HEAD)" \ | ||||
|     && /anylink/server/anylink tool -v | ||||
| RUN apk add gcc musl-dev | ||||
| RUN cd /anylink;go mod tidy;go build -o anylink -ldflags "-s -w -X main.CommitId=${GitCommitId}" \ | ||||
|     && /anylink/anylink tool -v | ||||
|  | ||||
|  | ||||
| # anylink | ||||
| FROM alpine | ||||
| FROM alpine:3.18 | ||||
| LABEL maintainer="github.com/bjdgyc" | ||||
|  | ||||
| #ENV IPV4_CIDR="192.168.10.0/24" | ||||
| ENV ANYLINK_IN_CONTAINER=true | ||||
|  | ||||
| WORKDIR /app | ||||
| COPY --from=builder_golang /anylink/server/anylink  /app/ | ||||
| COPY --from=builder_golang /anylink/anylink  /app/ | ||||
| COPY docker/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 | ||||
|  | ||||
| COPY ./home  /app/home | ||||
|  | ||||
| #TODO 本地打包时使用镜像 | ||||
| RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories | ||||
|   | ||||
							
								
								
									
										101
									
								
								home/自定义首页1.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								home/自定义首页1.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|   <meta charset="UTF-8"> | ||||
|   <title>AnyLink - 企业级远程办公 SSL VPN</title> | ||||
|   <style> | ||||
|     /* CSS样式表 */ | ||||
|     body { | ||||
|       font-family: Arial, sans-serif; | ||||
|       margin: 0; | ||||
|       padding: 0; | ||||
|     } | ||||
|      | ||||
|     header { | ||||
|       background-color: #333; | ||||
|       color: #fff; | ||||
|       padding: 20px; | ||||
|       text-align: center; | ||||
|     } | ||||
|      | ||||
|     h1 { | ||||
|       margin: 0; | ||||
|       font-size: 32px; | ||||
|     } | ||||
|      | ||||
|     main { | ||||
|       max-width: 960px; | ||||
|       margin: 20px auto; | ||||
|       padding: 0 20px; | ||||
| 	  margin-bottom: 100px; | ||||
|     } | ||||
|      | ||||
|     p { | ||||
|       line-height: 1.5; | ||||
|     } | ||||
|      | ||||
| 	/* 设置页脚固定在底部,并且占满横向宽度 */ | ||||
| 	footer { | ||||
| 		position: fixed; | ||||
| 		bottom: 0; | ||||
| 		left: 0; | ||||
| 		width: 100%; | ||||
| 	} | ||||
| 	 | ||||
|     footer { | ||||
|       background-color: #f2f2f2; | ||||
|       padding: 20px; | ||||
|       text-align: center; | ||||
|     } | ||||
| 	 | ||||
|      | ||||
|     .cta-button { | ||||
|       display: inline-block; | ||||
|       background-color: #007bff; | ||||
|       color: #fff; | ||||
|       padding: 10px 20px; | ||||
|       text-decoration: none; | ||||
|       border-radius: 4px; | ||||
|       font-weight: bold; | ||||
|       margin-right: 10px; | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <header> | ||||
|     <h1>欢迎使用 AnyLink</h1> | ||||
|   </header> | ||||
|  | ||||
|   <main> | ||||
|     <h2>什么是 AnyLink?</h2> | ||||
|     <p>AnyLink 是一款面向企业级的远程办公 SSL VPN 软件,支持多人同时在线使用。它提供安全、便捷的访问内部网络资源的方式,使远程工作者能够有效协作。</p> | ||||
|  | ||||
|     <h2>核心功能</h2> | ||||
|     <ul> | ||||
|       <li>安全远程访问:AnyLink 使用 SSL/TLS 加密技术,确保远程用户与企业网络之间的连接安全可靠。</li> | ||||
|       <li>多用户支持:多个用户可以同时连接 VPN,实现不同地点团队的无缝协作。</li> | ||||
|       <li>灵活网络访问:AnyLink 能够安全地让远程工作者访问内部资源,如文件、应用程序和数据库。</li> | ||||
|       <li>集中化管理:该 VPN 解决方案提供集中化管理控制台,便于用户管理、访问控制和监控。</li> | ||||
|     </ul> | ||||
|  | ||||
|     <h2>开始使用 AnyLink</h2> | ||||
|     <p>体验 AnyLink 为您的企业远程办公需求所带来的便利和安全。</p> | ||||
|  | ||||
|     <h2>下载客户端</h2> | ||||
|     <a href="/files/anyconnect-win-4.10.05111.msi" class="cta-button">Windows 客户端</a> | ||||
|     <a href="/files/anyconnect-macos-4.10.05111.dmg" class="cta-button">Mac 客户端</a> | ||||
|  | ||||
|     <a href="https://apps.apple.com/cn/app/cisco-secure-client/id1135064690" class="cta-button">iOS 客户端</a> | ||||
|     <a href="/files/CiscoSecureClientAnyConnect_v5.0.00247.apk" class="cta-button">Android 客户端</a> | ||||
|  | ||||
|     <a href="/files/freeotp.apk" class="cta-button">Android FreeOTP客户端</a> | ||||
|     <a href="https://apps.apple.com/cn/app/freeotp-authenticator/id872559395" class="cta-button">iOS FreeOTP客户端</a> | ||||
|     <h2>使用手册</h2> | ||||
|     <a href="/files/anylink_doc.pdf" class="cta-button">使用手册(Windows)</a> | ||||
|   </main> | ||||
|  | ||||
|   <footer> | ||||
|     © 2023 AnyLink. 保留所有权利。 | ||||
|   </footer> | ||||
| </body> | ||||
| </html> | ||||
| @@ -67,6 +67,14 @@ func Login(w http.ResponseWriter, r *http.Request) { | ||||
| 	data["admin_user"] = adminUser | ||||
| 	data["expires_at"] = expiresAt | ||||
|  | ||||
| 	ck := &http.Cookie{ | ||||
| 		Name:     "jwt", | ||||
| 		Value:    tokenString, | ||||
| 		Path:     "/", | ||||
| 		HttpOnly: true, | ||||
| 	} | ||||
| 	http.SetCookie(w, ck) | ||||
|  | ||||
| 	RespSucess(w, data) | ||||
| } | ||||
|  | ||||
| @@ -76,13 +84,15 @@ func authMiddleware(next http.Handler) http.Handler { | ||||
| 		w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") | ||||
| 		w.Header().Set("Access-Control-Allow-Headers", "*") | ||||
| 		if r.Method == http.MethodOptions { | ||||
| 			// 正式环境不支持 OPTIONS | ||||
| 			w.WriteHeader(http.StatusForbidden) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		route := mux.CurrentRoute(r) | ||||
| 		name := route.GetName() | ||||
| 		// fmt.Println("bb", r.URL.Path, name) | ||||
| 		if utils.InArrStr([]string{"login", "index", "static", "debug"}, name) { | ||||
| 		if utils.InArrStr([]string{"login", "index", "static"}, name) { | ||||
| 			// 不进行鉴权 | ||||
| 			next.ServeHTTP(w, r) | ||||
| 			return | ||||
| @@ -93,6 +103,12 @@ func authMiddleware(next http.Handler) http.Handler { | ||||
| 		if jwtToken == "" { | ||||
| 			jwtToken = r.FormValue("jwt") | ||||
| 		} | ||||
| 		if jwtToken == "" { | ||||
| 			cc, err := r.Cookie("jwt") | ||||
| 			if err == nil { | ||||
| 				jwtToken = cc.Value | ||||
| 			} | ||||
| 		} | ||||
| 		data, err := GetJwtData(jwtToken) | ||||
| 		if err != nil || base.Cfg.AdminUser != fmt.Sprint(data["admin_user"]) { | ||||
| 			w.WriteHeader(http.StatusUnauthorized) | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"github.com/arl/statsviz" | ||||
| 	"github.com/bjdgyc/anylink/base" | ||||
| 	"github.com/bjdgyc/anylink/dbdata" | ||||
| 	"github.com/bjdgyc/anylink/pkg/utils" | ||||
| 	"github.com/gorilla/handlers" | ||||
| 	"github.com/gorilla/mux" | ||||
| ) | ||||
| @@ -20,6 +21,13 @@ var UiData embed.FS | ||||
| func StartAdmin() { | ||||
|  | ||||
| 	r := mux.NewRouter() | ||||
| 	// 所有路由添加安全头 | ||||
| 	r.Use(func(next http.Handler) http.Handler { | ||||
| 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 			utils.SetSecureHeader(w) | ||||
| 			next.ServeHTTP(w, req) | ||||
| 		}) | ||||
| 	}) | ||||
| 	r.Use(authMiddleware) | ||||
| 	r.Use(handlers.CompressHandler) | ||||
|  | ||||
|   | ||||
| @@ -3,5 +3,5 @@ package base | ||||
| const ( | ||||
| 	APP_NAME = "AnyLink" | ||||
| 	// app版本号 | ||||
| 	APP_VER = "0.9.4" | ||||
| 	APP_VER = "0.10.1" | ||||
| ) | ||||
|   | ||||
							
								
								
									
										65
									
								
								server/base/mod.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								server/base/mod.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| package base | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	procModulesPath = "/proc/modules" | ||||
| 	inContainerKey  = "ANYLINK_IN_CONTAINER" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	inContainer = false | ||||
| 	modMap      = map[string]struct{}{} | ||||
| ) | ||||
|  | ||||
| func initMod() { | ||||
| 	container := os.Getenv(inContainerKey) | ||||
| 	if container == "true" { | ||||
| 		inContainer = true | ||||
| 	} | ||||
| 	log.Println("inContainer", inContainer) | ||||
|  | ||||
| 	file, err := os.Open(procModulesPath) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("[ERROR] Problem with open file: %s", err) | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 	scanner := bufio.NewScanner(file) | ||||
| 	scanner.Split(bufio.ScanLines) | ||||
| 	for scanner.Scan() { | ||||
| 		splited := strings.Split(scanner.Text(), " ") | ||||
| 		if len(splited[0]) > 0 { | ||||
| 			modMap[splited[0]] = struct{}{} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func CheckModOrLoad(mod string) { | ||||
| 	log.Println("CheckModOrLoad", mod) | ||||
|  | ||||
| 	if _, ok := modMap[mod]; ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if inContainer { | ||||
| 		err := fmt.Errorf("Linux modules %s is not loaded, please run `modprobe %s`", mod, mod) | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	cmdstr := fmt.Sprintln("modprobe", mod) | ||||
|  | ||||
| 	cmd := exec.Command("sh", "-c", cmdstr) | ||||
| 	b, err := cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		log.Println(string(b)) | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| @@ -4,6 +4,7 @@ func Start() { | ||||
| 	execute() | ||||
| 	initCfg() | ||||
| 	initLog() | ||||
| 	initMod() | ||||
| } | ||||
|  | ||||
| func Test() { | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| 客户端软件需放置在files目录内, | ||||
| 如需要帮助请加QQ群:567510628 | ||||
| 如需要帮助请加QQ群:567510628 、739072205 | ||||
| @@ -8,6 +8,7 @@ | ||||
|         <RestrictPreferenceCaching>false</RestrictPreferenceCaching> | ||||
|         <RestrictTunnelProtocols>IPSec</RestrictTunnelProtocols> | ||||
|         <BypassDownloader>true</BypassDownloader> | ||||
|         <AutoUpdate UserControllable="false">false</AutoUpdate> | ||||
|         <WindowsVPNEstablishment>AllowRemoteUsers</WindowsVPNEstablishment> | ||||
|         <LinuxVPNEstablishment>AllowRemoteUsers</LinuxVPNEstablishment> | ||||
|         <CertEnrollmentPin>pinAllowed</CertEnrollmentPin> | ||||
| @@ -20,15 +21,19 @@ | ||||
|             </ExtendedKeyUsage> | ||||
|         </CertificateMatch> | ||||
|  | ||||
|         <BackupServerList> | ||||
|             <HostAddress>localhost</HostAddress> | ||||
|         </BackupServerList> | ||||
|     </ClientInitialization> | ||||
|  | ||||
|     <ServerList> | ||||
|  | ||||
|         <HostEntry> | ||||
|             <HostName>VPN Server</HostName> | ||||
|             <HostName>VPN</HostName> | ||||
|             <HostAddress>localhost</HostAddress> | ||||
|         </HostEntry> | ||||
|  | ||||
|         <HostEntry> | ||||
|             <HostName>VPN2</HostName> | ||||
|             <HostAddress>localhost2</HostAddress> | ||||
|         </HostEntry> | ||||
|  | ||||
|     </ServerList> | ||||
| </AnyConnectProfile> | ||||
| @@ -41,6 +41,6 @@ iptables_nat = true | ||||
|  | ||||
|  | ||||
| #客户端显示详细错误信息(线上环境慎开启) | ||||
| display_error = false | ||||
| display_error = true | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,10 +2,13 @@ package handler | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/bjdgyc/anylink/base" | ||||
| @@ -20,10 +23,13 @@ func startDtls() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	certificate, err := selfsign.GenerateSelfSigned() | ||||
| 	// rsa 兼容 open connect | ||||
| 	priv, _ := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 	certificate, err := selfsign.SelfSign(priv) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	logf := logging.NewDefaultLoggerFactory() | ||||
| 	logf.Writer = base.GetBaseLw() | ||||
| 	// logf.DefaultLogLevel = logging.LogLevelTrace | ||||
| @@ -34,9 +40,14 @@ func startDtls() { | ||||
|  | ||||
| 	config := &dtls.Config{ | ||||
| 		Certificates:         []tls.Certificate{certificate}, | ||||
| 		InsecureSkipVerify:   true, | ||||
| 		ExtendedMasterSecret: dtls.DisableExtendedMasterSecret, | ||||
| 		CipherSuites:         []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, | ||||
| 		CipherSuites: func() []dtls.CipherSuiteID { | ||||
| 			var cs = []dtls.CipherSuiteID{} | ||||
| 			for _, vv := range dtlsCipherSuites { | ||||
| 				cs = append(cs, vv) | ||||
| 			} | ||||
| 			return cs | ||||
| 		}(), | ||||
| 		LoggerFactory: logf, | ||||
| 		MTU:           BufferSize, | ||||
| 		SessionStore:  sessStore, | ||||
| @@ -98,3 +109,23 @@ func (ms *sessionStore) Get(key []byte) (dtls.Session, error) { | ||||
| func (ms *sessionStore) Del(key []byte) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // 客户端和服务端映射 X-DTLS12-CipherSuite | ||||
| var dtlsCipherSuites = map[string]dtls.CipherSuiteID{ | ||||
| 	// "ECDHE-ECDSA-AES256-GCM-SHA384": dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	// "ECDHE-ECDSA-AES128-GCM-SHA256": dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"ECDHE-RSA-AES256-GCM-SHA384": dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"ECDHE-RSA-AES128-GCM-SHA256": dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| } | ||||
|  | ||||
| func checkDtls12Ciphersuite(ciphersuite string) string { | ||||
| 	csArr := strings.Split(ciphersuite, ":") | ||||
|  | ||||
| 	for _, v := range csArr { | ||||
| 		if _, ok := dtlsCipherSuites[v]; ok { | ||||
| 			return v | ||||
| 		} | ||||
| 	} | ||||
| 	// 返回默认值 | ||||
| 	return "ECDHE-RSA-AES128-GCM-SHA256" | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/md5" | ||||
| 	"encoding/xml" | ||||
| 	"fmt" | ||||
| @@ -49,7 +50,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 	// fmt.Printf("%+v \n", cr) | ||||
| 	setCommonHeader(w) | ||||
| 	// setCommonHeader(w) | ||||
| 	if cr.Type == "logout" { | ||||
| 		// 退出删除session信息 | ||||
| 		if cr.SessionToken != "" { | ||||
| @@ -154,10 +155,12 @@ func tplRequest(typ int, w io.Writer, data RequestData) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if strings.Contains(data.Banner, "\n") { | ||||
| 		// 替换xml文件的换行符 | ||||
| 		data.Banner = strings.ReplaceAll(data.Banner, "\n", "
") | ||||
| 	if data.Banner != "" { | ||||
| 		buf := new(bytes.Buffer) | ||||
| 		xml.EscapeText(buf, []byte(data.Banner)) | ||||
| 		data.Banner = buf.String() | ||||
| 	} | ||||
|  | ||||
| 	t, _ := template.New("auth_complete").Parse(auth_complete) | ||||
| 	_ = t.Execute(w, data) | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package handler | ||||
| import ( | ||||
| 	"encoding/xml" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os/exec" | ||||
| ) | ||||
|  | ||||
| @@ -42,28 +41,6 @@ type macAddressList struct { | ||||
| 	MacAddress string `xml:"mac-address"` | ||||
| } | ||||
|  | ||||
| func setCommonHeader(w http.ResponseWriter) { | ||||
| 	// Content-Length Date 默认已经存在 | ||||
| 	w.Header().Set("Server", "AnyLinkOpenSource") | ||||
| 	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") | ||||
| 	w.Header().Set("Transfer-Encoding", "chunked") | ||||
| 	w.Header().Set("Connection", "keep-alive") | ||||
| 	w.Header().Set("X-Frame-Options", "deny") | ||||
| 	w.Header().Set("X-Content-Type-Options", "nosniff") | ||||
| 	w.Header().Set("Content-Security-Policy", "default-src 'none'") | ||||
| 	w.Header().Set("X-Permitted-Cross-Domain-Policies", "none") | ||||
| 	w.Header().Set("Referrer-Policy", "no-referrer") | ||||
| 	w.Header().Set("Clear-Site-Data", "cache,cookies,storage") | ||||
| 	w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") | ||||
| 	w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") | ||||
| 	w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") | ||||
| 	w.Header().Set("X-XSS-Protection", "0") | ||||
| 	w.Header().Set("X-Aggregate-Auth", "1") | ||||
| 	w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") | ||||
| } | ||||
|  | ||||
| func execCmd(cmdStrs []string) error { | ||||
| 	for _, cmdStr := range cmdStrs { | ||||
| 		cmd := exec.Command("sh", "-c", cmdStr) | ||||
|   | ||||
| @@ -13,7 +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") | ||||
| 	w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||||
| 	connection := strings.ToLower(r.Header.Get("Connection")) | ||||
| 	userAgent := strings.ToLower(r.UserAgent()) | ||||
| 	if connection == "close" && (strings.Contains(userAgent, "anyconnect") || strings.Contains(userAgent, "openconnect")) { | ||||
|   | ||||
| @@ -22,22 +22,29 @@ func checkTun() { | ||||
| 	defer ifce.Close() | ||||
|  | ||||
| 	// 测试ip命令 | ||||
| 	cmdstr := fmt.Sprintf("ip link set dev %s up mtu %s multicast off", ifce.Name(), "1399") | ||||
| 	err = execCmd([]string{cmdstr}) | ||||
| 	base.CheckModOrLoad("tun") | ||||
|  | ||||
| 	cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %s multicast off", ifce.Name(), "1399") | ||||
| 	err = execCmd([]string{cmdstr1}) | ||||
| 	if err != nil { | ||||
| 		base.Fatal("testTun err: ", err) | ||||
| 	} | ||||
| 	// 开启服务器转发 | ||||
| 	if err := execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"}); err != nil { | ||||
| 		base.Error(err) | ||||
| 		base.Fatal(err) | ||||
| 	} | ||||
| 	if base.Cfg.IptablesNat { | ||||
| 		// 添加NAT转发规则 | ||||
| 		ipt, err := iptables.New() | ||||
| 		if err != nil { | ||||
| 			base.Error(err) | ||||
| 			base.Fatal(err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// 修复 rockyos nat 不生效 | ||||
| 		base.CheckModOrLoad("iptable_filter") | ||||
| 		base.CheckModOrLoad("iptable_nat") | ||||
|  | ||||
| 		natRule := []string{"-s", base.Cfg.Ipv4CIDR, "-o", base.Cfg.Ipv4Master, "-j", "MASQUERADE"} | ||||
| 		forwardRule := []string{"-j", "ACCEPT"} | ||||
| 		if natExists, _ := ipt.Exists("nat", "POSTROUTING", natRule...); !natExists { | ||||
| @@ -65,7 +72,10 @@ func LinkTun(cSess *sessdata.ConnSession) error { | ||||
| 	// log.Printf("Interface Name: %s\n", ifce.Name()) | ||||
| 	cSess.SetIfName(ifce.Name()) | ||||
|  | ||||
| 	cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %d multicast off", ifce.Name(), cSess.Mtu) | ||||
| 	// 通过 ip link show  查看 alias 信息 | ||||
|  | ||||
| 	cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %d multicast off alias %s.%s", ifce.Name(), cSess.Mtu, | ||||
| 		cSess.Group.Name, cSess.Username) | ||||
| 	cmdstr2 := fmt.Sprintf("ip addr add dev %s local %s peer %s/32", | ||||
| 		ifce.Name(), base.Cfg.Ipv4Gateway, cSess.IpAddr) | ||||
| 	err = execCmd([]string{cmdstr1, cmdstr2}) | ||||
|   | ||||
| @@ -92,6 +92,10 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 	base.Debug(cSess.IpAddr, cSess.MacHw, sess.Username, mobile) | ||||
|  | ||||
| 	// 检测密码套件 | ||||
| 	dtlsCiphersuite := checkDtls12Ciphersuite(r.Header.Get("X-Dtls12-Ciphersuite")) | ||||
| 	base.Trace("dtlsCiphersuite", dtlsCiphersuite) | ||||
|  | ||||
| 	// 压缩 | ||||
| 	if cmpName, ok := cSess.SetPickCmp("cstp", r.Header.Get("X-Cstp-Accept-Encoding")); ok { | ||||
| 		HttpSetHeader(w, "X-CSTP-Content-Encoding", cmpName) | ||||
| @@ -164,7 +168,7 @@ 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-DTLS12-CipherSuite", "ECDHE-ECDSA-AES128-GCM-SHA256") | ||||
| 	HttpSetHeader(w, "X-DTLS12-CipherSuite", dtlsCiphersuite) | ||||
|  | ||||
| 	HttpSetHeader(w, "X-CSTP-License", "accept") | ||||
| 	HttpSetHeader(w, "X-CSTP-Routing-Filtering-Ignore", "false") | ||||
| @@ -234,7 +238,11 @@ func SetPostAuthXml(g *dbdata.Group, w http.ResponseWriter) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	HttpSetHeader(w, "X-CSTP-Post-Auth-XML", result.String()) | ||||
| 	xmlAuth := "" | ||||
| 	for _, v := range strings.Split(result.String(), "\n") { | ||||
| 		xmlAuth += strings.TrimSpace(v) | ||||
| 	} | ||||
| 	HttpSetHeader(w, "X-CSTP-Post-Auth-XML", xmlAuth) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,13 +33,14 @@ func checkMacvtap() { | ||||
|  | ||||
| 	ifName := "anylinkMacvtap" | ||||
| 	// 加载 macvtap | ||||
| 	cmdstr0 := fmt.Sprintln("modprobe -i macvtap") | ||||
| 	base.CheckModOrLoad("macvtap") | ||||
|  | ||||
| 	// 开启主网卡混杂模式 | ||||
| 	cmdstr1 := fmt.Sprintf("ip link set dev %s promisc on", base.Cfg.Ipv4Master) | ||||
| 	// 测试 macvtap 功能 | ||||
| 	cmdstr2 := fmt.Sprintf("ip link add link %s name %s type macvtap mode bridge", base.Cfg.Ipv4Master, ifName) | ||||
| 	cmdstr3 := fmt.Sprintf("ip link del %s", ifName) | ||||
| 	err := execCmd([]string{cmdstr0, cmdstr1, cmdstr2, cmdstr3}) | ||||
| 	err := execCmd([]string{cmdstr1, cmdstr2, cmdstr3}) | ||||
| 	if err != nil { | ||||
| 		base.Fatal(err) | ||||
| 	} | ||||
| @@ -54,7 +55,8 @@ func LinkMacvtap(cSess *sessdata.ConnSession) error { | ||||
| 	cSess.SetIfName(ifName) | ||||
|  | ||||
| 	cmdstr1 := fmt.Sprintf("ip link add link %s name %s type macvtap mode bridge", base.Cfg.Ipv4Master, ifName) | ||||
| 	cmdstr2 := fmt.Sprintf("ip link set dev %s up mtu %d address %s", ifName, cSess.Mtu, cSess.MacHw) | ||||
| 	cmdstr2 := fmt.Sprintf("ip link set dev %s up mtu %d address %s alias %s.%s", ifName, cSess.Mtu, cSess.MacHw, | ||||
| 		cSess.Group.Name, cSess.Username) | ||||
| 	err := execCmd([]string{cmdstr1, cmdstr2}) | ||||
| 	if err != nil { | ||||
| 		base.Error(err) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package handler | ||||
| import ( | ||||
| 	"crypto/md5" | ||||
| 	"encoding/binary" | ||||
| 	"runtime/debug" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/bjdgyc/anylink/base" | ||||
| @@ -101,11 +102,17 @@ func logAuditBatch() { | ||||
|  | ||||
| // 解析IP包的数据 | ||||
| func logAudit(userName string, pl *sessdata.Payload) { | ||||
| 	defer putPayload(pl) | ||||
| 	defer func() { | ||||
| 		if err := recover(); err != nil { | ||||
| 			base.Error("logAudit is panic: ", err, "\n", string(debug.Stack()), "\n", pl.Data) | ||||
| 		} | ||||
| 		putPayload(pl) | ||||
| 	}() | ||||
|  | ||||
| 	if !(pl.LType == sessdata.LTypeIPData && pl.PType == 0x00) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ipProto := waterutil.IPv4Protocol(pl.Data) | ||||
| 	// 访问协议 | ||||
| 	var accessProto uint8 | ||||
| @@ -118,11 +125,15 @@ func logAudit(userName string, pl *sessdata.Payload) { | ||||
| 	default: | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// IP报文只包含头部信息时, 则打印LOG,并退出 | ||||
| 	ipPl := waterutil.IPv4Payload(pl.Data) | ||||
| 	if len(ipPl) < 4 { | ||||
| 		base.Error("ipPl len < 4", ipPl, pl.Data) | ||||
| 		return | ||||
| 	} | ||||
| 	ipPort := (uint16(ipPl[2]) << 8) | uint16(ipPl[3]) | ||||
| 	ipSrc := waterutil.IPv4Source(pl.Data) | ||||
| 	ipDst := waterutil.IPv4Destination(pl.Data) | ||||
| 	ipPort := waterutil.IPv4DestinationPort(pl.Data) | ||||
|  | ||||
| 	b := getByte51() | ||||
| 	key := *b | ||||
| 	copy(key[:16], ipSrc) | ||||
| @@ -178,7 +189,6 @@ func logAudit(userName string, pl *sessdata.Payload) { | ||||
| 		AccessProto: accessProto, | ||||
| 		Info:        info, | ||||
| 	} | ||||
|  | ||||
| 	select { | ||||
| 	case logBatch.LogChan <- audit: | ||||
| 	default: | ||||
|   | ||||
| @@ -29,7 +29,7 @@ func onTCP(payload []byte) (uint8, string) { | ||||
| } | ||||
|  | ||||
| func sniNewParser(b []byte) (uint8, string) { | ||||
| 	if len(b) < 2 || b[0] != 0x16 || b[1] != 0x03 { | ||||
| 	if len(b) < 6 || b[0] != 0x16 || b[1] != 0x03 { | ||||
| 		return acc_proto_tcp, "" | ||||
| 	} | ||||
| 	rest := b[5:] | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
|  | ||||
| 	"github.com/bjdgyc/anylink/base" | ||||
| 	"github.com/bjdgyc/anylink/dbdata" | ||||
| 	"github.com/bjdgyc/anylink/pkg/utils" | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/pires/go-proxyproto" | ||||
| ) | ||||
| @@ -53,7 +54,6 @@ func startTls() { | ||||
| 			base.Trace("GetCertificate", chi.ServerName) | ||||
| 			return dbdata.GetCertificateBySNI(chi.ServerName) | ||||
| 		}, | ||||
| 		// InsecureSkipVerify: true, | ||||
| 	} | ||||
| 	srv := &http.Server{ | ||||
| 		Addr:         addr, | ||||
| @@ -86,6 +86,14 @@ func startTls() { | ||||
|  | ||||
| func initRoute() http.Handler { | ||||
| 	r := mux.NewRouter() | ||||
| 	// 所有路由添加安全头 | ||||
| 	r.Use(func(next http.Handler) http.Handler { | ||||
| 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||||
| 			utils.SetSecureHeader(w) | ||||
| 			next.ServeHTTP(w, req) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	r.HandleFunc("/", LinkHome).Methods(http.MethodGet) | ||||
| 	r.HandleFunc("/", LinkAuth).Methods(http.MethodPost) | ||||
| 	r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) | ||||
|   | ||||
							
								
								
									
										32
									
								
								server/pkg/utils/secure_header.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/pkg/utils/secure_header.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| package utils | ||||
|  | ||||
| import "net/http" | ||||
|  | ||||
| // SetSecureHeader 设置安全的header头 | ||||
| // https://blog.csdn.net/liwan09/article/details/130248003 | ||||
| // https://zhuanlan.zhihu.com/p/335165168 | ||||
| func SetSecureHeader(w http.ResponseWriter) { | ||||
| 	// Content-Length Date 默认已经存在 | ||||
| 	w.Header().Set("Server", "AnyLinkOpenSource") | ||||
| 	// w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||||
| 	// w.Header().Set("Transfer-Encoding", "chunked") | ||||
| 	w.Header().Set("X-Aggregate-Auth", "1") | ||||
|  | ||||
| 	w.Header().Set("Cache-Control", "no-store,no-cache") | ||||
| 	w.Header().Set("Pragma", "no-cache") | ||||
| 	w.Header().Set("Connection", "keep-alive") | ||||
| 	w.Header().Set("X-Frame-Options", "SAMEORIGIN") | ||||
| 	w.Header().Set("X-Content-Type-Options", "nosniff") | ||||
| 	w.Header().Set("X-Download-Options", "noopen") | ||||
| 	w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'; base-uri 'self'; block-all-mixed-content") | ||||
| 	w.Header().Set("X-Permitted-Cross-Domain-Policies", "none") | ||||
| 	w.Header().Set("Referrer-Policy", "same-origin") | ||||
| 	w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") | ||||
| 	w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") | ||||
| 	w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") | ||||
| 	w.Header().Set("X-XSS-Protection", "1;mode=block") | ||||
| 	w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") | ||||
|  | ||||
| 	// w.Header().Set("Clear-Site-Data", "cache,cookies,storage") | ||||
|  | ||||
| } | ||||
| @@ -11,12 +11,14 @@ Restart=on-failure | ||||
| RestartSec=5s | ||||
| ExecStart=/usr/local/anylink-deploy/anylink --conf=/usr/local/anylink-deploy/conf/server.toml | ||||
|  | ||||
| # systemctl --version | ||||
|  | ||||
| # 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 | ||||
| # systemd new than v236 | ||||
| # StandardOutput=file:/usr/local/anylink-deploy/log/anylink.log | ||||
| # StandardError=file:/usr/local/anylink-deploy/log/anylink.log | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
|   | ||||
| @@ -281,12 +281,17 @@ | ||||
|             <el-tab-pane label="路由设置" name="route"> | ||||
|                 <el-form-item label="包含路由" prop="route_include"> | ||||
|                 <el-row class="msg-info"> | ||||
|                     <el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col> | ||||
|                     <el-col :span="4"> | ||||
|                     <el-col :span="18">输入CIDR格式如: 192.168.1.0/24</el-col> | ||||
|                     <el-col :span="2"> | ||||
|                     <el-button size="mini" type="success" icon="el-icon-plus" circle | ||||
|                                 @click.prevent="addDomain(ruleForm.route_include)"></el-button> | ||||
|                     </el-col> | ||||
|                     <el-col :span="4"> | ||||
|                       <el-button size="mini" type="info" icon="el-icon-edit" circle | ||||
|                                 @click.prevent="openIpListDialog('route_include')"></el-button> | ||||
|                     </el-col>                     | ||||
|                 </el-row> | ||||
|                 <templete v-if="activeTab == 'route'"> | ||||
|                     <el-row v-for="(item,index) in ruleForm.route_include" | ||||
|                             :key="index" style="margin-bottom: 5px" :gutter="10"> | ||||
|                         <el-col :span="10"> | ||||
| @@ -300,16 +305,22 @@ | ||||
|                                     @click.prevent="removeDomain(ruleForm.route_include,index)"></el-button> | ||||
|                         </el-col> | ||||
|                     </el-row> | ||||
|                 </templete> | ||||
|                 </el-form-item> | ||||
|  | ||||
|                 <el-form-item label="排除路由" prop="route_exclude"> | ||||
|                 <el-row class="msg-info"> | ||||
|                     <el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col> | ||||
|                     <el-col :span="4"> | ||||
|                     <el-col :span="18">输入CIDR格式如: 192.168.2.0/24</el-col> | ||||
|                     <el-col :span="2"> | ||||
|                     <el-button size="mini" type="success" icon="el-icon-plus" circle | ||||
|                                 @click.prevent="addDomain(ruleForm.route_exclude)"></el-button> | ||||
|                     </el-col> | ||||
|                     <el-col :span="4"> | ||||
|                       <el-button size="mini" type="info" icon="el-icon-edit" circle | ||||
|                                 @click.prevent="openIpListDialog('route_exclude')"></el-button> | ||||
|                     </el-col>                     | ||||
|                 </el-row> | ||||
|                 <templete v-if="activeTab == 'route'"> | ||||
|                     <el-row v-for="(item,index) in ruleForm.route_exclude" | ||||
|                             :key="index" style="margin-bottom: 5px" :gutter="10"> | ||||
|                         <el-col :span="10"> | ||||
| @@ -323,6 +334,7 @@ | ||||
|                                     @click.prevent="removeDomain(ruleForm.route_exclude,index)"></el-button> | ||||
|                         </el-col> | ||||
|                     </el-row> | ||||
|                 </templete> | ||||
|                 </el-form-item> | ||||
|             </el-tab-pane> | ||||
|             <el-tab-pane label="权限控制" name="link_acl"> | ||||
| @@ -365,6 +377,7 @@ | ||||
|                 </el-form-item>                 | ||||
|                 <el-form-item label="排除域名" prop="ds_exclude_domains"> | ||||
|                     <el-input type="textarea" :rows="5" v-model="ruleForm.ds_exclude_domains" placeholder="输入域名用,号分隔,默认匹配所有子域名, 如baidu.com,163.com"></el-input> | ||||
|                     <div class="msg-info">注:域名拆分隧道,仅支持AnyConnect的桌面客户端,不支持移动端.</div> | ||||
|                 </el-form-item> | ||||
|             </el-tab-pane> | ||||
|             <el-form-item> | ||||
| @@ -398,6 +411,25 @@ | ||||
|             </el-form-item> | ||||
|         </el-form> | ||||
|     </el-dialog>  | ||||
|     <!--编辑模式弹窗--> | ||||
|     <el-dialog | ||||
|     :close-on-click-modal="false" | ||||
|     title="编辑模式" | ||||
|     :visible.sync="ipListDialog" | ||||
|     width="650px" | ||||
|     custom-class="valgin-dialog" | ||||
|     center> | ||||
|       <el-form ref="ipEditForm" label-width="80px"> | ||||
|           <el-form-item label="路由表" prop="ip_list"> | ||||
|               <el-input type="textarea" :rows="10" v-model="ipEditForm.ip_list" placeholder="每行一条路由,例:192.168.1.0/24,备注 或 192.168.1.0/24"></el-input> | ||||
|               <div class="msg-info">当前共 {{ ipEditForm.ip_list.trim() === '' ? 0 : ipEditForm.ip_list.trim().split("\n").length }} 条(注:AnyConnect客户端最多支持{{ this.maxRouteRows }}条路由)</div> | ||||
|           </el-form-item> | ||||
|           <el-form-item> | ||||
|               <el-button type="primary" @click="ipEdit()" :loading="ipEditLoading">更新</el-button> | ||||
|               <el-button @click="ipListDialog = false">取 消</el-button> | ||||
|           </el-form-item> | ||||
|       </el-form> | ||||
|     </el-dialog> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -424,6 +456,7 @@ export default { | ||||
|       activeTab : "general", | ||||
|       readMore: {}, | ||||
|       readMinRows : 5, | ||||
|       maxRouteRows : 2500, | ||||
|       defAuth : { | ||||
|                 type:'local',  | ||||
|                 radius:{addr:"", secret:""}, | ||||
| @@ -450,11 +483,17 @@ export default { | ||||
|         auth : {}, | ||||
|       }, | ||||
|       authLoginDialog : false, | ||||
|       ipListDialog : false, | ||||
|       authLoginLoading : false,       | ||||
|       authLoginForm : { | ||||
|         name : "", | ||||
|         pwd : "", | ||||
|       }, | ||||
|       ipEditForm: { | ||||
|         ip_list: "", | ||||
|         type : "", | ||||
|       }, | ||||
|       ipEditLoading : false, | ||||
|       authLoginRules: { | ||||
|         name: [ | ||||
|           {required: true, message: '请输入账号', trigger: 'blur'}, | ||||
| @@ -644,6 +683,70 @@ export default { | ||||
|         }); | ||||
|       }); | ||||
|     }, | ||||
|     openIpListDialog(type) { | ||||
|       this.ipListDialog = true; | ||||
|       this.ipEditForm.type = type; | ||||
|       this.ipEditForm.ip_list = this.ruleForm[type].map(item => item.val + (item.note ? "," + item.note : "")).join("\n");       | ||||
|     }, | ||||
|     ipEdit() { | ||||
|         this.ipEditLoading = true; | ||||
|         let ipList = []; | ||||
|         if (this.ipEditForm.ip_list.trim() !== "") { | ||||
|             ipList = this.ipEditForm.ip_list.trim().split("\n"); | ||||
|         }         | ||||
|         let arr = []; | ||||
|         for (let i = 0; i < ipList.length; i++) { | ||||
|           let item = ipList[i]; | ||||
|           if (item.trim() === "") { | ||||
|             continue; | ||||
|           } | ||||
|           let ip = item.split(","); | ||||
|           if (ip.length > 2) { | ||||
|             ip[1] = ip.slice(1).join(","); | ||||
|           } | ||||
|           let note = ip[1] ? ip[1] : ""; | ||||
|           const pushToArr = () => { | ||||
|             arr.push({val: ip[0], note: note}); | ||||
|           }; | ||||
|           if (this.ipEditForm.type == "route_include" && ip[0] == "all") { | ||||
|             pushToArr(); | ||||
|             continue;   | ||||
|           } | ||||
|           let valid = this.isValidCIDR(ip[0]); | ||||
|           if (!valid.valid) { | ||||
|                 this.$message.error("错误:CIDR格式错误,建议 " + ip[0] + " 改为 " + valid.suggestion); | ||||
|                 this.ipEditLoading = false; | ||||
|                 return; | ||||
|           } | ||||
|           pushToArr(); | ||||
|         } | ||||
|         this.ruleForm[this.ipEditForm.type] = arr; | ||||
|         this.ipEditLoading = false; | ||||
|         this.ipListDialog = false; | ||||
|     }, | ||||
|     isValidCIDR(input) { | ||||
|         const cidrRegex = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)\/([12]?\d|3[0-2])$/; | ||||
|         if (!cidrRegex.test(input)) { | ||||
|             return { valid: false, suggestion: null }; | ||||
|         } | ||||
|         const [ip, mask] = input.split('/'); | ||||
|         const maskNum = parseInt(mask); | ||||
|         const ipParts = ip.split('.').map(part => parseInt(part)); | ||||
|         const binaryIP = ipParts.map(part => part.toString(2).padStart(8, '0')).join(''); | ||||
|         for (let i = maskNum; i < 32; i++) { | ||||
|             if (binaryIP[i] === '1') { | ||||
|                 const binaryNetworkPart = binaryIP.substring(0, maskNum).padEnd(32, '0'); | ||||
|                 const networkIPParts = []; | ||||
|                 for (let j = 0; j < 4; j++) { | ||||
|                     const octet = binaryNetworkPart.substring(j * 8, (j + 1) * 8); | ||||
|                     networkIPParts.push(parseInt(octet, 2)); | ||||
|                 } | ||||
|                 const suggestedIP = networkIPParts.join('.'); | ||||
|                 return { valid: false, suggestion: `${suggestedIP}/${mask}` }; | ||||
|             } | ||||
|         } | ||||
|         return { valid: true, suggestion: null }; | ||||
|     }, | ||||
|     resetForm(formName) { | ||||
|       this.$refs[formName].resetFields(); | ||||
|     }, | ||||
|   | ||||
| @@ -248,11 +248,14 @@ | ||||
|           <el-form-item label="自定义首页" prop="homeindex"> | ||||
|             <el-input | ||||
|               type="textarea" | ||||
|               :rows="5" | ||||
|               :rows="10" | ||||
|               placeholder="请输入内容" | ||||
|               v-model="dataOther.homeindex" | ||||
|             > | ||||
|             </el-input> | ||||
|             <el-tooltip content="自定义内容可以参考 home 目录下的文件" placement="top"> | ||||
|               <i class="el-icon-question"></i> | ||||
|             </el-tooltip> | ||||
|           </el-form-item> | ||||
|  | ||||
|           <el-form-item label="账户开通邮件" prop="account_mail"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user