diff --git a/README.md b/README.md index c4c67c2..2aad3e5 100644 --- a/README.md +++ b/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 群共享文件有相关软件下载 diff --git a/build.sh b/build.sh old mode 100755 new mode 100644 index 8abf20e..057bff0 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/build_docker.sh b/build_docker.sh index ec94d0f..ef8afac 100644 --- a/build_docker.sh +++ b/build_docker.sh @@ -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 diff --git a/doc/README.md b/doc/README.md index a56d567..6953384 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,37 +9,39 @@ ## Donator > 感谢以下同学的打赏,AnyLink 有你更美好! -> +> > 需要展示主页的同学,可以在QQ群 直接联系我添加。 -| 昵称 | 主页 | -|---------| ---------------------------- | -| 代码 oo8 | | -| 甘磊 | https://github.com/ganlei333 | -| Oo@ | https://github.com/chooop | -| 虚极静笃 | | -| 请喝可乐 | | -| 加油加油 | | -| 李建 | | -| lanbin | | -| 乐在东途 | | -| 孤鸿 | | -| 刘国华 | | -| 改名好无聊 | | -| 全能互联网专家 | | -| JCM | | -| Eh... | | -| 沉 | | -| 刘国华 | | -| 忧郁的豚骨拉面 | | -| 张小旋当爹地 | | -| 对方正在输入 | | -| Ronny | | -| 奔跑的少年 | | -| ZBW | | -| 悲鸣 | | -| 谢谢 | | -| 云思科技 | | +| 昵称 | 主页 / 留言 | +|-----------|------------------------------| +| 代码 oo8 | | +| 甘磊 | https://github.com/ganlei333 | +| Oo@ | https://github.com/chooop | +| 虚极静笃 | | +| 请喝可乐 | | +| 加油加油 | | +| 李建 | | +| lanbin | | +| 乐在东途 | | +| 孤鸿 | | +| 刘国华 | | +| 改名好无聊 | | +| 全能互联网专家 | | +| JCM | | +| Eh... | | +| 沉 | | +| 刘国华 | | +| 忧郁的豚骨拉面 | | +| 张小旋当爹地 | | +| 对方正在输入 | | +| Ronny | | +| 奔跑的少年 | | +| ZBW | | +| 悲鸣 | | +| 谢谢 | | +| 云思科技 | | +| 哆啦A伟(张佳伟) | | +| nobody | 开源不易,感谢分享 | diff --git a/doc/question.md b/doc/question.md index 190da8a..82334c2 100644 --- a/doc/question.md +++ b/doc/question.md @@ -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; + } +} + +``` + ### 性能问题 ``` 内网环境测试数据 diff --git a/docker/Dockerfile b/docker/Dockerfile index b1a5cea..af5cde3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 diff --git a/home/自定义首页1.html b/home/自定义首页1.html new file mode 100644 index 0000000..171febc --- /dev/null +++ b/home/自定义首页1.html @@ -0,0 +1,101 @@ + + + + + AnyLink - 企业级远程办公 SSL VPN + + + +
+

欢迎使用 AnyLink

+
+ +
+

什么是 AnyLink?

+

AnyLink 是一款面向企业级的远程办公 SSL VPN 软件,支持多人同时在线使用。它提供安全、便捷的访问内部网络资源的方式,使远程工作者能够有效协作。

+ +

核心功能

+ + +

开始使用 AnyLink

+

体验 AnyLink 为您的企业远程办公需求所带来的便利和安全。

+ +

下载客户端

+ Windows 客户端 + Mac 客户端 + + iOS 客户端 + Android 客户端 + + Android FreeOTP客户端 + iOS FreeOTP客户端 +

使用手册

