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 是一款面向企业级的远程办公 SSL VPN 软件,支持多人同时在线使用。它提供安全、便捷的访问内部网络资源的方式,使远程工作者能够有效协作。
+
+ 核心功能
+
+ - 安全远程访问:AnyLink 使用 SSL/TLS 加密技术,确保远程用户与企业网络之间的连接安全可靠。
+ - 多用户支持:多个用户可以同时连接 VPN,实现不同地点团队的无缝协作。
+ - 灵活网络访问:AnyLink 能够安全地让远程工作者访问内部资源,如文件、应用程序和数据库。
+ - 集中化管理:该 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 @@
+
+
+