diff --git a/server/admin/api_group.go b/server/admin/api_group.go index 44522a9..f5fa6d7 100644 --- a/server/admin/api_group.go +++ b/server/admin/api_group.go @@ -75,6 +75,10 @@ func GroupDetail(w http.ResponseWriter, r *http.Request) { if len(data.Auth) == 0 { data.Auth["type"] = "local" } + // 兼容旧数据 + if data.SplitDns == nil { + data.SplitDns = []dbdata.ValData{} + } RespSucess(w, data) } diff --git a/server/conf/profile.xml b/server/conf/profile.xml index 914f53d..a821a88 100644 --- a/server/conf/profile.xml +++ b/server/conf/profile.xml @@ -9,6 +9,7 @@ IPSec true false + true AllowRemoteUsers AllowRemoteUsers pinAllowed diff --git a/server/dbdata/group.go b/server/dbdata/group.go index fa2270e..81a48cd 100644 --- a/server/dbdata/group.go +++ b/server/dbdata/group.go @@ -215,6 +215,7 @@ func SetGroup(g *Group) error { // DNS 判断 clientDns := []ValData{} for _, v := range g.ClientDns { + v.Val = strings.TrimSpace(v.Val) if v.Val != "" { ip := net.ParseIP(v.Val) if ip.String() != v.Val { @@ -229,6 +230,20 @@ func SetGroup(g *Group) error { return errors.New("默认路由,必须设置一个DNS") } g.ClientDns = clientDns + + splitDns := []ValData{} + for _, v := range g.SplitDns { + v.Val = strings.TrimSpace(v.Val) + if v.Val != "" { + ValidateDomainName(v.Val) + if !ValidateDomainName(v.Val) { + return errors.New("域名 错误") + } + splitDns = append(splitDns, v) + } + } + g.SplitDns = splitDns + // 域名拆分隧道,不能同时填写 g.DsIncludeDomains = strings.TrimSpace(g.DsIncludeDomains) g.DsExcludeDomains = strings.TrimSpace(g.DsExcludeDomains) diff --git a/server/dbdata/tables.go b/server/dbdata/tables.go index b70e45a..752a2a0 100644 --- a/server/dbdata/tables.go +++ b/server/dbdata/tables.go @@ -11,6 +11,7 @@ type Group struct { Note string `json:"note" xorm:"varchar(255)"` AllowLan bool `json:"allow_lan" xorm:"Bool"` ClientDns []ValData `json:"client_dns" xorm:"Text"` + SplitDns []ValData `json:"split_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"` diff --git a/server/handler/link_tunnel.go b/server/handler/link_tunnel.go index a854a07..cf618b0 100644 --- a/server/handler/link_tunnel.go +++ b/server/handler/link_tunnel.go @@ -86,7 +86,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { } cSess.CstpDpd = cstpDpd - dtlsPort := "4433" + dtlsPort := "443" if strings.Contains(base.Cfg.ServerDTLSAddr, ":") { ss := strings.Split(base.Cfg.ServerDTLSAddr, ":") dtlsPort = ss[1] @@ -131,6 +131,11 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { for _, v := range cSess.Group.ClientDns { HttpAddHeader(w, "X-CSTP-DNS", v.Val) } + // 分割dns + for _, v := range cSess.Group.SplitDns { + HttpAddHeader(w, "X-CSTP-Split-DNS", v.Val) + } + // 允许的路由 for _, v := range cSess.Group.RouteInclude { if strings.ToLower(v.Val) == dbdata.All { @@ -156,9 +161,9 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { HttpSetHeader(w, "X-CSTP-Keep", "true") HttpSetHeader(w, "X-CSTP-Tunnel-All-DNS", "false") - HttpSetHeader(w, "X-CSTP-Rekey-Time", "43200") // 172800 + HttpSetHeader(w, "X-CSTP-Rekey-Time", "86400") // 172800 HttpSetHeader(w, "X-CSTP-Rekey-Method", "new-tunnel") - HttpSetHeader(w, "X-DTLS-Rekey-Time", "43200") + HttpSetHeader(w, "X-DTLS-Rekey-Time", "86400") HttpSetHeader(w, "X-DTLS-Rekey-Method", "new-tunnel") HttpSetHeader(w, "X-CSTP-DPD", fmt.Sprintf("%d", cstpDpd)) @@ -180,7 +185,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) { HttpSetHeader(w, "X-CSTP-Routing-Filtering-Ignore", "false") HttpSetHeader(w, "X-CSTP-Quarantine", "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", "true") HttpSetHeader(w, "X-CSTP-TCP-Keepalive", "false") // 设置域名拆分隧道(移动端不支持) if mobile != "mobile" { diff --git a/version b/version index aa3545d..aac2dac 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.11.4 \ No newline at end of file +0.12.1 \ No newline at end of file diff --git a/web/src/pages/group/List.vue b/web/src/pages/group/List.vue index cc8fc55..e06ce3f 100644 --- a/web/src/pages/group/List.vue +++ b/web/src/pages/group/List.vue @@ -49,10 +49,11 @@ prop="bandwidth" label="带宽限制" width="90"> - - {{ convertBandwidth(scope.row.bandwidth, 'BYTE', 'Mbps') }} Mbps - 不限 - + + {{ convertBandwidth(scope.row.bandwidth, 'BYTE', 'Mbps') }} Mbps + + 不限 + - {{ item.val }} + {{ + item.val + }} + - {{ item.val }} + {{ + item.val + }} + - {{ readMore[`ri_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }} + + {{ readMore[`ri_${scope.row.id}`] ? "▲ 收起" : "▼ 更多" }} + @@ -84,12 +93,20 @@ label="路由排除" width="180"> - {{ item.val }} + {{ + item.val + }} + - {{ item.val }} + {{ + item.val + }} + - {{ readMore[`re_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }} + + {{ readMore[`re_${scope.row.id}`] ? "▲ 收起" : "▼ 更多" }} + @@ -108,7 +125,9 @@ {{ item.action }} => {{ item.val }} : {{ item.port }} - {{ readMore[`la_${ scope.row.id }`] ? "▲ 收起" : "▼ 更多" }} + + {{ readMore[`la_${scope.row.id}`] ? "▲ 收起" : "▼ 更多" }} + @@ -178,221 +197,263 @@ - - - - - - - - - - - - - - - - Mbps - - - - - - - 注:本地网络 指的是: - 运行 anyconnect 客户端的PC 所在的的网络,即本地路由网段。 - 开启后,PC本地路由网段的数据就不会走隧道链路转发数据了。 - 同时 anyconnect 客户端需要勾选本地网络(Allow Local Lan)的开关,功能才能生效。 - - - - - - 输入IP格式如: 192.168.0.10 - - - - - - - - - - - - - - - - - - - 启用 - 停用 - - - - - - - - 本地 - Radius - LDAP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 输入CIDR格式如: 192.168.1.0/24 - - - - - - - - - - - - - - - - - - - - - - - - - 输入CIDR格式如: 192.168.2.0/24 - - - - - - - - - - - - - - - - - - - - - - - - - - 输入CIDR格式如: 192.168.3.0/24 端口0表示所有端口,多个端口用','号分隔,连续端口:1234-5678 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 注:域名拆分隧道,仅支持AnyConnect的windows和MacOS桌面客户端,不支持移动端. - - - - - 测试登录 - - 保存 - 取消 + + + - - + + + + + + + + + + + + Mbps + + + + + + + 注:本地网络 指的是: + 运行 anyconnect 客户端的PC 所在的的网络,即本地路由网段。 + 开启后,PC本地路由网段的数据就不会走隧道链路转发数据了。 + 同时 anyconnect 客户端需要勾选本地网络(Allow Local Lan)的开关,功能才能生效。 + + + + + + 输入IP格式如: 192.168.0.10 + + + + + + + + + + + + + + + + + + + + 一般留空。如果输入域名,只有配置的域名(包含子域名)走配置的dns + + + + + + + + + + + + + + + + + + + + 启用 + 停用 + + + + + + + + 本地 + Radius + LDAP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 输入CIDR格式如: 192.168.1.0/24 + + + + + + + + + + + + + + + + + + + + + + + + + 输入CIDR格式如: 192.168.2.0/24 + + + + + + + + + + + + + + + + + + + + + + + + + + 输入CIDR格式如: 192.168.3.0/24 + 端口0表示所有端口,多个端口用','号分隔,连续端口:1234-5678 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 注:域名拆分隧道,仅支持AnyConnect的windows和MacOS桌面客户端,不支持移动端. + + + + + 测试登录 + + 保存 + 取消 + + + - - - - - - - - - 登录 - 取 消 - - + + + + + + + + + 登录 + 取 消 + + + :close-on-click-modal="false" + title="编辑模式" + :visible.sync="ipListDialog" + width="650px" + custom-class="valgin-dialog" + center> - - - 当前共 {{ ipEditForm.ip_list.trim() === '' ? 0 : ipEditForm.ip_list.trim().split("\n").length }} 条(注:AnyConnect客户端最多支持{{ this.maxRouteRows }}条路由) - - - 更新 - 取 消 - + + + 当前共 + {{ ipEditForm.ip_list.trim() === '' ? 0 : ipEditForm.ip_list.trim().split("\n").length }} + 条(注:AnyConnect客户端最多支持{{ this.maxRouteRows }}条路由) + + + + 更新 + 取 消 + @@ -457,47 +523,48 @@ export default { page: 1, tableData: [], count: 10, - activeTab : "general", + activeTab: "general", readMore: {}, - readMinRows : 5, - maxRouteRows : 2500, - defAuth : { - type:'local', - radius:{addr:"", secret:""}, - ldap:{ - addr:"", - tls:false, - base_dn:"", - object_class:"person", - search_attr:"sAMAccountName", - member_of:"", - bind_name:"", - bind_pwd:"", - }, + readMinRows: 5, + maxRouteRows: 2500, + defAuth: { + type: 'local', + radius: {addr: "", secret: ""}, + ldap: { + addr: "", + tls: false, + base_dn: "", + object_class: "person", + search_attr: "sAMAccountName", + member_of: "", + bind_name: "", + bind_pwd: "", + }, }, ruleForm: { bandwidth: 0, bandwidth_format: '0', status: 1, allow_lan: true, - client_dns: [{val: '114.114.114.114'}], + client_dns: [{val: '114.114.114.114', note: '默认dns'}], + split_dns: [], route_include: [{val: 'all', note: '默认全局代理'}], route_exclude: [], link_acl: [], - auth : {}, + auth: {}, }, - authLoginDialog : false, - ipListDialog : false, - authLoginLoading : false, - authLoginForm : { - name : "", - pwd : "", + authLoginDialog: false, + ipListDialog: false, + authLoginLoading: false, + authLoginForm: { + name: "", + pwd: "", }, ipEditForm: { ip_list: "", - type : "", + type: "", }, - ipEditLoading : false, + ipEditLoading: false, authLoginRules: { name: [ {required: true, message: '请输入账号', trigger: 'blur'}, @@ -548,11 +615,11 @@ export default { }, methods: { setAuthData(row) { - if (! row) { + if (!row) { this.ruleForm.auth = JSON.parse(JSON.stringify(this.defAuth)); - return ; + return; } - if (row.auth.type == "ldap" && ! row.auth.ldap.object_class) { + if (row.auth.type == "ldap" && !row.auth.ldap.object_class) { row.auth.ldap.object_class = this.defAuth.ldap.object_class; } this.ruleForm.auth = Object.assign(JSON.parse(JSON.stringify(this.defAuth)), row.auth); @@ -624,7 +691,8 @@ export default { // arr.pop() }, addDomain(arr) { - arr.push({val: "", action: "allow", port: 0}); + console.log("arr", arr) + arr.push({val: "", action: "allow", port: "0", note: ""}); }, submitForm(formName) { this.$refs[formName].validate((valid) => { @@ -650,29 +718,31 @@ export default { }); }, testAuthLogin() { - this.$refs["authLoginForm"].validate((valid) => { - if (!valid) { - console.log('error submit!!'); - return false; - } - this.authLoginLoading = true; - axios.post('/group/auth_login', {name:this.authLoginForm.name, - pwd:this.authLoginForm.pwd, - auth:this.ruleForm.auth}).then(resp => { - const rdata = resp.data; - if (rdata.code === 0) { - this.$message.success("登录成功"); - } else { - this.$message.error(rdata.msg); - } - this.authLoginLoading = false; - console.log(rdata); - }).catch(error => { - this.$message.error('哦,请求出错'); - console.log(error); - this.authLoginLoading = false; - }); + this.$refs["authLoginForm"].validate((valid) => { + if (!valid) { + console.log('error submit!!'); + return false; + } + this.authLoginLoading = true; + axios.post('/group/auth_login', { + name: this.authLoginForm.name, + pwd: this.authLoginForm.pwd, + auth: this.ruleForm.auth + }).then(resp => { + const rdata = resp.data; + if (rdata.code === 0) { + this.$message.success("登录成功"); + } else { + this.$message.error(rdata.msg); + } + this.authLoginLoading = false; + console.log(rdata); + }).catch(error => { + this.$message.error('哦,请求出错'); + console.log(error); + this.authLoginLoading = false; }); + }); }, openAuthLoginDialog() { this.$refs["ruleForm"].validate((valid) => { @@ -683,7 +753,7 @@ export default { this.authLoginDialog = true; // set authLoginFormName focus this.$nextTick(() => { - this.$refs['authLoginFormName'].focus(); + this.$refs['authLoginFormName'].focus(); }); }); }, @@ -693,63 +763,63 @@ export default { this.ipEditForm.ip_list = this.ruleForm[type].map(item => item.val + (item.note ? "," + item.note : "")).join("\n"); }, ipEdit() { - this.ipEditLoading = true; - let ipList = []; - if (this.ipEditForm.ip_list.trim() !== "") { - ipList = this.ipEditForm.ip_list.trim().split("\n"); + this.ipEditLoading = true; + let ipList = []; + if (this.ipEditForm.ip_list.trim() !== "") { + ipList = this.ipEditForm.ip_list.trim().split("\n"); + } + let arr = []; + for (let i = 0; i < ipList.length; i++) { + let item = ipList[i]; + if (item.trim() === "") { + continue; } - let arr = []; - for (let i = 0; i < ipList.length; i++) { - let item = ipList[i]; - if (item.trim() === "") { - continue; - } - let ip = item.split(","); - if (ip.length > 2) { - ip[1] = ip.slice(1).join(","); - } - let note = ip[1] ? ip[1] : ""; - const pushToArr = () => { - arr.push({val: ip[0], note: note}); - }; - if (this.ipEditForm.type == "route_include" && ip[0] == "all") { - pushToArr(); - continue; - } - let valid = this.isValidCIDR(ip[0]); - if (!valid.valid) { - this.$message.error("错误:CIDR格式错误,建议 " + ip[0] + " 改为 " + valid.suggestion); - this.ipEditLoading = false; - return; - } + let ip = item.split(","); + if (ip.length > 2) { + ip[1] = ip.slice(1).join(","); + } + let note = ip[1] ? ip[1] : ""; + const pushToArr = () => { + arr.push({val: ip[0], note: note}); + }; + if (this.ipEditForm.type == "route_include" && ip[0] == "all") { pushToArr(); + continue; } - this.ruleForm[this.ipEditForm.type] = arr; - this.ipEditLoading = false; - this.ipListDialog = false; + let valid = this.isValidCIDR(ip[0]); + if (!valid.valid) { + this.$message.error("错误:CIDR格式错误,建议 " + ip[0] + " 改为 " + valid.suggestion); + this.ipEditLoading = false; + return; + } + pushToArr(); + } + this.ruleForm[this.ipEditForm.type] = arr; + this.ipEditLoading = false; + this.ipListDialog = false; }, isValidCIDR(input) { - const cidrRegex = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)\/([12]?\d|3[0-2])$/; - if (!cidrRegex.test(input)) { - return { valid: false, suggestion: null }; + const cidrRegex = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)\/([12]?\d|3[0-2])$/; + if (!cidrRegex.test(input)) { + return {valid: false, suggestion: null}; + } + const [ip, mask] = input.split('/'); + const maskNum = parseInt(mask); + const ipParts = ip.split('.').map(part => parseInt(part)); + const binaryIP = ipParts.map(part => part.toString(2).padStart(8, '0')).join(''); + for (let i = maskNum; i < 32; i++) { + if (binaryIP[i] === '1') { + const binaryNetworkPart = binaryIP.substring(0, maskNum).padEnd(32, '0'); + const networkIPParts = []; + for (let j = 0; j < 4; j++) { + const octet = binaryNetworkPart.substring(j * 8, (j + 1) * 8); + networkIPParts.push(parseInt(octet, 2)); + } + const suggestedIP = networkIPParts.join('.'); + return {valid: false, suggestion: `${suggestedIP}/${mask}`}; } - const [ip, mask] = input.split('/'); - const maskNum = parseInt(mask); - const ipParts = ip.split('.').map(part => parseInt(part)); - const binaryIP = ipParts.map(part => part.toString(2).padStart(8, '0')).join(''); - for (let i = maskNum; i < 32; i++) { - if (binaryIP[i] === '1') { - const binaryNetworkPart = binaryIP.substring(0, maskNum).padEnd(32, '0'); - const networkIPParts = []; - for (let j = 0; j < 4; j++) { - const octet = binaryNetworkPart.substring(j * 8, (j + 1) * 8); - networkIPParts.push(parseInt(octet, 2)); - } - const suggestedIP = networkIPParts.join('.'); - return { valid: false, suggestion: `${suggestedIP}/${mask}` }; - } - } - return { valid: true, suggestion: null }; + } + return {valid: true, suggestion: null}; }, resetForm(formName) { this.$refs[formName].resetFields(); @@ -766,7 +836,7 @@ export default { }, beforeTabLeave() { var isSwitch = true - if (! this.user_edit_dialog) { + if (!this.user_edit_dialog) { return isSwitch; } this.$refs['ruleForm'].validate((valid) => { @@ -783,16 +853,16 @@ export default { this.activeTab = "general"; }, convertBandwidth(bandwidth, fromUnit, toUnit) { - const units = { - bps: 1, - Kbps: 1000, - Mbps: 1000000, - Gbps: 1000000000, - BYTE: 8, - }; - const result = bandwidth * units[fromUnit] / units[toUnit]; - const fixedResult = result.toFixed(2); - return parseFloat(fixedResult); + const units = { + bps: 1, + Kbps: 1000, + Mbps: 1000000, + Gbps: 1000000000, + BYTE: 8, + }; + const result = bandwidth * units[fromUnit] / units[toUnit]; + const fixedResult = result.toFixed(2); + return parseFloat(fixedResult); } }, } @@ -813,19 +883,20 @@ export default { width: 80px; } -::v-deep .valgin-dialog{ - display: flex; - flex-direction: column; - margin:0 !important; - position:absolute; - top:50%; - left:50%; - transform:translate(-50%,-50%); - max-height:calc(100% - 30px); - max-width:calc(100% - 30px); +::v-deep .valgin-dialog { + display: flex; + flex-direction: column; + margin: 0 !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-height: calc(100% - 30px); + max-width: calc(100% - 30px); } -::v-deep .valgin-dialog .el-dialog__body{ - flex:1; - overflow: auto; + +::v-deep .valgin-dialog .el-dialog__body { + flex: 1; + overflow: auto; }