+ 使用手册(Windows) +
+ + + + diff --git a/server/admin/api_base.go b/server/admin/api_base.go index fa63b39..61c81a6 100644 --- a/server/admin/api_base.go +++ b/server/admin/api_base.go @@ -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) diff --git a/server/admin/server.go b/server/admin/server.go index 3cabb99..b162755 100644 --- a/server/admin/server.go +++ b/server/admin/server.go @@ -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) diff --git a/server/base/app_ver.go b/server/base/app_ver.go index e3556a6..fa9e89f 100644 --- a/server/base/app_ver.go +++ b/server/base/app_ver.go @@ -3,5 +3,5 @@ package base const ( APP_NAME = "AnyLink" // app版本号 - APP_VER = "0.9.4" + APP_VER = "0.10.1" ) diff --git a/server/base/mod.go b/server/base/mod.go new file mode 100644 index 0000000..0f85830 --- /dev/null +++ b/server/base/mod.go @@ -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) + } +} diff --git a/server/base/start.go b/server/base/start.go index a2354c6..c25d202 100644 --- a/server/base/start.go +++ b/server/base/start.go @@ -4,6 +4,7 @@ func Start() { execute() initCfg() initLog() + initMod() } func Test() { diff --git a/server/conf/files/info.txt b/server/conf/files/info.txt index 2e7e2e0..9f89a52 100644 --- a/server/conf/files/info.txt +++ b/server/conf/files/info.txt @@ -1,2 +1,2 @@ 客户端软件需放置在files目录内, -如需要帮助请加QQ群:567510628 \ No newline at end of file +如需要帮助请加QQ群:567510628 、739072205 \ No newline at end of file diff --git a/server/conf/profile.xml b/server/conf/profile.xml index 0df0912..914f53d 100644 --- a/server/conf/profile.xml +++ b/server/conf/profile.xml @@ -8,6 +8,7 @@ false IPSec true + false AllowRemoteUsers AllowRemoteUsers pinAllowed @@ -20,15 +21,19 @@ - - localhost - + - VPN Server + VPN localhost + + + VPN2 + localhost2 + + \ No newline at end of file diff --git a/server/conf/server.toml b/server/conf/server.toml index 7969253..40f2213 100644 --- a/server/conf/server.toml +++ b/server/conf/server.toml @@ -41,6 +41,6 @@ iptables_nat = true #客户端显示详细错误信息(线上环境慎开启) -display_error = false +display_error = true diff --git a/server/handler/dtls.go b/server/handler/dtls.go index 56e54a9..f9f27a2 100644 --- a/server/handler/dtls.go +++ b/server/handler/dtls.go @@ -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,12 +40,17 @@ 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}, - LoggerFactory: logf, - MTU: BufferSize, - SessionStore: sessStore, + CipherSuites: func() []dtls.CipherSuiteID { + var cs = []dtls.CipherSuiteID{} + for _, vv := range dtlsCipherSuites { + cs = append(cs, vv) + } + return cs + }(), + LoggerFactory: logf, + MTU: BufferSize, + SessionStore: sessStore, ConnectContextMaker: func() (context.Context, func()) { return context.WithTimeout(context.Background(), 5*time.Second) }, @@ -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" +} diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index 2e9bf8f..9e3dd6f 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -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) } diff --git a/server/handler/link_base.go b/server/handler/link_base.go index 7581bcc..48e2258 100644 --- a/server/handler/link_base.go +++ b/server/handler/link_base.go @@ -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) diff --git a/server/handler/link_home.go b/server/handler/link_home.go index a2dc30b..066e6f1 100644 --- a/server/handler/link_home.go +++ b/server/handler/link_home.go @@ -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")) { diff --git a/server/handler/link_tun.go b/server/handler/link_tun.go index 0ba7c76..5ba65f2 100644 --- a/server/handler/link_tun.go +++ b/server/handler/link_tun.go @@ -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转发规则 + // 添加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}) diff --git a/server/handler/link_tunnel.go b/server/handler/link_tunnel.go index 4550854..3fd5b8b 100644 --- a/server/handler/link_tunnel.go +++ b/server/handler/link_tunnel.go @@ -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 } diff --git a/server/handler/link_vtap.go b/server/handler/link_vtap.go index 399f9da..fa2c5b0 100644 --- a/server/handler/link_vtap.go +++ b/server/handler/link_vtap.go @@ -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) diff --git a/server/handler/payload_access_audit.go b/server/handler/payload_access_audit.go index 4384352..a3cc0c2 100644 --- a/server/handler/payload_access_audit.go +++ b/server/handler/payload_access_audit.go @@ -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: diff --git a/server/handler/payload_tcp_parser.go b/server/handler/payload_tcp_parser.go index 6d3c4cc..95c9a0d 100644 --- a/server/handler/payload_tcp_parser.go +++ b/server/handler/payload_tcp_parser.go @@ -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:] diff --git a/server/handler/server.go b/server/handler/server.go index 34fd016..e8938e4 100644 --- a/server/handler/server.go +++ b/server/handler/server.go @@ -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) diff --git a/server/pkg/utils/secure_header.go b/server/pkg/utils/secure_header.go new file mode 100644 index 0000000..ae83774 --- /dev/null +++ b/server/pkg/utils/secure_header.go @@ -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") + +} diff --git a/systemd/anylink.service b/systemd/anylink.service index 72c3297..c2023a2 100644 --- a/systemd/anylink.service +++ b/systemd/anylink.service @@ -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 diff --git a/web/src/pages/group/List.vue b/web/src/pages/group/List.vue index 4f71eb7..6d5975c 100644 --- a/web/src/pages/group/List.vue +++ b/web/src/pages/group/List.vue @@ -281,48 +281,60 @@ - 输入CIDR格式如: 192.168.1.0/24 - + 输入CIDR格式如: 192.168.1.0/24 + + + + - - - - - - - - - - - + + + + + + + + + + + + + - 输入CIDR格式如: 192.168.2.0/24 - + 输入CIDR格式如: 192.168.2.0/24 + + + + - - - - - - - - - - - + + + + + + + + + + + + + @@ -333,7 +345,7 @@ - + @@ -365,6 +377,7 @@ +
注:域名拆分隧道,仅支持AnyConnect的桌面客户端,不支持移动端.
@@ -398,6 +411,25 @@ + + + + + +
当前共 {{ ipEditForm.ip_list.trim() === '' ? 0 : ipEditForm.ip_list.trim().split("\n").length }} 条(注:AnyConnect客户端最多支持{{ this.maxRouteRows }}条路由)
+
+ + 更新 + 取 消 + +
+
@@ -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, - authLoginLoading : 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(); }, diff --git a/web/src/pages/set/Other.vue b/web/src/pages/set/Other.vue index d7375bf..84b0b5c 100644 --- a/web/src/pages/set/Other.vue +++ b/web/src/pages/set/Other.vue @@ -248,11 +248,14 @@ + + +