mirror of
https://github.com/bjdgyc/anylink.git
synced 2025-09-29 00:19:36 +08:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
06931551d0 | ||
|
3aff1daaf2 | ||
|
918859cc62 | ||
|
9f98f71210 | ||
|
116c04f8b1 | ||
|
4b36577b00 | ||
|
f208be9126 | ||
|
2b9c0fdca8 | ||
|
04d025c68b | ||
|
7f1e769377 | ||
|
c76b074893 | ||
|
54f7a59a91 | ||
|
25cb1fc19a | ||
|
6a997bfd46 | ||
|
68076f58eb | ||
|
e24aa2d900 | ||
|
9dad2c08a5 | ||
|
8ede613488 | ||
|
f46a30488a | ||
|
b06c035cce | ||
|
730c34b2a4 | ||
|
c38f1e9b8c | ||
|
a450fe3eef | ||
|
e9c55a0853 | ||
|
f560fc459b | ||
|
d4f266de66 | ||
|
3e491d33c6 | ||
|
77e507fab7 | ||
|
9d5b9c4ded | ||
|
a0830f0440 | ||
|
3de75285de | ||
|
cde46c5765 | ||
|
f325970089 | ||
|
f6996363e3 | ||
|
90aa6c272d | ||
|
2964b34087 | ||
|
625e4eecf4 | ||
|
522be82a31 | ||
|
ccce143f85 | ||
|
01431b2230 | ||
|
7a92aa8dff | ||
|
2b580067a2 | ||
|
500a11612c | ||
|
64fc4d082e | ||
|
cf507d204b | ||
|
fd7242dbba | ||
|
9a712ca489 | ||
|
e44e1dcf2a | ||
|
fce96753c5 | ||
|
77efe7583d | ||
|
1cbe9bfc30 | ||
|
2e7afa9c35 | ||
|
a58507f5a6 | ||
|
1989b235fe | ||
|
21b047b307 | ||
|
2daad88159 | ||
|
7c86513b0c | ||
|
4b41e6c5b9 | ||
|
684fea69d0 | ||
|
85e2ba0b0f | ||
|
3fff44dde5 | ||
|
8b2b058450 | ||
|
f0305415ae | ||
|
ea84a29350 | ||
|
6c5969c5ea | ||
|
e780afe18c |
10
Dockerfile
10
Dockerfile
@@ -2,12 +2,12 @@
|
|||||||
FROM node:lts-alpine as builder_node
|
FROM node:lts-alpine as builder_node
|
||||||
WORKDIR /web
|
WORKDIR /web
|
||||||
COPY ./web /web
|
COPY ./web /web
|
||||||
RUN npm install --registry=https://registry.npm.taobao.org \
|
RUN yarn install \
|
||||||
&& npm run build \
|
&& yarn run build \
|
||||||
&& ls /web/ui
|
&& ls /web/ui
|
||||||
|
|
||||||
# server
|
# server
|
||||||
FROM golang:1.16-alpine as builder_golang
|
FROM golang:1.17-alpine as builder_golang
|
||||||
#TODO 本地打包时使用镜像
|
#TODO 本地打包时使用镜像
|
||||||
ENV GOPROXY=https://goproxy.io
|
ENV GOPROXY=https://goproxy.io
|
||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
@@ -18,7 +18,7 @@ COPY --from=builder_node /web/ui /anylink/server/ui
|
|||||||
#TODO 本地打包时使用镜像
|
#TODO 本地打包时使用镜像
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
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 apk add --no-cache git gcc musl-dev
|
||||||
RUN cd /anylink/server;go build -o anylink -ldflags "-X main.CommitId=$(git rev-parse HEAD)" \
|
RUN cd /anylink/server;go mod tidy;go build -o anylink -ldflags "-X main.CommitId=$(git rev-parse HEAD)" \
|
||||||
&& /anylink/server/anylink tool -v
|
&& /anylink/server/anylink tool -v
|
||||||
|
|
||||||
# anylink
|
# anylink
|
||||||
@@ -33,7 +33,7 @@ COPY docker_entrypoint.sh /app/
|
|||||||
|
|
||||||
COPY ./server/bridge-init.sh /app/
|
COPY ./server/bridge-init.sh /app/
|
||||||
COPY ./server/conf /app/conf
|
COPY ./server/conf /app/conf
|
||||||
#COPY ./server/files /app/conf/files
|
COPY ./LICENSE /app/LICENSE
|
||||||
|
|
||||||
|
|
||||||
#TODO 本地打包时使用镜像
|
#TODO 本地打包时使用镜像
|
||||||
|
52
README.md
52
README.md
@@ -6,6 +6,7 @@
|
|||||||
[](https://codecov.io/gh/bjdgyc/anylink)
|
[](https://codecov.io/gh/bjdgyc/anylink)
|
||||||

|

|
||||||

|

|
||||||
|
[](https://hub.docker.com/r/bjdgyc/anylink)
|
||||||

|

|
||||||
|
|
||||||
AnyLink 是一个企业级远程办公 sslvpn 的软件,可以支持多人同时在线使用。
|
AnyLink 是一个企业级远程办公 sslvpn 的软件,可以支持多人同时在线使用。
|
||||||
@@ -29,6 +30,16 @@ AnyLink 服务端仅在 CentOS 7、Ubuntu 18.04 测试通过,如需要安装
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
> 如果您觉得 anylink 对你有帮助,欢迎给我们打赏,也是帮助 anylink 更好的发展。
|
||||||
|
>
|
||||||
|
> [查看打赏列表](doc/README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="doc/screenshot/wxpay2.png" width="400" />
|
||||||
|
</p>
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
> 没有编程基础的同学建议直接下载 release 包,从下面的地址下载 anylink-deploy.tar.gz
|
> 没有编程基础的同学建议直接下载 release 包,从下面的地址下载 anylink-deploy.tar.gz
|
||||||
@@ -36,6 +47,7 @@ AnyLink 服务端仅在 CentOS 7、Ubuntu 18.04 测试通过,如需要安装
|
|||||||
> https://github.com/bjdgyc/anylink/releases
|
> https://github.com/bjdgyc/anylink/releases
|
||||||
|
|
||||||
### 使用问题
|
### 使用问题
|
||||||
|
|
||||||
> 对于测试环境,可以使用 vpn.test.vqilu.cn 绑定host进行测试
|
> 对于测试环境,可以使用 vpn.test.vqilu.cn 绑定host进行测试
|
||||||
>
|
>
|
||||||
> 对于线上环境,必须申请安全的 https 证书,不支持私有证书连接
|
> 对于线上环境,必须申请安全的 https 证书,不支持私有证书连接
|
||||||
@@ -44,7 +56,7 @@ AnyLink 服务端仅在 CentOS 7、Ubuntu 18.04 测试通过,如需要安装
|
|||||||
|
|
||||||
### 自行编译安装
|
### 自行编译安装
|
||||||
|
|
||||||
> 需要提前安装好 golang >= 1.16 和 nodejs >= 14.x
|
> 需要提前安装好 golang >= 1.17 和 nodejs >= 14.x 和 yarn >= v1.22.x
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/bjdgyc/anylink.git
|
git clone https://github.com/bjdgyc/anylink.git
|
||||||
@@ -57,7 +69,7 @@ cd anylink-deploy
|
|||||||
sudo ./anylink
|
sudo ./anylink
|
||||||
|
|
||||||
# 默认管理后台访问地址
|
# 默认管理后台访问地址
|
||||||
# http://host:8800
|
# https://host:8800
|
||||||
# 默认账号 密码
|
# 默认账号 密码
|
||||||
# admin 123456
|
# admin 123456
|
||||||
|
|
||||||
@@ -69,18 +81,22 @@ sudo ./anylink
|
|||||||
- [x] TLS-TCP 通道
|
- [x] TLS-TCP 通道
|
||||||
- [x] DTLS-UDP 通道
|
- [x] DTLS-UDP 通道
|
||||||
- [x] 兼容 AnyConnect
|
- [x] 兼容 AnyConnect
|
||||||
|
- [x] 兼容 OpenConnect
|
||||||
- [x] 基于 tun 设备的 nat 访问模式
|
- [x] 基于 tun 设备的 nat 访问模式
|
||||||
- [x] 基于 tap 设备的桥接访问模式
|
- [x] 基于 tap 设备的桥接访问模式
|
||||||
- [x] 基于 macvtap 设备的桥接访问模式
|
- [x] 基于 macvtap 设备的桥接访问模式
|
||||||
- [x] 支持 [proxy protocol v1](http://www.haproxy.org/download/2.2/doc/proxy-protocol.txt) 协议
|
- [x] 支持 [proxy protocol v1](http://www.haproxy.org/download/2.2/doc/proxy-protocol.txt) 协议
|
||||||
- [x] 用户组支持
|
- [x] 用户组支持
|
||||||
- [x] 多用户支持
|
- [x] 多用户支持
|
||||||
|
- [x] 用户策略支持
|
||||||
- [x] TOTP 令牌支持
|
- [x] TOTP 令牌支持
|
||||||
- [x] TOTP 令牌开关
|
- [x] TOTP 令牌开关
|
||||||
- [x] 流量速率限制
|
- [x] 流量速率限制
|
||||||
- [x] 后台管理界面
|
- [x] 后台管理界面
|
||||||
- [x] 访问权限管理
|
- [x] 访问权限管理
|
||||||
- [x] IP 访问审计功能
|
- [x] IP 访问审计功能
|
||||||
|
- [x] 域名动态拆分隧道(域名路由功能)
|
||||||
|
- [x] radius认证支持
|
||||||
- [ ] 基于 ipvtap 设备的桥接访问模式
|
- [ ] 基于 ipvtap 设备的桥接访问模式
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
@@ -113,7 +129,8 @@ sudo ./anylink
|
|||||||
|
|
||||||
网络模式选择,需要配置 `link_mode` 参数,如 `link_mode="tun"`,`link_mode="macvtap"`,`link_mode="tap"(不推荐)` 等参数。 不同的参数需要对服务器做相应的设置。
|
网络模式选择,需要配置 `link_mode` 参数,如 `link_mode="tun"`,`link_mode="macvtap"`,`link_mode="tap"(不推荐)` 等参数。 不同的参数需要对服务器做相应的设置。
|
||||||
|
|
||||||
建议优先选择 tun 模式,其次选择 macvtap 模式,因客户端传输的是 IP 层数据,无须进行数据转换。 tap 模式是在用户态做的链路层到 IP 层的数据互相转换,性能会有所下降。 如果需要在虚拟机内开启 tap 模式,请确认虚拟机的网卡开启混杂模式。
|
建议优先选择 tun 模式,其次选择 macvtap 模式,因客户端传输的是 IP 层数据,无须进行数据转换。 tap 模式是在用户态做的链路层到 IP 层的数据互相转换,性能会有所下降。 如果需要在虚拟机内开启 tap
|
||||||
|
模式,请确认虚拟机的网卡开启混杂模式。
|
||||||
|
|
||||||
### tun 设置
|
### tun 设置
|
||||||
|
|
||||||
@@ -125,6 +142,9 @@ net.ipv4.ip_forward = 1
|
|||||||
|
|
||||||
#执行如下命令
|
#执行如下命令
|
||||||
sysctl -w net.ipv4.ip_forward=1
|
sysctl -w net.ipv4.ip_forward=1
|
||||||
|
|
||||||
|
# 查看设置是否生效
|
||||||
|
cat /proc/sys/net/ipv4/ip_forward
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 设置 nat 转发规则
|
2. 设置 nat 转发规则
|
||||||
@@ -189,18 +209,18 @@ sh bridge-init.sh
|
|||||||
|
|
||||||
1. 添加 anylink 程序
|
1. 添加 anylink 程序
|
||||||
|
|
||||||
- anylink 程序目录放入 `/usr/local/anylink-deploy`
|
- anylink 程序目录放入 `/usr/local/anylink-deploy`
|
||||||
|
|
||||||
2. systemd/anylink.service 脚本放入:
|
2. systemd/anylink.service 脚本放入:
|
||||||
|
|
||||||
- centos: `/usr/lib/systemd/system/`
|
- centos: `/usr/lib/systemd/system/`
|
||||||
- ubuntu: `/lib/systemd/system/`
|
- ubuntu: `/lib/systemd/system/`
|
||||||
|
|
||||||
3. 操作命令:
|
3. 操作命令:
|
||||||
|
|
||||||
- 启动: `systemctl start anylink`
|
- 启动: `systemctl start anylink`
|
||||||
- 停止: `systemctl stop anylink`
|
- 停止: `systemctl stop anylink`
|
||||||
- 开机自启: `systemctl enable anylink`
|
- 开机自启: `systemctl enable anylink`
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
@@ -233,14 +253,15 @@ sh bridge-init.sh
|
|||||||
5. 启动容器
|
5. 启动容器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# -e IPV4_CIDR=192.168.10.0/24 这个参数要与配置文件内的网段一致
|
||||||
docker run -itd --name anylink --privileged \
|
docker run -itd --name anylink --privileged \
|
||||||
|
-e IPV4_CIDR=192.168.10.0/24
|
||||||
-p 443:443 -p 8800:8800 \
|
-p 443:443 -p 8800:8800 \
|
||||||
--restart=always \
|
--restart=always \
|
||||||
bjdgyc/anylink
|
bjdgyc/anylink
|
||||||
```
|
```
|
||||||
|
|
||||||
6. 使用自定义参数启动容器
|
6. 使用自定义参数启动容器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 参数可以参考 -h 命令
|
# 参数可以参考 -h 命令
|
||||||
docker run -itd --name anylink --privileged \
|
docker run -itd --name anylink --privileged \
|
||||||
@@ -260,20 +281,11 @@ sh bridge-init.sh
|
|||||||
docker build -t anylink .
|
docker build -t anylink .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
请前往 [问题地址](doc/question.md) 查看具体信息
|
请前往 [问题地址](doc/question.md) 查看具体信息
|
||||||
|
|
||||||
## Donate
|
|
||||||
|
|
||||||
> 如果您觉得 anylink 对你有帮助,欢迎给我们打赏,也是帮助 anylink 更好的发展。
|
|
||||||
>
|
|
||||||
> [查看打赏列表](doc/README.md)
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<img src="doc/screenshot/wxpay2.png" width="400" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Discussion
|
## Discussion
|
||||||
|
|
||||||
添加 QQ 群: 567510628
|
添加 QQ 群: 567510628
|
||||||
|
10
build.sh
10
build.sh
@@ -16,9 +16,13 @@ echo "编译前端项目"
|
|||||||
cd $cpath/web
|
cd $cpath/web
|
||||||
#国内可替换源加快速度
|
#国内可替换源加快速度
|
||||||
#npx browserslist@latest --update-db
|
#npx browserslist@latest --update-db
|
||||||
npm install --registry=https://registry.npm.taobao.org
|
#npm install --registry=https://registry.npm.taobao.org
|
||||||
#npm install
|
#npm install
|
||||||
npm run build
|
#npm run build
|
||||||
|
|
||||||
|
yarn install
|
||||||
|
yarn run build
|
||||||
|
|
||||||
RETVAL $?
|
RETVAL $?
|
||||||
|
|
||||||
echo "编译二进制文件"
|
echo "编译二进制文件"
|
||||||
@@ -27,6 +31,7 @@ rm -rf ui
|
|||||||
cp -rf $cpath/web/ui .
|
cp -rf $cpath/web/ui .
|
||||||
#国内可替换源加快速度
|
#国内可替换源加快速度
|
||||||
export GOPROXY=https://goproxy.io
|
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 "-X main.CommitId=$(git rev-parse HEAD)"
|
||||||
RETVAL $?
|
RETVAL $?
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@ cp -r server/bridge-init.sh $deploy
|
|||||||
cp -r server/conf $deploy
|
cp -r server/conf $deploy
|
||||||
|
|
||||||
cp -r systemd $deploy
|
cp -r systemd $deploy
|
||||||
|
cp -r LICENSE $deploy
|
||||||
|
|
||||||
tar zcvf ${deploy}.tar.gz $deploy
|
tar zcvf ${deploy}.tar.gz $deploy
|
||||||
|
|
||||||
|
@@ -10,11 +10,17 @@
|
|||||||
|
|
||||||
> 感谢以下同学的打赏,AnyLink 有你更美好!
|
> 感谢以下同学的打赏,AnyLink 有你更美好!
|
||||||
|
|
||||||
| 昵称 | 主页 |
|
| 昵称 | 主页 |
|
||||||
| -------- | ---------------------------- |
|
| -- | ---------------------------- |
|
||||||
| 代码oo8 | |
|
| 代码oo8 | |
|
||||||
| 甘磊 | https://github.com/ganlei333 |
|
| 甘磊 | https://github.com/ganlei333 |
|
||||||
| Oo@ | https://github.com/chooop |
|
| Oo@ | https://github.com/chooop |
|
||||||
| 虚极静笃 | |
|
| 虚极静笃 | |
|
||||||
| Ficapy | |
|
| 请喝可乐 | |
|
||||||
|
| 加油加油 | |
|
||||||
|
| 李建 | |
|
||||||
|
| lanbin | |
|
||||||
|
| 乐在东途 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -18,6 +18,6 @@ case $var1 in
|
|||||||
iptables -t nat -A POSTROUTING -s "${IPV4_CIDR}" -o eth0+ -j MASQUERADE
|
iptables -t nat -A POSTROUTING -s "${IPV4_CIDR}" -o eth0+ -j MASQUERADE
|
||||||
iptables -nL -t nat
|
iptables -nL -t nat
|
||||||
|
|
||||||
/app/anylink "$@"
|
exec /app/anylink "$@"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
@@ -62,7 +62,9 @@ func GroupDetail(w http.ResponseWriter, r *http.Request) {
|
|||||||
RespError(w, RespInternalErr, err)
|
RespError(w, RespInternalErr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(data.Auth) == 0 {
|
||||||
|
data.Auth["type"] = "local"
|
||||||
|
}
|
||||||
RespSucess(w, data)
|
RespSucess(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
98
server/admin/api_policy.go
Normal file
98
server/admin/api_policy.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/bjdgyc/anylink/dbdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PolicyList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
pageS := r.FormValue("page")
|
||||||
|
page, _ := strconv.Atoi(pageS)
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageSize = dbdata.PageSize
|
||||||
|
|
||||||
|
count := dbdata.CountAll(&dbdata.Policy{})
|
||||||
|
|
||||||
|
var datas []dbdata.Policy
|
||||||
|
err := dbdata.Find(&datas, pageSize, page)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"count": count,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"datas": datas,
|
||||||
|
}
|
||||||
|
|
||||||
|
RespSucess(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PolicyDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
idS := r.FormValue("id")
|
||||||
|
id, _ := strconv.Atoi(idS)
|
||||||
|
if id < 1 {
|
||||||
|
RespError(w, RespParamErr, "Id错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data dbdata.Policy
|
||||||
|
err := dbdata.One("Id", id, &data)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespSucess(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PolicySet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
v := &dbdata.Policy{}
|
||||||
|
err = json.Unmarshal(body, v)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbdata.SetPolicy(v)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespSucess(w, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PolicyDel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
idS := r.FormValue("id")
|
||||||
|
id, _ := strconv.Atoi(idS)
|
||||||
|
if id < 1 {
|
||||||
|
RespError(w, RespParamErr, "Id错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := dbdata.Policy{Id: id}
|
||||||
|
err := dbdata.Del(&data)
|
||||||
|
if err != nil {
|
||||||
|
RespError(w, RespInternalErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
RespSucess(w, nil)
|
||||||
|
}
|
@@ -2,6 +2,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"embed"
|
"embed"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
@@ -18,6 +19,11 @@ func StartAdmin() {
|
|||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.Use(authMiddleware)
|
r.Use(authMiddleware)
|
||||||
|
|
||||||
|
// 监控检测
|
||||||
|
r.HandleFunc("/status.html", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}).Name("index")
|
||||||
|
|
||||||
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound)).Name("index")
|
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound)).Name("index")
|
||||||
r.PathPrefix("/ui/").Handler(
|
r.PathPrefix("/ui/").Handler(
|
||||||
// http.StripPrefix("/ui/", http.FileServer(http.Dir(base.Cfg.UiPath))),
|
// http.StripPrefix("/ui/", http.FileServer(http.Dir(base.Cfg.UiPath))),
|
||||||
@@ -46,6 +52,10 @@ func StartAdmin() {
|
|||||||
r.HandleFunc("/user/ip_map/detail", UserIpMapDetail)
|
r.HandleFunc("/user/ip_map/detail", UserIpMapDetail)
|
||||||
r.HandleFunc("/user/ip_map/set", UserIpMapSet)
|
r.HandleFunc("/user/ip_map/set", UserIpMapSet)
|
||||||
r.HandleFunc("/user/ip_map/del", UserIpMapDel)
|
r.HandleFunc("/user/ip_map/del", UserIpMapDel)
|
||||||
|
r.HandleFunc("/user/policy/list", PolicyList)
|
||||||
|
r.HandleFunc("/user/policy/detail", PolicyDetail)
|
||||||
|
r.HandleFunc("/user/policy/set", PolicySet)
|
||||||
|
r.HandleFunc("/user/policy/del", PolicyDel)
|
||||||
|
|
||||||
r.HandleFunc("/group/list", GroupList)
|
r.HandleFunc("/group/list", GroupList)
|
||||||
r.HandleFunc("/group/names", GroupNames)
|
r.HandleFunc("/group/names", GroupNames)
|
||||||
@@ -64,7 +74,25 @@ func StartAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
base.Info("Listen admin", base.Cfg.AdminAddr)
|
base.Info("Listen admin", base.Cfg.AdminAddr)
|
||||||
err := http.ListenAndServe(base.Cfg.AdminAddr, r)
|
|
||||||
|
// 修复 CVE-2016-2183
|
||||||
|
cipherSuites := tls.CipherSuites()
|
||||||
|
selectedCipherSuites := make([]uint16, 0, len(cipherSuites))
|
||||||
|
for _, s := range cipherSuites {
|
||||||
|
selectedCipherSuites = append(selectedCipherSuites, s.ID)
|
||||||
|
}
|
||||||
|
// 设置tls信息
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
NextProtos: []string{"http/1.1"},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CipherSuites: selectedCipherSuites,
|
||||||
|
}
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: base.Cfg.AdminAddr,
|
||||||
|
Handler: r,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
err := srv.ListenAndServeTLS(base.Cfg.CertFile, base.Cfg.CertKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
base.Fatal(err)
|
base.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,6 @@ package base
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
APP_NAME = "AnyLink"
|
APP_NAME = "AnyLink"
|
||||||
// 修复前端bug
|
// 添加radius支持
|
||||||
APP_VER = "0.7.1"
|
APP_VER = "0.8.1"
|
||||||
)
|
)
|
||||||
|
@@ -32,6 +32,7 @@ var (
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
// LinkAddr string `json:"link_addr"`
|
// LinkAddr string `json:"link_addr"`
|
||||||
Conf string `json:"conf"`
|
Conf string `json:"conf"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
ServerAddr string `json:"server_addr"`
|
ServerAddr string `json:"server_addr"`
|
||||||
ServerDTLSAddr string `json:"server_dtls_addr"`
|
ServerDTLSAddr string `json:"server_dtls_addr"`
|
||||||
ServerDTLS bool `json:"server_dtls"`
|
ServerDTLS bool `json:"server_dtls"`
|
||||||
@@ -65,6 +66,7 @@ type ServerConfig struct {
|
|||||||
CstpDpd int `json:"cstp_dpd"` // Dead peer detection in seconds
|
CstpDpd int `json:"cstp_dpd"` // Dead peer detection in seconds
|
||||||
MobileKeepalive int `json:"mobile_keepalive"`
|
MobileKeepalive int `json:"mobile_keepalive"`
|
||||||
MobileDpd int `json:"mobile_dpd"`
|
MobileDpd int `json:"mobile_dpd"`
|
||||||
|
Mtu int `json:"mtu"`
|
||||||
|
|
||||||
SessionTimeout int `json:"session_timeout"` // in seconds
|
SessionTimeout int `json:"session_timeout"` // in seconds
|
||||||
// AuthTimeout int `json:"auth_timeout"` // in seconds
|
// AuthTimeout int `json:"auth_timeout"` // in seconds
|
||||||
|
@@ -21,6 +21,7 @@ type config struct {
|
|||||||
|
|
||||||
var configs = []config{
|
var configs = []config{
|
||||||
{Typ: cfgStr, Name: "conf", Usage: "config file", ValStr: "./conf/server.toml", Short: "c"},
|
{Typ: cfgStr, Name: "conf", Usage: "config file", ValStr: "./conf/server.toml", Short: "c"},
|
||||||
|
{Typ: cfgStr, Name: "profile", Usage: "profile.xml file", ValStr: "./conf/profile.xml"},
|
||||||
{Typ: cfgStr, Name: "server_addr", Usage: "服务监听地址", ValStr: ":443"},
|
{Typ: cfgStr, Name: "server_addr", Usage: "服务监听地址", ValStr: ":443"},
|
||||||
{Typ: cfgBool, Name: "server_dtls", Usage: "开启DTLS", ValBool: false},
|
{Typ: cfgBool, Name: "server_dtls", Usage: "开启DTLS", ValBool: false},
|
||||||
{Typ: cfgStr, Name: "server_dtls_addr", Usage: "DTLS监听地址", ValStr: ":4433"},
|
{Typ: cfgStr, Name: "server_dtls_addr", Usage: "DTLS监听地址", ValStr: ":4433"},
|
||||||
@@ -53,6 +54,7 @@ var configs = []config{
|
|||||||
{Typ: cfgInt, Name: "cstp_dpd", Usage: "死链接检测时间(秒)", ValInt: 30},
|
{Typ: cfgInt, Name: "cstp_dpd", Usage: "死链接检测时间(秒)", ValInt: 30},
|
||||||
{Typ: cfgInt, Name: "mobile_keepalive", Usage: "移动端keepalive接检测时间(秒)", ValInt: 50},
|
{Typ: cfgInt, Name: "mobile_keepalive", Usage: "移动端keepalive接检测时间(秒)", ValInt: 50},
|
||||||
{Typ: cfgInt, Name: "mobile_dpd", Usage: "移动端死链接检测时间(秒)", ValInt: 60},
|
{Typ: cfgInt, Name: "mobile_dpd", Usage: "移动端死链接检测时间(秒)", ValInt: 60},
|
||||||
|
{Typ: cfgInt, Name: "mtu", Usage: "最大传输单元MTU", ValInt: 1460},
|
||||||
{Typ: cfgInt, Name: "session_timeout", Usage: "session过期时间(秒)", ValInt: 3600},
|
{Typ: cfgInt, Name: "session_timeout", Usage: "session过期时间(秒)", ValInt: 3600},
|
||||||
// {Typ: cfgInt, Name: "auth_timeout", Usage: "auth_timeout", ValInt: 0},
|
// {Typ: cfgInt, Name: "auth_timeout", Usage: "auth_timeout", ValInt: 0},
|
||||||
{Typ: cfgInt, Name: "audit_interval", Usage: "审计去重间隔(秒),-1关闭", ValInt: -1},
|
{Typ: cfgInt, Name: "audit_interval", Usage: "审计去重间隔(秒),-1关闭", ValInt: -1},
|
||||||
|
@@ -6,10 +6,11 @@
|
|||||||
#数据文件
|
#数据文件
|
||||||
db_type = "sqlite3"
|
db_type = "sqlite3"
|
||||||
db_source = "./conf/anylink.db"
|
db_source = "./conf/anylink.db"
|
||||||
#证书文件
|
#证书文件 使用跟nginx一样的证书即可
|
||||||
cert_file = "./conf/vpn_cert.pem"
|
cert_file = "./conf/vpn_cert.crt"
|
||||||
cert_key = "./conf/vpn_cert.key"
|
cert_key = "./conf/vpn_cert.key"
|
||||||
files_path = "./conf/files"
|
files_path = "./conf/files"
|
||||||
|
profile = "./conf/profile.xml"
|
||||||
#日志目录,为空写入标准输出
|
#日志目录,为空写入标准输出
|
||||||
#log_path = "./log"
|
#log_path = "./log"
|
||||||
log_path = ""
|
log_path = ""
|
||||||
@@ -59,6 +60,10 @@ cstp_keepalive = 20
|
|||||||
cstp_dpd = 30
|
cstp_dpd = 30
|
||||||
mobile_keepalive = 40
|
mobile_keepalive = 40
|
||||||
mobile_dpd = 50
|
mobile_dpd = 50
|
||||||
|
|
||||||
|
#设置最大传输单元
|
||||||
|
mtu = 1460
|
||||||
|
|
||||||
#session过期时间,用于断线重连,0永不过期
|
#session过期时间,用于断线重连,0永不过期
|
||||||
session_timeout = 3600
|
session_timeout = 3600
|
||||||
auth_timeout = 0
|
auth_timeout = 0
|
||||||
|
@@ -7,9 +7,10 @@
|
|||||||
db_type = "sqlite3"
|
db_type = "sqlite3"
|
||||||
db_source = "./conf/anylink.db"
|
db_source = "./conf/anylink.db"
|
||||||
#证书文件
|
#证书文件
|
||||||
cert_file = "./conf/vpn_cert.pem"
|
cert_file = "./conf/vpn_cert.crt"
|
||||||
cert_key = "./conf/vpn_cert.key"
|
cert_key = "./conf/vpn_cert.key"
|
||||||
files_path = "./conf/files"
|
files_path = "./conf/files"
|
||||||
|
log_level = "debug"
|
||||||
|
|
||||||
#系统名称
|
#系统名称
|
||||||
issuer = "XX公司VPN"
|
issuer = "XX公司VPN"
|
||||||
|
@@ -25,7 +25,7 @@ func initDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{})
|
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
base.Fatal(err)
|
base.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,23 @@ func addInitData() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.Commit()
|
err = sess.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
g1 := Group{
|
||||||
|
Name: "ops",
|
||||||
|
AllowLan: true,
|
||||||
|
ClientDns: []ValData{{Val: "114.114.114.114"}},
|
||||||
|
RouteInclude: []ValData{{Val: All}},
|
||||||
|
}
|
||||||
|
err = SetGroup(&g1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckErrNotFound(err error) bool {
|
func CheckErrNotFound(err error) bool {
|
||||||
|
@@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bjdgyc/anylink/base"
|
"github.com/bjdgyc/anylink/base"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
Allow = "allow"
|
Allow = "allow"
|
||||||
Deny = "deny"
|
Deny = "deny"
|
||||||
|
All = "all"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupLinkAcl struct {
|
type GroupLinkAcl struct {
|
||||||
@@ -30,18 +33,21 @@ type ValData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// type Group struct {
|
// type Group struct {
|
||||||
// Id int `json:"id" xorm:"pk autoincr not null"`
|
// Id int `json:"id" xorm:"pk autoincr not null"`
|
||||||
// Name string `json:"name" xorm:"not null unique"`
|
// Name string `json:"name" xorm:"varchar(60) not null unique"`
|
||||||
// Note string `json:"note"`
|
// Note string `json:"note" xorm:"varchar(255)"`
|
||||||
// AllowLan bool `json:"allow_lan"`
|
// AllowLan bool `json:"allow_lan" xorm:"Bool"`
|
||||||
// ClientDns []ValData `json:"client_dns"`
|
// ClientDns []ValData `json:"client_dns" xorm:"Text"`
|
||||||
// RouteInclude []ValData `json:"route_include"`
|
// RouteInclude []ValData `json:"route_include" xorm:"Text"`
|
||||||
// RouteExclude []ValData `json:"route_exclude"`
|
// RouteExclude []ValData `json:"route_exclude" xorm:"Text"`
|
||||||
// LinkAcl []GroupLinkAcl `json:"link_acl"`
|
// DsExcludeDomains string `json:"ds_exclude_domains" xorm:"Text"`
|
||||||
// Bandwidth int `json:"bandwidth"` // 带宽限制
|
// DsIncludeDomains string `json:"ds_include_domains" xorm:"Text"`
|
||||||
// Status int8 `json:"status"` // 1正常
|
// LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"`
|
||||||
// CreatedAt time.Time `json:"created_at"`
|
// Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制
|
||||||
// UpdatedAt time.Time `json:"updated_at"`
|
// Auth map[string]interface{} `json:"auth" xorm:"not null default '{}' varchar(255)"` // 认证方式
|
||||||
|
// Status int8 `json:"status" xorm:"Int"` // 1正常
|
||||||
|
// CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
|
||||||
|
// UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func GetGroupNames() []string {
|
func GetGroupNames() []string {
|
||||||
@@ -65,25 +71,10 @@ func SetGroup(g *Group) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 判断数据
|
// 判断数据
|
||||||
clientDns := []ValData{}
|
|
||||||
for _, v := range g.ClientDns {
|
|
||||||
if v.Val != "" {
|
|
||||||
ip := net.ParseIP(v.Val)
|
|
||||||
if ip.String() != v.Val {
|
|
||||||
return errors.New("DNS IP 错误")
|
|
||||||
}
|
|
||||||
clientDns = append(clientDns, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(clientDns) == 0 {
|
|
||||||
return errors.New("必须设置一个DNS")
|
|
||||||
}
|
|
||||||
g.ClientDns = clientDns
|
|
||||||
|
|
||||||
routeInclude := []ValData{}
|
routeInclude := []ValData{}
|
||||||
for _, v := range g.RouteInclude {
|
for _, v := range g.RouteInclude {
|
||||||
if v.Val != "" {
|
if v.Val != "" {
|
||||||
if v.Val == "all" {
|
if v.Val == All {
|
||||||
routeInclude = append(routeInclude, v)
|
routeInclude = append(routeInclude, v)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -124,6 +115,61 @@ func SetGroup(g *Group) error {
|
|||||||
}
|
}
|
||||||
g.LinkAcl = linkAcl
|
g.LinkAcl = linkAcl
|
||||||
|
|
||||||
|
// DNS 判断
|
||||||
|
clientDns := []ValData{}
|
||||||
|
for _, v := range g.ClientDns {
|
||||||
|
if v.Val != "" {
|
||||||
|
ip := net.ParseIP(v.Val)
|
||||||
|
if ip.String() != v.Val {
|
||||||
|
return errors.New("DNS IP 错误")
|
||||||
|
}
|
||||||
|
clientDns = append(clientDns, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(routeInclude) == 0 || (len(routeInclude) == 1 && routeInclude[0].Val == "all") {
|
||||||
|
if len(clientDns) == 0 {
|
||||||
|
return errors.New("默认路由,必须设置一个DNS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.ClientDns = clientDns
|
||||||
|
// 域名拆分隧道,不能同时填写
|
||||||
|
g.DsIncludeDomains = strings.TrimSpace(g.DsIncludeDomains)
|
||||||
|
g.DsExcludeDomains = strings.TrimSpace(g.DsExcludeDomains)
|
||||||
|
if g.DsIncludeDomains != "" && g.DsExcludeDomains != "" {
|
||||||
|
return errors.New("包含/排除域名不能同时填写")
|
||||||
|
}
|
||||||
|
// 校验包含域名的格式
|
||||||
|
err = CheckDomainNames(g.DsIncludeDomains)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("包含域名有误:" + err.Error())
|
||||||
|
}
|
||||||
|
// 校验排除域名的格式
|
||||||
|
err = CheckDomainNames(g.DsExcludeDomains)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("排除域名有误:" + err.Error())
|
||||||
|
}
|
||||||
|
// 处理登入方式的逻辑
|
||||||
|
defAuth := map[string]interface{}{
|
||||||
|
"type": "local",
|
||||||
|
}
|
||||||
|
if len(g.Auth) == 0 {
|
||||||
|
g.Auth = defAuth
|
||||||
|
}
|
||||||
|
authType := g.Auth["type"].(string)
|
||||||
|
if authType == "local" {
|
||||||
|
g.Auth = defAuth
|
||||||
|
} else {
|
||||||
|
_, ok := authRegistry[authType]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("未知的认证方式: " + authType)
|
||||||
|
}
|
||||||
|
auth := makeInstance(authType).(IUserAuth)
|
||||||
|
err = auth.checkData(g.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g.UpdatedAt = time.Now()
|
g.UpdatedAt = time.Now()
|
||||||
if g.Id > 0 {
|
if g.Id > 0 {
|
||||||
err = Set(g)
|
err = Set(g)
|
||||||
@@ -145,3 +191,24 @@ func parseIpNet(s string) (string, *net.IPNet, error) {
|
|||||||
|
|
||||||
return ipMask, ipNet, nil
|
return ipMask, ipNet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckDomainNames(domains string) error {
|
||||||
|
if domains == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
str_slice := strings.Split(domains, ",")
|
||||||
|
for _, val := range str_slice {
|
||||||
|
if val == "" {
|
||||||
|
return errors.New(val + " 请以逗号分隔域名")
|
||||||
|
}
|
||||||
|
if !ValidateDomainName(val) {
|
||||||
|
return errors.New(val + " 域名有误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateDomainName(domain string) bool {
|
||||||
|
RegExp := regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+[A-Za-z]{2,18}$`)
|
||||||
|
return RegExp.MatchString(domain)
|
||||||
|
}
|
||||||
|
@@ -24,8 +24,25 @@ func TestGetGroupNames(t *testing.T) {
|
|||||||
err = SetGroup(&g3)
|
err = SetGroup(&g3)
|
||||||
ast.Nil(err)
|
ast.Nil(err)
|
||||||
|
|
||||||
|
authData := map[string]interface{}{
|
||||||
|
"type": "radius",
|
||||||
|
"radius": map[string]string{
|
||||||
|
"addr": "192.168.8.12:1044",
|
||||||
|
"secret": "43214132",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
g4 := Group{Name: "g4", ClientDns: []ValData{{Val: "114.114.114.114"}}, Auth: authData}
|
||||||
|
err = SetGroup(&g4)
|
||||||
|
ast.Nil(err)
|
||||||
|
g5 := Group{Name: "g5", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsIncludeDomains: "baidu.com,163.com"}
|
||||||
|
err = SetGroup(&g5)
|
||||||
|
ast.Nil(err)
|
||||||
|
g6 := Group{Name: "g6", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "com.cn,qq.com"}
|
||||||
|
err = SetGroup(&g6)
|
||||||
|
ast.Nil(err)
|
||||||
|
|
||||||
// 判断所有数据
|
// 判断所有数据
|
||||||
gAll := []string{"g1", "g2", "g3"}
|
gAll := []string{"g1", "g2", "g3", "g4", "g5", "g6"}
|
||||||
gs := GetGroupNames()
|
gs := GetGroupNames()
|
||||||
for _, v := range gs {
|
for _, v := range gs {
|
||||||
ast.Equal(true, utils.InArrStr(gAll, v))
|
ast.Equal(true, utils.InArrStr(gAll, v))
|
||||||
|
101
server/dbdata/policy.go
Normal file
101
server/dbdata/policy.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package dbdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPolicy(Username string) *Policy {
|
||||||
|
policyData := &Policy{}
|
||||||
|
err := One("Username", Username, policyData)
|
||||||
|
if err != nil {
|
||||||
|
return policyData
|
||||||
|
}
|
||||||
|
return policyData
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetPolicy(p *Policy) error {
|
||||||
|
var err error
|
||||||
|
if p.Username == "" {
|
||||||
|
return errors.New("用户名错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包含路由
|
||||||
|
routeInclude := []ValData{}
|
||||||
|
for _, v := range p.RouteInclude {
|
||||||
|
if v.Val != "" {
|
||||||
|
if v.Val == All {
|
||||||
|
routeInclude = append(routeInclude, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipMask, _, err := parseIpNet(v.Val)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("RouteInclude 错误" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
v.IpMask = ipMask
|
||||||
|
routeInclude = append(routeInclude, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.RouteInclude = routeInclude
|
||||||
|
// 排除路由
|
||||||
|
routeExclude := []ValData{}
|
||||||
|
for _, v := range p.RouteExclude {
|
||||||
|
if v.Val != "" {
|
||||||
|
ipMask, _, err := parseIpNet(v.Val)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("RouteExclude 错误" + err.Error())
|
||||||
|
}
|
||||||
|
v.IpMask = ipMask
|
||||||
|
routeExclude = append(routeExclude, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.RouteExclude = routeExclude
|
||||||
|
|
||||||
|
// DNS 判断
|
||||||
|
clientDns := []ValData{}
|
||||||
|
for _, v := range p.ClientDns {
|
||||||
|
if v.Val != "" {
|
||||||
|
ip := net.ParseIP(v.Val)
|
||||||
|
if ip.String() != v.Val {
|
||||||
|
return errors.New("DNS IP 错误")
|
||||||
|
}
|
||||||
|
clientDns = append(clientDns, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(routeInclude) == 0 || (len(routeInclude) == 1 && routeInclude[0].Val == "all") {
|
||||||
|
if len(clientDns) == 0 {
|
||||||
|
return errors.New("默认路由,必须设置一个DNS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.ClientDns = clientDns
|
||||||
|
|
||||||
|
// 域名拆分隧道,不能同时填写
|
||||||
|
p.DsIncludeDomains = strings.TrimSpace(p.DsIncludeDomains)
|
||||||
|
p.DsExcludeDomains = strings.TrimSpace(p.DsExcludeDomains)
|
||||||
|
if p.DsIncludeDomains != "" && p.DsExcludeDomains != "" {
|
||||||
|
return errors.New("包含/排除域名不能同时填写")
|
||||||
|
}
|
||||||
|
// 校验包含域名的格式
|
||||||
|
err = CheckDomainNames(p.DsIncludeDomains)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("包含域名有误:" + err.Error())
|
||||||
|
}
|
||||||
|
// 校验排除域名的格式
|
||||||
|
err = CheckDomainNames(p.DsExcludeDomains)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("排除域名有误:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.UpdatedAt = time.Now()
|
||||||
|
if p.Id > 0 {
|
||||||
|
err = Set(p)
|
||||||
|
} else {
|
||||||
|
err = Add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
45
server/dbdata/policy_test.go
Normal file
45
server/dbdata/policy_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package dbdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetPolicy(t *testing.T) {
|
||||||
|
ast := assert.New(t)
|
||||||
|
|
||||||
|
preIpData()
|
||||||
|
defer closeIpdata()
|
||||||
|
|
||||||
|
// 添加 Policy
|
||||||
|
p1 := Policy{Username: "a1", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "baidu.com,163.com"}
|
||||||
|
err := SetPolicy(&p1)
|
||||||
|
ast.Nil(err)
|
||||||
|
|
||||||
|
p2 := Policy{Username: "a2", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "com.cn,qq.com"}
|
||||||
|
err = SetPolicy(&p2)
|
||||||
|
ast.Nil(err)
|
||||||
|
|
||||||
|
route := []ValData{{Val: "192.168.1.1/24"}}
|
||||||
|
p3 := Policy{Username: "a3", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteInclude: route, DsExcludeDomains: "com.cn,qq.com"}
|
||||||
|
err = SetPolicy(&p3)
|
||||||
|
ast.Nil(err)
|
||||||
|
// 判断 IpMask
|
||||||
|
ast.Equal(p3.RouteInclude[0].IpMask, "192.168.1.1/255.255.255.0")
|
||||||
|
|
||||||
|
route2 := []ValData{{Val: "192.168.2.1/24"}}
|
||||||
|
p4 := Policy{Username: "a4", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteExclude: route2, DsIncludeDomains: "com.cn,qq.com"}
|
||||||
|
err = SetPolicy(&p4)
|
||||||
|
ast.Nil(err)
|
||||||
|
// 判断 IpMask
|
||||||
|
ast.Equal(p4.RouteExclude[0].IpMask, "192.168.2.1/255.255.255.0")
|
||||||
|
|
||||||
|
// 判断所有数据
|
||||||
|
var userPolicy *Policy
|
||||||
|
pAll := []string{"a1", "a2", "a3", "a4"}
|
||||||
|
for _, v := range pAll {
|
||||||
|
userPolicy = GetPolicy(v)
|
||||||
|
ast.NotEqual(userPolicy.Id, 0, "user policy id is zero")
|
||||||
|
}
|
||||||
|
}
|
@@ -6,18 +6,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
Id int `json:"id" xorm:"pk autoincr not null"`
|
Id int `json:"id" xorm:"pk autoincr not null"`
|
||||||
Name string `json:"name" xorm:"varchar(60) not null unique"`
|
Name string `json:"name" xorm:"varchar(60) not null unique"`
|
||||||
Note string `json:"note" xorm:"varchar(255)"`
|
Note string `json:"note" xorm:"varchar(255)"`
|
||||||
AllowLan bool `json:"allow_lan" xorm:"Bool"`
|
AllowLan bool `json:"allow_lan" xorm:"Bool"`
|
||||||
ClientDns []ValData `json:"client_dns" xorm:"Text"`
|
ClientDns []ValData `json:"client_dns" xorm:"Text"`
|
||||||
RouteInclude []ValData `json:"route_include" xorm:"Text"`
|
RouteInclude []ValData `json:"route_include" xorm:"Text"`
|
||||||
RouteExclude []ValData `json:"route_exclude" xorm:"Text"`
|
RouteExclude []ValData `json:"route_exclude" xorm:"Text"`
|
||||||
LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"`
|
DsExcludeDomains string `json:"ds_exclude_domains" xorm:"Text"`
|
||||||
Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制
|
DsIncludeDomains string `json:"ds_include_domains" xorm:"Text"`
|
||||||
Status int8 `json:"status" xorm:"Int"` // 1正常
|
LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"`
|
||||||
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
|
Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制
|
||||||
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
|
Auth map[string]interface{} `json:"auth" xorm:"not null default '{}' varchar(255)"` // 认证方式
|
||||||
|
Status int8 `json:"status" xorm:"Int"` // 1正常
|
||||||
|
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -65,3 +68,17 @@ type AccessAudit struct {
|
|||||||
DstPort uint16 `json:"dst_port" xorm:"not null"`
|
DstPort uint16 `json:"dst_port" xorm:"not null"`
|
||||||
CreatedAt time.Time `json:"created_at" xorm:"DateTime"`
|
CreatedAt time.Time `json:"created_at" xorm:"DateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
Id int `json:"id" xorm:"pk autoincr not null"`
|
||||||
|
Username string `json:"username" xorm:"varchar(60) not null unique"`
|
||||||
|
AllowLan bool `json:"allow_lan" xorm:"Bool"`
|
||||||
|
ClientDns []ValData `json:"client_dns" xorm:"Text"`
|
||||||
|
RouteInclude []ValData `json:"route_include" xorm:"Text"`
|
||||||
|
RouteExclude []ValData `json:"route_exclude" xorm:"Text"`
|
||||||
|
DsExcludeDomains string `json:"ds_exclude_domains" xorm:"Text"`
|
||||||
|
DsIncludeDomains string `json:"ds_include_domains" xorm:"Text"`
|
||||||
|
Status int8 `json:"status" xorm:"Int"` // 1正常 0 禁用
|
||||||
|
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
|
||||||
|
}
|
||||||
|
@@ -66,8 +66,34 @@ func SetUser(v *User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户登陆信息
|
// 验证用户登录信息
|
||||||
func CheckUser(name, pwd, group string) error {
|
func CheckUser(name, pwd, group string) error {
|
||||||
|
// 获取登入的group数据
|
||||||
|
groupData := &Group{}
|
||||||
|
err := One("Name", group, groupData)
|
||||||
|
if err != nil || groupData.Status != 1 {
|
||||||
|
return fmt.Errorf("%s - %s", name, "用户组错误")
|
||||||
|
}
|
||||||
|
// 初始化Auth
|
||||||
|
if len(groupData.Auth) == 0 {
|
||||||
|
groupData.Auth["type"] = "local"
|
||||||
|
}
|
||||||
|
authType := groupData.Auth["type"].(string)
|
||||||
|
// 本地认证方式
|
||||||
|
if authType == "local" {
|
||||||
|
return checkLocalUser(name, pwd, group)
|
||||||
|
}
|
||||||
|
// 其它认证方式, 支持自定义
|
||||||
|
_, ok := authRegistry[authType]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s %s", "未知的认证方式: ", authType)
|
||||||
|
}
|
||||||
|
auth := makeInstance(authType).(IUserAuth)
|
||||||
|
return auth.checkUser(name, pwd, groupData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证本地用户登录信息
|
||||||
|
func checkLocalUser(name, pwd, group string) error {
|
||||||
// TODO 严重问题
|
// TODO 严重问题
|
||||||
// return nil
|
// return nil
|
||||||
|
|
||||||
@@ -84,12 +110,6 @@ func CheckUser(name, pwd, group string) error {
|
|||||||
if !utils.InArrStr(v.Groups, group) {
|
if !utils.InArrStr(v.Groups, group) {
|
||||||
return fmt.Errorf("%s %s", name, "用户组错误")
|
return fmt.Errorf("%s %s", name, "用户组错误")
|
||||||
}
|
}
|
||||||
groupData := &Group{}
|
|
||||||
err = One("Name", group, groupData)
|
|
||||||
if err != nil || groupData.Status != 1 {
|
|
||||||
return fmt.Errorf("%s - %s", name, "用户组错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断otp信息
|
// 判断otp信息
|
||||||
pinCode := pwd
|
pinCode := pwd
|
||||||
if !v.DisableOtp {
|
if !v.DisableOtp {
|
||||||
|
@@ -40,4 +40,30 @@ func TestCheckUser(t *testing.T) {
|
|||||||
_ = SetUser(&u)
|
_ = SetUser(&u)
|
||||||
err = CheckUser("aaa", u.PinCode, group)
|
err = CheckUser("aaa", u.PinCode, group)
|
||||||
ast.Nil(err)
|
ast.Nil(err)
|
||||||
|
|
||||||
|
// 添加一个radius组
|
||||||
|
group2 := "group2"
|
||||||
|
authData := map[string]interface{}{
|
||||||
|
"type": "radius",
|
||||||
|
"radius": map[string]string{
|
||||||
|
"addr": "192.168.1.12:1044",
|
||||||
|
"secret": "43214132",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
g2 := Group{Name: group2, Status: 1, ClientDns: dns, RouteInclude: route, Auth: authData}
|
||||||
|
err = SetGroup(&g2)
|
||||||
|
ast.Nil(err)
|
||||||
|
err = CheckUser("aaa", "bbbbbbb", group2)
|
||||||
|
if ast.NotNil(err) {
|
||||||
|
ast.Equal("aaa Radius服务器连接异常, 请检测服务器和端口", err.Error())
|
||||||
|
|
||||||
|
}
|
||||||
|
// 添加用户策略
|
||||||
|
dns2 := []ValData{{Val: "8.8.8.8"}}
|
||||||
|
route2 := []ValData{{Val: "192.168.2.1/24"}}
|
||||||
|
p1 := Policy{Username: "aaa", Status: 1, ClientDns: dns2, RouteInclude: route2}
|
||||||
|
err = SetPolicy(&p1)
|
||||||
|
ast.Nil(err)
|
||||||
|
err = CheckUser("aaa", u.PinCode, group)
|
||||||
|
ast.Nil(err)
|
||||||
}
|
}
|
||||||
|
23
server/dbdata/userauth.go
Normal file
23
server/dbdata/userauth.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package dbdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authRegistry = make(map[string]reflect.Type)
|
||||||
|
|
||||||
|
type IUserAuth interface {
|
||||||
|
checkData(authData map[string]interface{}) error
|
||||||
|
checkUser(name, pwd string, g *Group) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeInstance(name string) interface{} {
|
||||||
|
v := reflect.New(authRegistry[name]).Elem()
|
||||||
|
return v.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateIpPort(addr string) bool {
|
||||||
|
RegExp := regexp.MustCompile(`^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\:([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$$`)
|
||||||
|
return RegExp.MatchString(addr)
|
||||||
|
}
|
72
server/dbdata/userauth_radius.go
Normal file
72
server/dbdata/userauth_radius.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package dbdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"layeh.com/radius"
|
||||||
|
"layeh.com/radius/rfc2865"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthRadius struct {
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
authRegistry["radius"] = reflect.TypeOf(AuthRadius{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth AuthRadius) checkData(authData map[string]interface{}) error {
|
||||||
|
authType := authData["type"].(string)
|
||||||
|
bodyBytes, err := json.Marshal(authData[authType])
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Radius的密钥/服务器地址填写有误")
|
||||||
|
}
|
||||||
|
json.Unmarshal(bodyBytes, &auth)
|
||||||
|
if !ValidateIpPort(auth.Addr) {
|
||||||
|
return errors.New("Radius的服务器地址填写有误")
|
||||||
|
}
|
||||||
|
// freeradius官网最大8000字符, 这里限制200
|
||||||
|
if len(auth.Secret) < 8 || len(auth.Secret) > 200 {
|
||||||
|
return errors.New("Radius的密钥长度需在8~200个字符之间")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth AuthRadius) checkUser(name, pwd string, g *Group) error {
|
||||||
|
pl := len(pwd)
|
||||||
|
if name == "" || pl < 1 {
|
||||||
|
return fmt.Errorf("%s %s", name, "密码错误")
|
||||||
|
}
|
||||||
|
authType := g.Auth["type"].(string)
|
||||||
|
if _, ok := g.Auth[authType]; !ok {
|
||||||
|
return fmt.Errorf("%s %s", name, "Radius的radius值不存在")
|
||||||
|
}
|
||||||
|
bodyBytes, err := json.Marshal(g.Auth[authType])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s %s", name, "Radius Marshal出现错误")
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(bodyBytes, &auth)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s %s", name, "Radius Unmarshal出现错误")
|
||||||
|
}
|
||||||
|
// radius认证时,设置超时3秒
|
||||||
|
packet := radius.New(radius.CodeAccessRequest, []byte(auth.Secret))
|
||||||
|
rfc2865.UserName_SetString(packet, name)
|
||||||
|
rfc2865.UserPassword_SetString(packet, pwd)
|
||||||
|
ctx, done := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer done()
|
||||||
|
response, err := radius.Exchange(ctx, packet, auth.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s %s", name, "Radius服务器连接异常, 请检测服务器和端口")
|
||||||
|
}
|
||||||
|
if response.Code != radius.CodeAccessAccept {
|
||||||
|
return fmt.Errorf("%s %s", name, "Radius:用户名或密码错误")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -25,6 +25,7 @@ require (
|
|||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||||
|
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
|
||||||
xorm.io/xorm v1.2.2
|
xorm.io/xorm v1.2.2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -565,6 +565,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
@@ -964,6 +965,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab h1:05KeMI4s7jEdIfHb7QCjUr5X2BRA0gjLZLZEmmjGNc4=
|
||||||
|
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab/go.mod h1:pFWM9De99EY9TPVyHIyA56QmoRViVck/x41WFkUlc9A=
|
||||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.33.6 h1:r63dgSzVzRxUpAJFPQWHy1QeZeY1ydNENUDaBx1GqYc=
|
modernc.org/cc/v3 v3.33.6 h1:r63dgSzVzRxUpAJFPQWHy1QeZeY1ydNENUDaBx1GqYc=
|
||||||
|
@@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/bjdgyc/anylink/sessdata"
|
"github.com/bjdgyc/anylink/sessdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var profileHash = ""
|
||||||
|
|
||||||
func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
// 判断anyconnect客户端
|
// 判断anyconnect客户端
|
||||||
userAgent := strings.ToLower(r.UserAgent())
|
userAgent := strings.ToLower(r.UserAgent())
|
||||||
@@ -89,7 +91,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
other := &dbdata.SettingOther{}
|
other := &dbdata.SettingOther{}
|
||||||
_ = dbdata.SettingGet(other)
|
_ = dbdata.SettingGet(other)
|
||||||
rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
|
rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
|
||||||
Banner: other.Banner}
|
Banner: other.Banner, ProfileHash: profileHash}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
tplRequest(tpl_complete, w, rd)
|
tplRequest(tpl_complete, w, rd)
|
||||||
base.Debug("login", cr.Auth.Username)
|
base.Debug("login", cr.Auth.Username)
|
||||||
@@ -125,6 +127,7 @@ type RequestData struct {
|
|||||||
SessionId string
|
SessionId string
|
||||||
SessionToken string
|
SessionToken string
|
||||||
Banner string
|
Banner string
|
||||||
|
ProfileHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
var auth_request = `<?xml version="1.0" encoding="UTF-8"?>
|
var auth_request = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -176,8 +179,8 @@ var auth_complete = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
<vpn-profile-manifest>
|
<vpn-profile-manifest>
|
||||||
<vpn rev="1.0">
|
<vpn rev="1.0">
|
||||||
<file type="profile" service-type="user">
|
<file type="profile" service-type="user">
|
||||||
<uri>/files/profile.xml</uri>
|
<uri>/profile.xml</uri>
|
||||||
<hash type="sha1">A8B0B07FBA93D06E8501E40AB807AEE2464E73B7</hash>
|
<hash type="sha1">{{.ProfileHash}}</hash>
|
||||||
</file>
|
</file>
|
||||||
</vpn>
|
</vpn>
|
||||||
</vpn-profile-manifest>
|
</vpn-profile-manifest>
|
||||||
@@ -218,3 +221,19 @@ var auth_profile = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
</ServerList>
|
</ServerList>
|
||||||
</AnyConnectProfile>
|
</AnyConnectProfile>
|
||||||
`
|
`
|
||||||
|
var ds_domains_xml = `
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<config-auth client="vpn" type="complete" aggregate-auth-version="2">
|
||||||
|
<config client="vpn" type="private">
|
||||||
|
<opaque is-for="vpn-client">
|
||||||
|
<custom-attr>
|
||||||
|
{{if .DsExcludeDomains}}
|
||||||
|
<dynamic-split-exclude-domains><![CDATA[{{.DsExcludeDomains}},]]></dynamic-split-exclude-domains>
|
||||||
|
{{else if .DsIncludeDomains}}
|
||||||
|
<dynamic-split-include-domains><![CDATA[{{.DsIncludeDomains}}]]></dynamic-split-include-domains>
|
||||||
|
{{end}}
|
||||||
|
</custom-attr>
|
||||||
|
</opaque>
|
||||||
|
</config>
|
||||||
|
</config-auth>
|
||||||
|
`
|
||||||
|
@@ -8,8 +8,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/bjdgyc/anylink/base"
|
"github.com/bjdgyc/anylink/base"
|
||||||
|
"github.com/bjdgyc/anylink/dbdata"
|
||||||
"github.com/bjdgyc/anylink/sessdata"
|
"github.com/bjdgyc/anylink/sessdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,11 +25,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HttpSetHeader(w http.ResponseWriter, key string, value string) {
|
func HttpSetHeader(w http.ResponseWriter, key string, value string) {
|
||||||
w.Header()[key] = []string{value}
|
w.Header()[key] = []string{value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HttpAddHeader(w http.ResponseWriter, key string, value string) {
|
func HttpAddHeader(w http.ResponseWriter, key string, value string) {
|
||||||
w.Header()[key] = append(w.Header()[key], value)
|
w.Header()[key] = append(w.Header()[key], value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -98,6 +100,9 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
|||||||
//HttpSetHeader(w, "X-CSTP-Default-Domain", cSess.LocalIp)
|
//HttpSetHeader(w, "X-CSTP-Default-Domain", cSess.LocalIp)
|
||||||
HttpSetHeader(w, "X-CSTP-Base-MTU", cstpBaseMtu)
|
HttpSetHeader(w, "X-CSTP-Base-MTU", cstpBaseMtu)
|
||||||
|
|
||||||
|
// 设置用户策略
|
||||||
|
SetUserPolicy(sess.Username, cSess.Group)
|
||||||
|
|
||||||
// 允许本地LAN访问vpn网络,必须放在路由的第一个
|
// 允许本地LAN访问vpn网络,必须放在路由的第一个
|
||||||
if cSess.Group.AllowLan {
|
if cSess.Group.AllowLan {
|
||||||
HttpSetHeader(w, "X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255")
|
HttpSetHeader(w, "X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255")
|
||||||
@@ -108,7 +113,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// 允许的路由
|
// 允许的路由
|
||||||
for _, v := range cSess.Group.RouteInclude {
|
for _, v := range cSess.Group.RouteInclude {
|
||||||
if v.Val == "all" {
|
if v.Val == dbdata.All {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
HttpAddHeader(w, "X-CSTP-Split-Include", v.IpMask)
|
HttpAddHeader(w, "X-CSTP-Split-Include", v.IpMask)
|
||||||
@@ -117,7 +122,6 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, v := range cSess.Group.RouteExclude {
|
for _, v := range cSess.Group.RouteExclude {
|
||||||
HttpAddHeader(w, "X-CSTP-Split-Exclude", v.IpMask)
|
HttpAddHeader(w, "X-CSTP-Split-Exclude", v.IpMask)
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpSetHeader(w, "X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.IpLease)) // ip地址租期
|
HttpSetHeader(w, "X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.IpLease)) // ip地址租期
|
||||||
HttpSetHeader(w, "X-CSTP-Session-Timeout", "none")
|
HttpSetHeader(w, "X-CSTP-Session-Timeout", "none")
|
||||||
HttpSetHeader(w, "X-CSTP-Session-Timeout-Alert-Interval", "60")
|
HttpSetHeader(w, "X-CSTP-Session-Timeout-Alert-Interval", "60")
|
||||||
@@ -152,7 +156,11 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
|||||||
HttpSetHeader(w, "X-CSTP-Disable-Always-On-VPN", "false")
|
HttpSetHeader(w, "X-CSTP-Disable-Always-On-VPN", "false")
|
||||||
HttpSetHeader(w, "X-CSTP-Client-Bypass-Protocol", "false")
|
HttpSetHeader(w, "X-CSTP-Client-Bypass-Protocol", "false")
|
||||||
HttpSetHeader(w, "X-CSTP-TCP-Keepalive", "false")
|
HttpSetHeader(w, "X-CSTP-TCP-Keepalive", "false")
|
||||||
// HttpSetHeader(w, "X-CSTP-Post-Auth-XML", ``)
|
// 设置域名拆分隧道(移动端不支持)
|
||||||
|
if mobile != "mobile" {
|
||||||
|
SetPostAuthXml(cSess.Group, w)
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
hClone := w.Header().Clone()
|
hClone := w.Header().Clone()
|
||||||
@@ -186,3 +194,35 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
go LinkCstp(conn, bufRW, cSess)
|
go LinkCstp(conn, bufRW, cSess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置域名拆分隧道
|
||||||
|
func SetPostAuthXml(g *dbdata.Group, w http.ResponseWriter) error {
|
||||||
|
if g.DsExcludeDomains == "" && g.DsIncludeDomains == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tmpl, err := template.New("post_auth_xml").Parse(ds_domains_xml)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var result bytes.Buffer
|
||||||
|
err = tmpl.Execute(&result, g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
HttpSetHeader(w, "X-CSTP-Post-Auth-XML", result.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户策略, 覆盖Group的属性值
|
||||||
|
func SetUserPolicy(username string, g *dbdata.Group) {
|
||||||
|
userPolicy := dbdata.GetPolicy(username)
|
||||||
|
if userPolicy.Id != 0 && userPolicy.Status == 1 {
|
||||||
|
base.Debug(username + " use UserPolicy")
|
||||||
|
g.AllowLan = userPolicy.AllowLan
|
||||||
|
g.ClientDns = userPolicy.ClientDns
|
||||||
|
g.RouteInclude = userPolicy.RouteInclude
|
||||||
|
g.RouteExclude = userPolicy.RouteExclude
|
||||||
|
g.DsExcludeDomains = userPolicy.DsExcludeDomains
|
||||||
|
g.DsIncludeDomains = userPolicy.DsIncludeDomains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bjdgyc/anylink/base"
|
"github.com/bjdgyc/anylink/base"
|
||||||
@@ -18,33 +19,34 @@ func startTls() {
|
|||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
|
|
||||||
addr = base.Cfg.ServerAddr
|
addr = base.Cfg.ServerAddr
|
||||||
certFile = base.Cfg.CertFile
|
ln net.Listener
|
||||||
keyFile = base.Cfg.CertKey
|
|
||||||
certs = make([]tls.Certificate, 1)
|
|
||||||
ln net.Listener
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 判断证书文件
|
// 判断证书文件
|
||||||
//_, err = os.Stat(certFile)
|
// _, err = os.Stat(certFile)
|
||||||
//if errors.Is(err, os.ErrNotExist) {
|
// if errors.Is(err, os.ErrNotExist) {
|
||||||
// // 自动生成证书
|
// // 自动生成证书
|
||||||
// certs[0], err = selfsign.GenerateSelfSignedWithDNS("vpn.anylink")
|
// certs[0], err = selfsign.GenerateSelfSignedWithDNS("vpn.anylink")
|
||||||
//} else {
|
// } else {
|
||||||
// // 使用自定义证书
|
// // 使用自定义证书
|
||||||
// certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
// certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
//}
|
// }
|
||||||
|
|
||||||
certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
// 修复 CVE-2016-2183
|
||||||
if err != nil {
|
// https://segmentfault.com/a/1190000038486901
|
||||||
panic(err)
|
// nmap -sV --script ssl-enum-ciphers -p 443 www.example.com
|
||||||
|
cipherSuites := tls.CipherSuites()
|
||||||
|
selectedCipherSuites := make([]uint16, 0, len(cipherSuites))
|
||||||
|
for _, s := range cipherSuites {
|
||||||
|
selectedCipherSuites = append(selectedCipherSuites, s.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置tls信息
|
// 设置tls信息
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
NextProtos: []string{"http/1.1"},
|
NextProtos: []string{"http/1.1"},
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
Certificates: certs,
|
CipherSuites: selectedCipherSuites,
|
||||||
// InsecureSkipVerify: true,
|
// InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
@@ -65,7 +67,7 @@ func startTls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
base.Info("listen server", addr)
|
base.Info("listen server", addr)
|
||||||
err = srv.ServeTLS(ln, "", "")
|
err = srv.ServeTLS(ln, base.Cfg.CertFile, base.Cfg.CertKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
base.Fatal(err)
|
base.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -77,9 +79,10 @@ func initRoute() http.Handler {
|
|||||||
r.HandleFunc("/", LinkAuth).Methods(http.MethodPost)
|
r.HandleFunc("/", LinkAuth).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect)
|
r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect)
|
||||||
r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet)
|
r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet)
|
||||||
// r.HandleFunc("/profile.xml", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/profile.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// w.Write([]byte(auth_profile))
|
b, _ := os.ReadFile(base.Cfg.Profile)
|
||||||
// }).Methods(http.MethodGet)
|
w.Write(b)
|
||||||
|
}).Methods(http.MethodGet)
|
||||||
r.PathPrefix("/files/").Handler(
|
r.PathPrefix("/files/").Handler(
|
||||||
http.StripPrefix("/files/",
|
http.StripPrefix("/files/",
|
||||||
http.FileServer(http.Dir(base.Cfg.FilesPath)),
|
http.FileServer(http.Dir(base.Cfg.FilesPath)),
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/bjdgyc/anylink/admin"
|
"github.com/bjdgyc/anylink/admin"
|
||||||
"github.com/bjdgyc/anylink/base"
|
"github.com/bjdgyc/anylink/base"
|
||||||
"github.com/bjdgyc/anylink/dbdata"
|
"github.com/bjdgyc/anylink/dbdata"
|
||||||
@@ -22,6 +26,14 @@ func Start() {
|
|||||||
base.Fatal("LinkMode is err")
|
base.Fatal("LinkMode is err")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算profile.xml的hash
|
||||||
|
b, err := os.ReadFile(base.Cfg.Profile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ha := sha1.Sum(b)
|
||||||
|
profileHash = hex.EncodeToString(ha[:])
|
||||||
|
|
||||||
go admin.StartAdmin()
|
go admin.StartAdmin()
|
||||||
go startTls()
|
go startTls()
|
||||||
go startDtls()
|
go startDtls()
|
||||||
|
@@ -14,7 +14,7 @@ type Payload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
var header = []byte{'S', 'T', 'F', 0x01, 0, 0, 0x00, 0}
|
var header = []byte{'S', 'T', 'F', 0x01, 0, 0, 0x07, 0}
|
||||||
https://tools.ietf.org/html/draft-mavrogiannopoulos-openconnect-02#section-2.2
|
https://tools.ietf.org/html/draft-mavrogiannopoulos-openconnect-02#section-2.2
|
||||||
|
|
||||||
+---------------------+---------------------------------------------+
|
+---------------------+---------------------------------------------+
|
||||||
|
@@ -294,9 +294,12 @@ func (cs *ConnSession) ratePeriod() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MaxMtu = 1460
|
var MaxMtu = 1460
|
||||||
|
|
||||||
func (cs *ConnSession) SetMtu(mtu string) {
|
func (cs *ConnSession) SetMtu(mtu string) {
|
||||||
|
if base.Cfg.Mtu > 0 {
|
||||||
|
MaxMtu = base.Cfg.Mtu
|
||||||
|
}
|
||||||
cs.Mtu = MaxMtu
|
cs.Mtu = MaxMtu
|
||||||
|
|
||||||
mi, err := strconv.Atoi(mtu)
|
mi, err := strconv.Atoi(mtu)
|
||||||
|
27581
web/package-lock.json
generated
27581
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-menu-item index="/admin/user/list">用户列表</el-menu-item>
|
<el-menu-item index="/admin/user/list">用户列表</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/user/policy">用户策略</el-menu-item>
|
||||||
<el-menu-item index="/admin/user/online">在线用户</el-menu-item>
|
<el-menu-item index="/admin/user/online">在线用户</el-menu-item>
|
||||||
<el-menu-item index="/admin/user/ip_map">IP映射</el-menu-item>
|
<el-menu-item index="/admin/user/ip_map">IP映射</el-menu-item>
|
||||||
</el-submenu>
|
</el-submenu>
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card>
|
<el-card>
|
||||||
|
|
||||||
<el-form :inline="true">
|
<el-form :inline="true">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -65,7 +64,13 @@
|
|||||||
label="路由包含"
|
label="路由包含"
|
||||||
width="200">
|
width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-row v-for="(item,inx) in scope.row.route_include" :key="inx">{{ item.val }}</el-row>
|
<el-row v-for="(item,inx) in scope.row.route_include.slice(0, readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
<div v-if="scope.row.route_include.length > readMinRows">
|
||||||
|
<div v-if="readMore[`ri_${ scope.row.id }`]">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_include.slice(readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" type="text" @click="toggleMore(`ri_${ scope.row.id }`)">{{ readMore[`ri_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }}</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -74,7 +79,13 @@
|
|||||||
label="路由排除"
|
label="路由排除"
|
||||||
width="200">
|
width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-row v-for="(item,inx) in scope.row.route_exclude" :key="inx">{{ item.val }}</el-row>
|
<el-row v-for="(item,inx) in scope.row.route_exclude.slice(0, readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
<div v-if="scope.row.route_exclude.length > readMinRows">
|
||||||
|
<div v-if="readMore[`re_${ scope.row.id }`]">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_exclude.slice(readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" type="text" @click="toggleMore(`re_${ scope.row.id }`)">{{ readMore[`re_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }}</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -83,9 +94,17 @@
|
|||||||
label="LINK-ACL"
|
label="LINK-ACL"
|
||||||
min-width="200">
|
min-width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-row v-for="(item,inx) in scope.row.link_acl" :key="inx">
|
<el-row v-for="(item,inx) in scope.row.link_acl.slice(0, readMinRows)" :key="inx">
|
||||||
{{ item.action }} => {{ item.val }} : {{ item.port }}
|
{{ item.action }} => {{ item.val }} : {{ item.port }}
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<div v-if="scope.row.link_acl.length > readMinRows">
|
||||||
|
<div v-if="readMore[`la_${ scope.row.id }`]">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.link_acl.slice(readMinRows)" :key="inx">
|
||||||
|
{{ item.action }} => {{ item.val }} : {{ item.port }}
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" type="text" @click="toggleMore(`la_${ scope.row.id }`)">{{ readMore[`la_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }}</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -152,143 +171,175 @@
|
|||||||
center>
|
center>
|
||||||
|
|
||||||
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
|
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
|
||||||
<el-form-item label="用户组ID" prop="id">
|
<el-tabs v-model="activeTab">
|
||||||
<el-input v-model="ruleForm.id" disabled></el-input>
|
<el-tab-pane label="通用" name="general">
|
||||||
</el-form-item>
|
<el-form-item label="用户组ID" prop="id">
|
||||||
|
<el-input v-model="ruleForm.id" disabled></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="组名" prop="name">
|
<el-form-item label="组名" prop="name">
|
||||||
<el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input>
|
<el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="note">
|
<el-form-item label="备注" prop="note">
|
||||||
<el-input v-model="ruleForm.note"></el-input>
|
<el-input v-model="ruleForm.note"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="带宽限制" prop="bandwidth">
|
<el-form-item label="带宽限制" prop="bandwidth">
|
||||||
<el-input v-model.number="ruleForm.bandwidth">
|
<el-input v-model.number="ruleForm.bandwidth">
|
||||||
<template slot="append">BYTE/S</template>
|
<template slot="append">BYTE/S</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="本地网络" prop="allow_lan">
|
<el-form-item label="本地网络" prop="allow_lan">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="ruleForm.allow_lan">
|
v-model="ruleForm.allow_lan">
|
||||||
</el-switch>
|
</el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="客户端DNS" prop="client_dns">
|
<el-form-item label="客户端DNS" prop="client_dns">
|
||||||
<el-row class="msg-info">
|
<el-row class="msg-info">
|
||||||
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
|
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
|
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row v-for="(item,index) in ruleForm.client_dns"
|
<el-row v-for="(item,index) in ruleForm.client_dns"
|
||||||
:key="index" style="margin-bottom: 5px" :gutter="10">
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
<el-col :span="10">
|
<el-col :span="10">
|
||||||
<el-input v-model="item.val"></el-input>
|
<el-input v-model="item.val"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2">
|
<el-col :span="2">
|
||||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
@click.prevent="removeDomain(ruleForm.client_dns,index)"></el-button>
|
@click.prevent="removeDomain(ruleForm.client_dns,index)"></el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="ruleForm.status">
|
||||||
|
<el-radio :label="1" border>启用</el-radio>
|
||||||
|
<el-radio :label="0" border>停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-form-item label="包含路由" prop="route_include">
|
<el-tab-pane label="认证方式" name="authtype">
|
||||||
<el-row class="msg-info">
|
<el-form-item label="认证" prop="authtype">
|
||||||
<el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col>
|
<el-radio-group v-model="ruleForm.auth.type">
|
||||||
<el-col :span="4">
|
<el-radio label="local" border>本地</el-radio>
|
||||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
<el-radio label="radius" border>Radius</el-radio>
|
||||||
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
|
</el-radio-group>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
</el-row>
|
<el-form-item label="Radius密钥" v-if="ruleForm.auth.type == 'radius'">
|
||||||
<el-row v-for="(item,index) in ruleForm.route_include"
|
<el-col :span="10">
|
||||||
:key="index" style="margin-bottom: 5px" :gutter="10">
|
<el-input v-model="ruleForm.auth.radius.secret"></el-input>
|
||||||
<el-col :span="10">
|
</el-col>
|
||||||
<el-input v-model="item.val"></el-input>
|
</el-form-item>
|
||||||
</el-col>
|
<el-form-item label="Radius服务器" v-if="ruleForm.auth.type == 'radius'">
|
||||||
<el-col :span="12">
|
<el-col :span="10">
|
||||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
<el-input v-model="ruleForm.auth.radius.addr" placeholder="输入IP和端口 192.168.2.1:1812"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2">
|
</el-form-item>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
</el-tab-pane>
|
||||||
@click.prevent="removeDomain(ruleForm.route_include,index)"></el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="排除路由" prop="route_exclude">
|
<el-tab-pane label="路由设置" name="route">
|
||||||
<el-row class="msg-info">
|
<el-form-item label="包含路由" prop="route_include">
|
||||||
<el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col>
|
<el-row class="msg-info">
|
||||||
<el-col :span="4">
|
<el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col>
|
||||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
<el-col :span="4">
|
||||||
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
|
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
</el-col>
|
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
|
||||||
</el-row>
|
</el-col>
|
||||||
<el-row v-for="(item,index) in ruleForm.route_exclude"
|
</el-row>
|
||||||
:key="index" style="margin-bottom: 5px" :gutter="10">
|
<el-row v-for="(item,index) in ruleForm.route_include"
|
||||||
<el-col :span="10">
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
<el-input v-model="item.val"></el-input>
|
<el-col :span="10">
|
||||||
</el-col>
|
<el-input v-model="item.val"></el-input>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
<el-col :span="12">
|
||||||
</el-col>
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
<el-col :span="2">
|
</el-col>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
<el-col :span="2">
|
||||||
@click.prevent="removeDomain(ruleForm.route_exclude,index)"></el-button>
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
</el-col>
|
@click.prevent="removeDomain(ruleForm.route_include,index)"></el-button>
|
||||||
</el-row>
|
</el-col>
|
||||||
</el-form-item>
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="权限控制" prop="link_acl">
|
<el-form-item label="排除路由" prop="route_exclude">
|
||||||
<el-row class="msg-info">
|
<el-row class="msg-info">
|
||||||
<el-col :span="20">输入CIDR格式如: 192.168.3.0/24 端口0表示所有端口</el-col>
|
<el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col>
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
@click.prevent="addDomain(ruleForm.link_acl)"></el-button>
|
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row v-for="(item,index) in ruleForm.route_exclude"
|
||||||
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-input v-model="item.val"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2">
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
|
@click.prevent="removeDomain(ruleForm.route_exclude,index)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="权限控制" name="link_acl">
|
||||||
|
<el-form-item label="权限控制" prop="link_acl">
|
||||||
|
<el-row class="msg-info">
|
||||||
|
<el-col :span="20">输入CIDR格式如: 192.168.3.0/24 端口0表示所有端口</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
|
@click.prevent="addDomain(ruleForm.link_acl)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<el-row v-for="(item,index) in ruleForm.link_acl"
|
<el-row v-for="(item,index) in ruleForm.link_acl"
|
||||||
:key="index" style="margin-bottom: 5px" :gutter="5">
|
:key="index" style="margin-bottom: 5px" :gutter="5">
|
||||||
<el-col :span="11">
|
<el-col :span="11">
|
||||||
<el-input placeholder="请输入CIDR地址" v-model="item.val">
|
<el-input placeholder="请输入CIDR地址" v-model="item.val">
|
||||||
<el-select v-model="item.action" slot="prepend">
|
<el-select v-model="item.action" slot="prepend">
|
||||||
<el-option label="允许" value="allow"></el-option>
|
<el-option label="允许" value="allow"></el-option>
|
||||||
<el-option label="禁止" value="deny"></el-option>
|
<el-option label="禁止" value="deny"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="3">
|
<el-col :span="3">
|
||||||
<el-input v-model.number="item.port" placeholder="端口"></el-input>
|
<el-input v-model.number="item.port" placeholder="端口"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="2">
|
<el-col :span="2">
|
||||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
@click.prevent="removeDomain(ruleForm.link_acl,index)"></el-button>
|
@click.prevent="removeDomain(ruleForm.link_acl,index)"></el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-form-item label="状态" prop="status">
|
<el-tab-pane label="域名拆分隧道" name="ds_domains">
|
||||||
<el-radio-group v-model="ruleForm.status">
|
<el-form-item label="包含域名" prop="ds_include_domains">
|
||||||
<el-radio :label="1" border>启用</el-radio>
|
<el-input type="textarea" :rows="5" v-model="ruleForm.ds_include_domains" placeholder="输入域名用,号分隔,默认匹配所有子域名, 如baidu.com,163.com"></el-input>
|
||||||
<el-radio :label="0" border>停用</el-radio>
|
</el-form-item>
|
||||||
</el-radio-group>
|
<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>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
|
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
|
||||||
<el-button @click="disVisible">取消</el-button>
|
<el-button @click="disVisible">取消</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-tabs>
|
||||||
|
</el-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -306,14 +357,17 @@ export default {
|
|||||||
this.$emit('update:route_name', ['用户组信息', '用户组列表'])
|
this.$emit('update:route_name', ['用户组信息', '用户组列表'])
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getData(1)
|
this.getData(1);
|
||||||
|
this.setAuthData();
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
page: 1,
|
page: 1,
|
||||||
tableData: [],
|
tableData: [],
|
||||||
count: 10,
|
count: 10,
|
||||||
|
activeTab : "general",
|
||||||
|
readMore: {},
|
||||||
|
readMinRows : 5,
|
||||||
ruleForm: {
|
ruleForm: {
|
||||||
bandwidth: 0,
|
bandwidth: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
@@ -322,21 +376,17 @@ export default {
|
|||||||
route_include: [{val: 'all', note: '默认全局代理'}],
|
route_include: [{val: 'all', note: '默认全局代理'}],
|
||||||
route_exclude: [],
|
route_exclude: [],
|
||||||
link_acl: [],
|
link_acl: [],
|
||||||
|
auth : {"type":'local'}
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
name: [
|
name: [
|
||||||
{required: true, message: '请输入用户名', trigger: 'blur'},
|
{required: true, message: '请输入组名', trigger: 'blur'},
|
||||||
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
|
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
|
||||||
],
|
],
|
||||||
bandwidth: [
|
bandwidth: [
|
||||||
{required: true, message: '请输入用户姓名', trigger: 'blur'},
|
{required: true, message: '请输入带宽限制', trigger: 'blur'},
|
||||||
{type: 'number', message: '年龄必须为数字值'}
|
{type: 'number', message: '带宽限制必须为数字值'}
|
||||||
],
|
],
|
||||||
email: [
|
|
||||||
{required: true, message: '请输入用户邮箱', trigger: 'blur'},
|
|
||||||
{type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']}
|
|
||||||
],
|
|
||||||
|
|
||||||
status: [
|
status: [
|
||||||
{required: true}
|
{required: true}
|
||||||
],
|
],
|
||||||
@@ -344,6 +394,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setAuthData(row) {
|
||||||
|
var defAuthData = {"type":'local',
|
||||||
|
"radius":{"addr":"", "secret":""},
|
||||||
|
}
|
||||||
|
if (this.ruleForm.auth.type == "local" || !row) {
|
||||||
|
this.ruleForm.auth = defAuthData;
|
||||||
|
}
|
||||||
|
},
|
||||||
handleDel(row) {
|
handleDel(row) {
|
||||||
axios.post('/group/del?id=' + row.id).then(resp => {
|
axios.post('/group/del?id=' + row.id).then(resp => {
|
||||||
const rdata = resp.data;
|
const rdata = resp.data;
|
||||||
@@ -362,17 +420,19 @@ export default {
|
|||||||
handleEdit(row) {
|
handleEdit(row) {
|
||||||
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
|
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
|
||||||
console.log(row)
|
console.log(row)
|
||||||
|
this.activeTab = "general"
|
||||||
this.user_edit_dialog = true
|
this.user_edit_dialog = true
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
this.setAuthData(row)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.get('/group/detail', {
|
axios.get('/group/detail', {
|
||||||
params: {
|
params: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
}
|
}
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
this.ruleForm = resp.data.data
|
this.ruleForm = resp.data.data;
|
||||||
|
this.setAuthData(resp.data.data);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.$message.error('哦,请求出错');
|
this.$message.error('哦,请求出错');
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -417,12 +477,12 @@ export default {
|
|||||||
console.log('error submit!!');
|
console.log('error submit!!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post('/group/set', this.ruleForm).then(resp => {
|
axios.post('/group/set', this.ruleForm).then(resp => {
|
||||||
const rdata = resp.data;
|
const rdata = resp.data;
|
||||||
if (rdata.code === 0) {
|
if (rdata.code === 0) {
|
||||||
this.$message.success(rdata.msg);
|
this.$message.success(rdata.msg);
|
||||||
this.getData(1);
|
this.getData(1);
|
||||||
|
this.user_edit_dialog = false
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(rdata.msg);
|
this.$message.error(rdata.msg);
|
||||||
}
|
}
|
||||||
@@ -435,9 +495,15 @@ export default {
|
|||||||
},
|
},
|
||||||
resetForm(formName) {
|
resetForm(formName) {
|
||||||
this.$refs[formName].resetFields();
|
this.$refs[formName].resetFields();
|
||||||
}
|
},
|
||||||
|
toggleMore(id) {
|
||||||
|
if (this.readMore[id]) {
|
||||||
|
this.$set(this.readMore, id, false);
|
||||||
|
} else {
|
||||||
|
this.$set(this.readMore, id, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -388,6 +388,7 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success(data.msg);
|
this.$message.success(data.msg);
|
||||||
this.getData(1);
|
this.getData(1);
|
||||||
|
this.user_edit_dialog = false
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg);
|
this.$message.error(data.msg);
|
||||||
}
|
}
|
||||||
|
421
web/src/pages/user/Policy.vue
Normal file
421
web/src/pages/user/Policy.vue
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card>
|
||||||
|
<el-form :inline="true">
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
@click="handleEdit('')">添加
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
ref="multipleTable"
|
||||||
|
:data="tableData"
|
||||||
|
border>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
sortable="true"
|
||||||
|
prop="id"
|
||||||
|
label="ID"
|
||||||
|
width="60">
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="username"
|
||||||
|
label="用户名">
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="allow_lan"
|
||||||
|
label="本地网络">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch
|
||||||
|
v-model="scope.row.allow_lan"
|
||||||
|
disabled>
|
||||||
|
</el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="client_dns"
|
||||||
|
label="客户端DNS"
|
||||||
|
width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.client_dns" :key="inx">{{ item.val }}</el-row>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="route_include"
|
||||||
|
label="路由包含"
|
||||||
|
width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_include.slice(0, readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
<div v-if="scope.row.route_include.length > readMinRows">
|
||||||
|
<div v-if="readMore[`ri_${ scope.row.id }`]">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_include.slice(readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" type="text" @click="toggleMore(`ri_${ scope.row.id }`)">{{ readMore[`ri_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }}</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="route_exclude"
|
||||||
|
label="路由排除"
|
||||||
|
width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_exclude.slice(0, readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
<div v-if="scope.row.route_exclude.length > readMinRows">
|
||||||
|
<div v-if="readMore[`re_${ scope.row.id }`]">
|
||||||
|
<el-row v-for="(item,inx) in scope.row.route_exclude.slice(readMinRows)" :key="inx">{{ item.val }}</el-row>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" type="text" @click="toggleMore(`re_${ scope.row.id }`)">{{ readMore[`re_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }}</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="status"
|
||||||
|
label="状态"
|
||||||
|
width="70">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
|
||||||
|
<el-tag v-else type="danger">停用</el-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="updated_at"
|
||||||
|
label="更新时间"
|
||||||
|
:formatter="tableDateFormat">
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
@click="handleEdit(scope.row)">编辑
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-popconfirm
|
||||||
|
style="margin-left: 10px"
|
||||||
|
@confirm="handleDel(scope.row)"
|
||||||
|
title="确定要删除用户策略项吗?">
|
||||||
|
<el-button
|
||||||
|
slot="reference"
|
||||||
|
size="mini"
|
||||||
|
type="danger">删除
|
||||||
|
</el-button>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:pager-count="11"
|
||||||
|
@current-change="pageChange"
|
||||||
|
:current-page="page"
|
||||||
|
:total="count">
|
||||||
|
</el-pagination>
|
||||||
|
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!--新增、修改弹出框-->
|
||||||
|
<el-dialog
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
title="用户策略"
|
||||||
|
:visible.sync="user_edit_dialog"
|
||||||
|
width="750px"
|
||||||
|
top="50px"
|
||||||
|
center>
|
||||||
|
|
||||||
|
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="通用" name="general">
|
||||||
|
<el-form-item label="ID" prop="id">
|
||||||
|
<el-input v-model="ruleForm.id" disabled></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="ruleForm.username" :disabled="ruleForm.id > 0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="本地网络" prop="allow_lan">
|
||||||
|
<el-switch
|
||||||
|
v-model="ruleForm.allow_lan">
|
||||||
|
</el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="客户端DNS" prop="client_dns">
|
||||||
|
<el-row class="msg-info">
|
||||||
|
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
|
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-for="(item,index) in ruleForm.client_dns"
|
||||||
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-input v-model="item.val"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2">
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
|
@click.prevent="removeDomain(ruleForm.client_dns,index)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="ruleForm.status">
|
||||||
|
<el-radio :label="1" border>启用</el-radio>
|
||||||
|
<el-radio :label="0" border>停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<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-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
|
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-for="(item,index) in ruleForm.route_include"
|
||||||
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-input v-model="item.val"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2">
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
|
@click.prevent="removeDomain(ruleForm.route_include,index)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</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-button size="mini" type="success" icon="el-icon-plus" circle
|
||||||
|
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-for="(item,index) in ruleForm.route_exclude"
|
||||||
|
:key="index" style="margin-bottom: 5px" :gutter="10">
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-input v-model="item.val"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="2">
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||||
|
@click.prevent="removeDomain(ruleForm.route_exclude,index)"></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="动态拆分隧道" name="ds_domains">
|
||||||
|
<el-form-item label="包含域名" prop="ds_include_domains">
|
||||||
|
<el-input type="textarea" :rows="5" v-model="ruleForm.ds_include_domains"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排除域名" prop="ds_exclude_domains">
|
||||||
|
<el-input type="textarea" :rows="5" v-model="ruleForm.ds_exclude_domains"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
|
||||||
|
<el-button @click="disVisible">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Policy",
|
||||||
|
components: {},
|
||||||
|
mixins: [],
|
||||||
|
created() {
|
||||||
|
this.$emit('update:route_path', this.$route.path)
|
||||||
|
this.$emit('update:route_name', ['用户信息', '用户策略'])
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getData(1)
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
page: 1,
|
||||||
|
tableData: [],
|
||||||
|
count: 10,
|
||||||
|
activeTab : "general",
|
||||||
|
readMore: {},
|
||||||
|
readMinRows : 5,
|
||||||
|
ruleForm: {
|
||||||
|
bandwidth: 0,
|
||||||
|
status: 1,
|
||||||
|
allow_lan: true,
|
||||||
|
client_dns: [{val: '114.114.114.114'}],
|
||||||
|
route_include: [{val: 'all', note: '默认全局代理'}],
|
||||||
|
route_exclude: [],
|
||||||
|
re_upper_limit : 0,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{required: true, message: '请输入用户名', trigger: 'blur'},
|
||||||
|
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
bandwidth: [
|
||||||
|
{required: true, message: '请输入带宽限制', trigger: 'blur'},
|
||||||
|
{type: 'number', message: '带宽必须为数字值'}
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{required: true}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleDel(row) {
|
||||||
|
axios.post('/user/policy/del?id=' + row.id).then(resp => {
|
||||||
|
const rdata = resp.data;
|
||||||
|
if (rdata.code === 0) {
|
||||||
|
this.$message.success(rdata.msg);
|
||||||
|
this.getData(1);
|
||||||
|
} else {
|
||||||
|
this.$message.error(rdata.msg);
|
||||||
|
}
|
||||||
|
console.log(rdata);
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('哦,请求出错');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleEdit(row) {
|
||||||
|
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
|
||||||
|
console.log(row)
|
||||||
|
this.activeTab = "general"
|
||||||
|
this.user_edit_dialog = true
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.get('/user/policy/detail', {
|
||||||
|
params: {
|
||||||
|
id: row.id,
|
||||||
|
}
|
||||||
|
}).then(resp => {
|
||||||
|
this.ruleForm = resp.data.data
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('哦,请求出错');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pageChange(p) {
|
||||||
|
this.getData(p)
|
||||||
|
},
|
||||||
|
getData(page) {
|
||||||
|
this.page = page
|
||||||
|
axios.get('/user/policy/list', {
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
}
|
||||||
|
}).then(resp => {
|
||||||
|
const rdata = resp.data.data;
|
||||||
|
console.log(rdata);
|
||||||
|
this.tableData = rdata.datas;
|
||||||
|
this.count = rdata.count
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('哦,请求出错');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeDomain(arr, index) {
|
||||||
|
console.log(index)
|
||||||
|
if (index >= 0 && index < arr.length) {
|
||||||
|
arr.splice(index, 1)
|
||||||
|
}
|
||||||
|
// let index = arr.indexOf(item);
|
||||||
|
// if (index !== -1 && arr.length > 1) {
|
||||||
|
// arr.splice(index, 1)
|
||||||
|
// }
|
||||||
|
// arr.pop()
|
||||||
|
},
|
||||||
|
addDomain(arr) {
|
||||||
|
arr.push({val: "", action: "allow", port: 0});
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
console.log('error submit!!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/user/policy/set', this.ruleForm).then(resp => {
|
||||||
|
const rdata = resp.data;
|
||||||
|
if (rdata.code === 0) {
|
||||||
|
this.$message.success(rdata.msg);
|
||||||
|
this.getData(1);
|
||||||
|
this.user_edit_dialog = false
|
||||||
|
} else {
|
||||||
|
this.$message.error(rdata.msg);
|
||||||
|
}
|
||||||
|
console.log(rdata);
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('哦,请求出错');
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
},
|
||||||
|
toggleMore(id) {
|
||||||
|
if (this.readMore[id]) {
|
||||||
|
this.$set(this.readMore, id, false);
|
||||||
|
} else {
|
||||||
|
this.$set(this.readMore, id, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.msg-info {
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
color: #909399;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -5,9 +5,9 @@ function gDateFormat(p) {
|
|||||||
var year = da.getFullYear();
|
var year = da.getFullYear();
|
||||||
var month = da.getMonth() + 1;
|
var month = da.getMonth() + 1;
|
||||||
var dt = da.getDate();
|
var dt = da.getDate();
|
||||||
var h = da.getHours();
|
var h = ('0'+da.getHours()).slice(-2);
|
||||||
var m = da.getMinutes();
|
var m = ('0'+da.getMinutes()).slice(-2)
|
||||||
var s = da.getSeconds();
|
var s = ('0'+da.getSeconds()).slice(-2);
|
||||||
|
|
||||||
return year + '-' + month + '-' + dt + ' ' + h + ':' + m + ':' + s;
|
return year + '-' + month + '-' + dt + ' ' + h + ':' + m + ':' + s;
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ const routes = [
|
|||||||
{path: 'set/audit', component: () => import('@/pages/set/Audit')},
|
{path: 'set/audit', component: () => import('@/pages/set/Audit')},
|
||||||
|
|
||||||
{path: 'user/list', component: () => import('@/pages/user/List')},
|
{path: 'user/list', component: () => import('@/pages/user/List')},
|
||||||
|
{path: 'user/policy', component: () => import('@/pages/user/Policy')},
|
||||||
{path: 'user/online', component: () => import('@/pages/user/Online')},
|
{path: 'user/online', component: () => import('@/pages/user/Online')},
|
||||||
{path: 'user/ip_map', component: () => import('@/pages/user/IpMap')},
|
{path: 'user/ip_map', component: () => import('@/pages/user/IpMap')},
|
||||||
|
|
||||||
|
8661
web/yarn.lock
Normal file
8661
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user