diff --git a/Dockerfile b/Dockerfile index d1b58eb..cec779e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ COPY --from=builder_node /web/ui /anylink/server/ui #TODO 本地打包时使用镜像 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories RUN apk add --no-cache git gcc musl-dev -RUN cd /anylink/server;go 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 diff --git a/README.md b/README.md index f269afa..2622f03 100644 --- a/README.md +++ b/README.md @@ -30,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 > 没有编程基础的同学建议直接下载 release 包,从下面的地址下载 anylink-deploy.tar.gz @@ -78,12 +88,15 @@ sudo ./anylink - [x] 支持 [proxy protocol v1](http://www.haproxy.org/download/2.2/doc/proxy-protocol.txt) 协议 - [x] 用户组支持 - [x] 多用户支持 +- [x] 用户策略支持 - [x] TOTP 令牌支持 - [x] TOTP 令牌开关 - [x] 流量速率限制 - [x] 后台管理界面 - [x] 访问权限管理 - [x] IP 访问审计功能 +- [x] 域名动态拆分隧道(域名路由功能) +- [x] radius认证支持 - [ ] 基于 ipvtap 设备的桥接访问模式 ## Config @@ -268,15 +281,6 @@ sh bridge-init.sh docker build -t anylink . ``` -## Donate - -> 如果您觉得 anylink 对你有帮助,欢迎给我们打赏,也是帮助 anylink 更好的发展。 -> -> [查看打赏列表](doc/README.md) - -<p> - <img src="doc/screenshot/wxpay2.png" width="400" /> -</p> ## 常见问题 diff --git a/build.sh b/build.sh index 2b57fd5..76acaf5 100644 --- a/build.sh +++ b/build.sh @@ -31,6 +31,7 @@ rm -rf ui cp -rf $cpath/web/ui . #国内可替换源加快速度 export GOPROXY=https://goproxy.io +go mod tidy go build -v -o anylink -ldflags "-X main.CommitId=$(git rev-parse HEAD)" RETVAL $? diff --git a/doc/README.md b/doc/README.md index 64d5e58..6366342 100644 --- a/doc/README.md +++ b/doc/README.md @@ -10,11 +10,17 @@ > 感谢以下同学的打赏,AnyLink 有你更美好! -| 昵称 | 主页 | -| -------- | ---------------------------- | -| 代码oo8 | | -| 甘磊 | https://github.com/ganlei333 | -| Oo@ | https://github.com/chooop | -| 虚极静笃 | | -| Ficapy | | +| 昵称 | 主页 | +| -- | ---------------------------- | +| 代码oo8 | | +| 甘磊 | https://github.com/ganlei333 | +| Oo@ | https://github.com/chooop | +| 虚极静笃 | | +| 请喝可乐 | | +| 加油加油 | | +| 李建 | | +| lanbin | | +| 乐在东途 | | + + diff --git a/server/admin/api_group.go b/server/admin/api_group.go index 17fbdb7..dd440e1 100644 --- a/server/admin/api_group.go +++ b/server/admin/api_group.go @@ -62,7 +62,9 @@ func GroupDetail(w http.ResponseWriter, r *http.Request) { RespError(w, RespInternalErr, err) return } - + if len(data.Auth) == 0 { + data.Auth["type"] = "local" + } RespSucess(w, data) } diff --git a/server/admin/api_policy.go b/server/admin/api_policy.go new file mode 100644 index 0000000..a4934c9 --- /dev/null +++ b/server/admin/api_policy.go @@ -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) +} diff --git a/server/admin/server.go b/server/admin/server.go index 98f3b20..75b6ac9 100644 --- a/server/admin/server.go +++ b/server/admin/server.go @@ -52,6 +52,10 @@ func StartAdmin() { r.HandleFunc("/user/ip_map/detail", UserIpMapDetail) r.HandleFunc("/user/ip_map/set", UserIpMapSet) 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/names", GroupNames) diff --git a/server/base/cfg.go b/server/base/cfg.go index 49a9c92..5690f08 100644 --- a/server/base/cfg.go +++ b/server/base/cfg.go @@ -66,6 +66,7 @@ type ServerConfig struct { CstpDpd int `json:"cstp_dpd"` // Dead peer detection in seconds MobileKeepalive int `json:"mobile_keepalive"` MobileDpd int `json:"mobile_dpd"` + Mtu int `json:"mtu"` SessionTimeout int `json:"session_timeout"` // in seconds // AuthTimeout int `json:"auth_timeout"` // in seconds diff --git a/server/base/config.go b/server/base/config.go index b7edc7b..bdb6dc1 100644 --- a/server/base/config.go +++ b/server/base/config.go @@ -54,6 +54,7 @@ var configs = []config{ {Typ: cfgInt, Name: "cstp_dpd", Usage: "死链接检测时间(秒)", ValInt: 30}, {Typ: cfgInt, Name: "mobile_keepalive", Usage: "移动端keepalive接检测时间(秒)", ValInt: 50}, {Typ: cfgInt, Name: "mobile_dpd", Usage: "移动端死链接检测时间(秒)", ValInt: 60}, + {Typ: cfgInt, Name: "mtu", Usage: "最大传输单元MTU", ValInt: 1460}, {Typ: cfgInt, Name: "session_timeout", Usage: "session过期时间(秒)", ValInt: 3600}, // {Typ: cfgInt, Name: "auth_timeout", Usage: "auth_timeout", ValInt: 0}, {Typ: cfgInt, Name: "audit_interval", Usage: "审计去重间隔(秒),-1关闭", ValInt: -1}, diff --git a/server/conf/server-sample.toml b/server/conf/server-sample.toml index 402e03f..13a8835 100644 --- a/server/conf/server-sample.toml +++ b/server/conf/server-sample.toml @@ -60,6 +60,10 @@ cstp_keepalive = 20 cstp_dpd = 30 mobile_keepalive = 40 mobile_dpd = 50 + +#设置最大传输单元 +mtu = 1460 + #session过期时间,用于断线重连,0永不过期 session_timeout = 3600 auth_timeout = 0 diff --git a/server/dbdata/db.go b/server/dbdata/db.go index b6f9263..994651b 100644 --- a/server/dbdata/db.go +++ b/server/dbdata/db.go @@ -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 { base.Fatal(err) } diff --git a/server/dbdata/group.go b/server/dbdata/group.go index 252f3f8..112805d 100644 --- a/server/dbdata/group.go +++ b/server/dbdata/group.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "net" + "regexp" + "strings" "time" "github.com/bjdgyc/anylink/base" @@ -31,18 +33,21 @@ type ValData struct { } // type Group struct { -// Id int `json:"id" xorm:"pk autoincr not null"` -// Name string `json:"name" xorm:"not null unique"` -// Note string `json:"note"` -// AllowLan bool `json:"allow_lan"` -// ClientDns []ValData `json:"client_dns"` -// RouteInclude []ValData `json:"route_include"` -// RouteExclude []ValData `json:"route_exclude"` -// LinkAcl []GroupLinkAcl `json:"link_acl"` -// Bandwidth int `json:"bandwidth"` // 带宽限制 -// Status int8 `json:"status"` // 1正常 -// CreatedAt time.Time `json:"created_at"` -// UpdatedAt time.Time `json:"updated_at"` +// Id int `json:"id" xorm:"pk autoincr not null"` +// Name string `json:"name" xorm:"varchar(60) not null unique"` +// Note string `json:"note" xorm:"varchar(255)"` +// 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"` +// LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"` +// Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制 +// 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 { @@ -127,6 +132,43 @@ func SetGroup(g *Group) error { } } 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() if g.Id > 0 { @@ -149,3 +191,24 @@ func parseIpNet(s string) (string, *net.IPNet, error) { 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) +} diff --git a/server/dbdata/group_test.go b/server/dbdata/group_test.go index ee1dee1..7d27c7f 100644 --- a/server/dbdata/group_test.go +++ b/server/dbdata/group_test.go @@ -24,8 +24,25 @@ func TestGetGroupNames(t *testing.T) { err = SetGroup(&g3) 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() for _, v := range gs { ast.Equal(true, utils.InArrStr(gAll, v)) diff --git a/server/dbdata/policy.go b/server/dbdata/policy.go new file mode 100644 index 0000000..9777804 --- /dev/null +++ b/server/dbdata/policy.go @@ -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 +} diff --git a/server/dbdata/policy_test.go b/server/dbdata/policy_test.go new file mode 100644 index 0000000..e2dd409 --- /dev/null +++ b/server/dbdata/policy_test.go @@ -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") + } +} diff --git a/server/dbdata/tables.go b/server/dbdata/tables.go index 48df52e..56b2dbe 100644 --- a/server/dbdata/tables.go +++ b/server/dbdata/tables.go @@ -6,18 +6,21 @@ import ( ) type Group struct { - Id int `json:"id" xorm:"pk autoincr not null"` - Name string `json:"name" xorm:"varchar(60) not null unique"` - Note string `json:"note" xorm:"varchar(255)"` - 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"` - LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"` - Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制 - 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"` + Id int `json:"id" xorm:"pk autoincr not null"` + Name string `json:"name" xorm:"varchar(60) not null unique"` + Note string `json:"note" xorm:"varchar(255)"` + 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"` + LinkAcl []GroupLinkAcl `json:"link_acl" xorm:"Text"` + Bandwidth int `json:"bandwidth" xorm:"Int"` // 带宽限制 + 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 { @@ -65,3 +68,17 @@ type AccessAudit struct { DstPort uint16 `json:"dst_port" xorm:"not null"` 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"` +} diff --git a/server/dbdata/user.go b/server/dbdata/user.go index 5fbb2d7..7834013 100644 --- a/server/dbdata/user.go +++ b/server/dbdata/user.go @@ -66,8 +66,34 @@ func SetUser(v *User) error { return err } -// 验证用户登陆信息 +// 验证用户登录信息 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 严重问题 // return nil @@ -84,12 +110,6 @@ func CheckUser(name, pwd, group string) error { if !utils.InArrStr(v.Groups, group) { 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信息 pinCode := pwd if !v.DisableOtp { diff --git a/server/dbdata/user_test.go b/server/dbdata/user_test.go index e076672..8e46dcc 100644 --- a/server/dbdata/user_test.go +++ b/server/dbdata/user_test.go @@ -40,4 +40,30 @@ func TestCheckUser(t *testing.T) { _ = SetUser(&u) err = CheckUser("aaa", u.PinCode, group) 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) } diff --git a/server/dbdata/userauth.go b/server/dbdata/userauth.go new file mode 100644 index 0000000..fbc3eb5 --- /dev/null +++ b/server/dbdata/userauth.go @@ -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) +} diff --git a/server/dbdata/userauth_radius.go b/server/dbdata/userauth_radius.go new file mode 100644 index 0000000..4d15eb1 --- /dev/null +++ b/server/dbdata/userauth_radius.go @@ -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 +} diff --git a/server/go.mod b/server/go.mod index f46c1b3..4f3d552 100644 --- a/server/go.mod +++ b/server/go.mod @@ -25,6 +25,7 @@ require ( golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac + layeh.com/radius v0.0.0-20210819152912-ad72663a72ab xorm.io/xorm v1.2.2 ) diff --git a/server/go.sum b/server/go.sum index 06fafa4..089a38c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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-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-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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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-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= +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/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.33.6 h1:r63dgSzVzRxUpAJFPQWHy1QeZeY1ydNENUDaBx1GqYc= diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index 6c98138..d1740af 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -221,3 +221,19 @@ var auth_profile = `<?xml version="1.0" encoding="UTF-8"?> </ServerList> </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> +` diff --git a/server/handler/link_tunnel.go b/server/handler/link_tunnel.go index a43cb5f..42bf87f 100644 --- a/server/handler/link_tunnel.go +++ b/server/handler/link_tunnel.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strings" + "text/template" "github.com/bjdgyc/anylink/base" "github.com/bjdgyc/anylink/dbdata" @@ -99,6 +100,9 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { //HttpSetHeader(w, "X-CSTP-Default-Domain", cSess.LocalIp) HttpSetHeader(w, "X-CSTP-Base-MTU", cstpBaseMtu) + // 设置用户策略 + SetUserPolicy(sess.Username, cSess.Group) + // 允许本地LAN访问vpn网络,必须放在路由的第一个 if cSess.Group.AllowLan { HttpSetHeader(w, "X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255") @@ -118,7 +122,6 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { for _, v := range cSess.Group.RouteExclude { HttpAddHeader(w, "X-CSTP-Split-Exclude", v.IpMask) } - HttpSetHeader(w, "X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.IpLease)) // ip地址租期 HttpSetHeader(w, "X-CSTP-Session-Timeout", "none") HttpSetHeader(w, "X-CSTP-Session-Timeout-Alert-Interval", "60") @@ -153,7 +156,11 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { HttpSetHeader(w, "X-CSTP-Disable-Always-On-VPN", "false") HttpSetHeader(w, "X-CSTP-Client-Bypass-Protocol", "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) hClone := w.Header().Clone() @@ -187,3 +194,35 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { 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 + } +} diff --git a/server/sessdata/session.go b/server/sessdata/session.go index 97d6e3e..f165247 100644 --- a/server/sessdata/session.go +++ b/server/sessdata/session.go @@ -294,9 +294,12 @@ func (cs *ConnSession) ratePeriod() { } } -const MaxMtu = 1460 +var MaxMtu = 1460 func (cs *ConnSession) SetMtu(mtu string) { + if base.Cfg.Mtu > 0 { + MaxMtu = base.Cfg.Mtu + } cs.Mtu = MaxMtu mi, err := strconv.Atoi(mtu) diff --git a/web/src/layout/LayoutAside.vue b/web/src/layout/LayoutAside.vue index fb2973c..fbb82dd 100644 --- a/web/src/layout/LayoutAside.vue +++ b/web/src/layout/LayoutAside.vue @@ -42,6 +42,7 @@ </template> <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/ip_map">IP映射</el-menu-item> </el-submenu> diff --git a/web/src/pages/group/List.vue b/web/src/pages/group/List.vue index 2fbb4b3..7a1b08b 100644 --- a/web/src/pages/group/List.vue +++ b/web/src/pages/group/List.vue @@ -1,7 +1,6 @@ <template> <div> <el-card> - <el-form :inline="true"> <el-form-item> <el-button @@ -65,7 +64,13 @@ label="路由包含" width="200"> <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> </el-table-column> @@ -74,7 +79,13 @@ label="路由排除" width="200"> <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> </el-table-column> @@ -83,9 +94,17 @@ label="LINK-ACL" min-width="200"> <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 }} </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> </el-table-column> @@ -152,143 +171,175 @@ center> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm"> - <el-form-item label="用户组ID" prop="id"> - <el-input v-model="ruleForm.id" disabled></el-input> - </el-form-item> + <el-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="name"> - <el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input> - </el-form-item> + <el-form-item label="组名" prop="name"> + <el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input> + </el-form-item> - <el-form-item label="备注" prop="note"> - <el-input v-model="ruleForm.note"></el-input> - </el-form-item> + <el-form-item label="备注" prop="note"> + <el-input v-model="ruleForm.note"></el-input> + </el-form-item> - <el-form-item label="带宽限制" prop="bandwidth"> - <el-input v-model.number="ruleForm.bandwidth"> - <template slot="append">BYTE/S</template> - </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="带宽限制" prop="bandwidth"> + <el-input v-model.number="ruleForm.bandwidth"> + <template slot="append">BYTE/S</template> + </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="客户端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-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-tab-pane label="认证方式" name="authtype"> + <el-form-item label="认证" prop="authtype"> + <el-radio-group v-model="ruleForm.auth.type"> + <el-radio label="local" border>本地</el-radio> + <el-radio label="radius" border>Radius</el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="Radius密钥" v-if="ruleForm.auth.type == 'radius'"> + <el-col :span="10"> + <el-input v-model="ruleForm.auth.radius.secret"></el-input> + </el-col> + </el-form-item> + <el-form-item label="Radius服务器" v-if="ruleForm.auth.type == 'radius'"> + <el-col :span="10"> + <el-input v-model="ruleForm.auth.radius.addr" placeholder="输入IP和端口 192.168.2.1:1812"></el-input> + </el-col> + </el-form-item> + </el-tab-pane> - <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 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="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-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="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" - :key="index" style="margin-bottom: 5px" :gutter="5"> - <el-col :span="11"> - <el-input placeholder="请输入CIDR地址" v-model="item.val"> - <el-select v-model="item.action" slot="prepend"> - <el-option label="允许" value="allow"></el-option> - <el-option label="禁止" value="deny"></el-option> - </el-select> - </el-input> - </el-col> - <el-col :span="3"> - <el-input v-model.number="item.port" placeholder="端口"></el-input> - </el-col> - <el-col :span="8"> - <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.link_acl,index)"></el-button> - </el-col> - </el-row> - </el-form-item> + <el-row v-for="(item,index) in ruleForm.link_acl" + :key="index" style="margin-bottom: 5px" :gutter="5"> + <el-col :span="11"> + <el-input placeholder="请输入CIDR地址" v-model="item.val"> + <el-select v-model="item.action" slot="prepend"> + <el-option label="允许" value="allow"></el-option> + <el-option label="禁止" value="deny"></el-option> + </el-select> + </el-input> + </el-col> + <el-col :span="3"> + <el-input v-model.number="item.port" placeholder="端口"></el-input> + </el-col> + <el-col :span="8"> + <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.link_acl,index)"></el-button> + </el-col> + </el-row> + </el-form-item> + </el-tab-pane> - <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-form-item> - <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button> - <el-button @click="disVisible">取消</el-button> - </el-form-item> - </el-form> + <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" placeholder="输入域名用,号分隔,默认匹配所有子域名, 如baidu.com,163.com"></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" placeholder="输入域名用,号分隔,默认匹配所有子域名, 如baidu.com,163.com"></el-input> + </el-form-item> + </el-tab-pane> + <el-form-item> + <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button> + <el-button @click="disVisible">取消</el-button> + </el-form-item> + </el-tabs> + </el-form> </el-dialog> </div> @@ -306,14 +357,17 @@ export default { this.$emit('update:route_name', ['用户组信息', '用户组列表']) }, mounted() { - this.getData(1) + this.getData(1); + this.setAuthData(); }, data() { return { page: 1, tableData: [], count: 10, - + activeTab : "general", + readMore: {}, + readMinRows : 5, ruleForm: { bandwidth: 0, status: 1, @@ -322,21 +376,17 @@ export default { route_include: [{val: 'all', note: '默认全局代理'}], route_exclude: [], link_acl: [], + auth : {"type":'local'} }, rules: { name: [ - {required: true, message: '请输入用户名', trigger: 'blur'}, + {required: true, message: '请输入组名', trigger: 'blur'}, {max: 30, message: '长度小于 30 个字符', trigger: 'blur'} ], bandwidth: [ - {required: true, message: '请输入用户姓名', trigger: 'blur'}, - {type: 'number', message: '年龄必须为数字值'} + {required: true, message: '请输入带宽限制', trigger: 'blur'}, + {type: 'number', message: '带宽限制必须为数字值'} ], - email: [ - {required: true, message: '请输入用户邮箱', trigger: 'blur'}, - {type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']} - ], - status: [ {required: true} ], @@ -344,6 +394,14 @@ export default { } }, methods: { + setAuthData(row) { + var defAuthData = {"type":'local', + "radius":{"addr":"", "secret":""}, + } + if (this.ruleForm.auth.type == "local" || !row) { + this.ruleForm.auth = defAuthData; + } + }, handleDel(row) { axios.post('/group/del?id=' + row.id).then(resp => { const rdata = resp.data; @@ -362,17 +420,19 @@ export default { handleEdit(row) { !this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields(); console.log(row) + this.activeTab = "general" this.user_edit_dialog = true if (!row) { + this.setAuthData(row) return; } - axios.get('/group/detail', { params: { id: row.id, } }).then(resp => { - this.ruleForm = resp.data.data + this.ruleForm = resp.data.data; + this.setAuthData(resp.data.data); }).catch(error => { this.$message.error('哦,请求出错'); console.log(error); @@ -417,7 +477,6 @@ export default { console.log('error submit!!'); return false; } - axios.post('/group/set', this.ruleForm).then(resp => { const rdata = resp.data; if (rdata.code === 0) { @@ -436,9 +495,15 @@ export default { }, 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> diff --git a/web/src/pages/user/Policy.vue b/web/src/pages/user/Policy.vue new file mode 100644 index 0000000..b2a852e --- /dev/null +++ b/web/src/pages/user/Policy.vue @@ -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> diff --git a/web/src/plugins/mixin.js b/web/src/plugins/mixin.js index 2ade4fa..274e7fd 100644 --- a/web/src/plugins/mixin.js +++ b/web/src/plugins/mixin.js @@ -5,9 +5,9 @@ function gDateFormat(p) { var year = da.getFullYear(); var month = da.getMonth() + 1; var dt = da.getDate(); - var h = da.getHours(); - var m = da.getMinutes(); - var s = da.getSeconds(); + var h = ('0'+da.getHours()).slice(-2); + var m = ('0'+da.getMinutes()).slice(-2) + var s = ('0'+da.getSeconds()).slice(-2); return year + '-' + month + '-' + dt + ' ' + h + ':' + m + ':' + s; } diff --git a/web/src/plugins/router.js b/web/src/plugins/router.js index f574b08..07b42e5 100644 --- a/web/src/plugins/router.js +++ b/web/src/plugins/router.js @@ -20,6 +20,7 @@ const routes = [ {path: 'set/audit', component: () => import('@/pages/set/Audit')}, {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/ip_map', component: () => import('@/pages/user/IpMap')},