实现基础的DDoS防护

This commit is contained in:
GoEdgeLab
2022-05-18 21:02:47 +08:00
parent d7bff7e1c8
commit 6c2301733a
29 changed files with 939 additions and 48 deletions

View File

@@ -5,6 +5,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/groups"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/cache"
ddosProtection "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/ddos-protection"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/dns"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/ssh"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/settings/system"
@@ -56,6 +57,8 @@ func init() {
GetPost("/settings/ssh", new(ssh.IndexAction)).
GetPost("/settings/ssh/test", new(ssh.TestAction)).
GetPost("/settings/thresholds", new(thresholds.IndexAction)).
GetPost("/settings/ddos-protection", new(ddosProtection.IndexAction)).
Post("/settings/ddos-protection/status", new(ddosProtection.StatusAction)).
// 分组相关
Prefix("/clusters/cluster/groups").

View File

@@ -23,6 +23,11 @@ func InitNodeInfo(parentAction *actionutils.ParentAction, nodeId int64) (*pb.Nod
}
var node = nodeResp.Node
info, err := parentAction.RPC().NodeRPC().FindEnabledNodeConfigInfo(parentAction.AdminContext(), &pb.FindEnabledNodeConfigInfoRequest{NodeId: nodeId})
if err != nil {
return nil, err
}
var groupMap maps.Map
if node.NodeGroup != nil {
groupMap = maps.Map{
@@ -60,30 +65,38 @@ func InitNodeInfo(parentAction *actionutils.ParentAction, nodeId int64) (*pb.Nod
"name": "DNS设置",
"url": prefix + "/settings/dns?" + query,
"isActive": menuItem == "dns",
"isOn": len(node.DnsRoutes) > 0,
"isOn": info.HasDNSInfo,
},
{
"name": "缓存设置",
"url": prefix + "/settings/cache?" + query,
"isActive": menuItem == "cache",
"isOn": len(node.CacheDiskDir) > 0 ||
(node.MaxCacheDiskCapacity != nil && node.MaxCacheDiskCapacity.Count > 0) ||
(node.MaxCacheMemoryCapacity != nil && node.MaxCacheMemoryCapacity.Count > 0),
"isOn": info.HasCacheInfo,
},
{
"name": "DDOS防护",
"url": prefix + "/settings/ddos-protection?" + query,
"isActive": menuItem == "ddosProtection",
"isOn": info.HasDDoSProtection,
},
{
"name": "-",
"url": "",
},
}
menuItems = filterMenuItems(menuItems, menuItem, prefix, query)
menuItems = filterMenuItems(menuItems, menuItem, prefix, query, info)
menuItems = append(menuItems, []maps.Map{
{
"name": "SSH设置",
"url": prefix + "/settings/ssh?" + query,
"isActive": menuItem == "ssh",
"isOn": node.NodeLogin != nil,
"isOn": info.HasSSH,
},
{
"name": "系统设置",
"url": prefix + "/settings/system?" + query,
"isActive": menuItem == "system",
"isOn": node.MaxCPU > 0,
"isOn": info.HasSystemSettings,
},
}...)
parentAction.Data["leftMenuItems"] = menuItems

View File

@@ -5,9 +5,10 @@
package nodeutils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
func filterMenuItems(menuItems []maps.Map, menuItem string, prefix string, query string) []maps.Map {
func filterMenuItems(menuItems []maps.Map, menuItem string, prefix string, query string, info *pb.FindEnabledNodeConfigInfoResponse) []maps.Map {
return menuItems
}

View File

@@ -0,0 +1,133 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ddosProtection
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node/nodeutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ddosconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
"net"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "node", "update")
this.SecondMenu("ddosProtection")
}
func (this *IndexAction) RunGet(params struct {
ClusterId int64
NodeId int64
}) {
_, err := nodeutils.InitNodeInfo(this.Parent(), params.NodeId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["nodeId"] = params.NodeId
// 集群设置
clusterProtectionResp, err := this.RPC().NodeClusterRPC().FindNodeClusterDDoSProtection(this.AdminContext(), &pb.FindNodeClusterDDoSProtectionRequest{NodeClusterId: params.ClusterId})
if err != nil {
this.ErrorPage(err)
return
}
var clusterDDoSProtectionIsOn = false
if len(clusterProtectionResp.DdosProtectionJSON) > 0 {
var clusterDDoSProtection = &ddosconfigs.ProtectionConfig{}
err = json.Unmarshal(clusterProtectionResp.DdosProtectionJSON, clusterDDoSProtection)
if err != nil {
this.ErrorPage(err)
return
}
clusterDDoSProtectionIsOn = clusterDDoSProtection.IsOn()
}
this.Data["clusterDDoSProtectionIsOn"] = clusterDDoSProtectionIsOn
// 节点设置
ddosProtectionResp, err := this.RPC().NodeRPC().FindNodeDDoSProtection(this.AdminContext(), &pb.FindNodeDDoSProtectionRequest{NodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
var ddosProtectionConfig = ddosconfigs.DefaultProtectionConfig()
if len(ddosProtectionResp.DdosProtectionJSON) > 0 {
err = json.Unmarshal(ddosProtectionResp.DdosProtectionJSON, ddosProtectionConfig)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["config"] = ddosProtectionConfig
this.Data["defaultConfigs"] = nodeconfigs.DefaultConfigs
this.Show()
}
func (this *IndexAction) RunPost(params struct {
NodeId int64
DdosProtectionJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改节点 %d 的DDOS防护设置", params.NodeId)
var ddosProtectionConfig = &ddosconfigs.ProtectionConfig{}
err := json.Unmarshal(params.DdosProtectionJSON, ddosProtectionConfig)
if err != nil {
this.ErrorPage(err)
return
}
err = ddosProtectionConfig.Init()
if err != nil {
this.Fail("配置校验失败:" + err.Error())
}
// 校验参数
if ddosProtectionConfig.TCP != nil {
var tcpConfig = ddosProtectionConfig.TCP
if tcpConfig.MaxConnectionsPerIP > 0 && tcpConfig.MaxConnectionsPerIP < nodeconfigs.DefaultTCPMinConnectionsPerIP {
this.FailField("tcpMaxConnectionsPerIP", "TCP: 单IP TCP最大连接数不能小于"+types.String(nodeconfigs.DefaultTCPMinConnectionsPerIP))
}
if tcpConfig.NewConnectionsRate > 0 && tcpConfig.NewConnectionsRate < nodeconfigs.DefaultTCPNewConnectionsMinRate {
this.FailField("tcpNewConnectionsRate", "TCP: 单IP连接速率不能小于"+types.String(nodeconfigs.DefaultTCPNewConnectionsMinRate))
}
// Port
for _, portConfig := range tcpConfig.Ports {
if portConfig.Port > 65535 {
this.Fail("端口号" + types.String(portConfig.Port) + "不能大于65535")
}
}
// IP
for _, ipConfig := range tcpConfig.AllowIPList {
if net.ParseIP(ipConfig.IP) == nil {
this.Fail("白名单IP '" + ipConfig.IP + "' 格式错误")
}
}
}
_, err = this.RPC().NodeRPC().UpdateNodeDDoSProtection(this.AdminContext(), &pb.UpdateNodeDDoSProtectionRequest{
NodeId: params.NodeId,
DdosProtectionJSON: params.DdosProtectionJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ddosProtection
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/nodeutils"
"github.com/TeaOSLab/EdgeCommon/pkg/messageconfigs"
)
type StatusAction struct {
actionutils.ParentAction
}
func (this *StatusAction) RunPost(params struct {
NodeId int64
}) {
results, err := nodeutils.SendMessageToNodeIds(this.AdminContext(), []int64{params.NodeId}, messageconfigs.MessageCodeCheckLocalFirewall, &messageconfigs.CheckLocalFirewallMessage{
Name: "nftables",
}, 10)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["results"] = results
this.Success()
}

View File

@@ -32,6 +32,8 @@ func (this *IndexAction) RunGet(params struct {
return
}
this.Data["hostIsAutoFilled"] = false
// 登录信息
var loginMap maps.Map = nil
if node.NodeLogin != nil {
@@ -79,6 +81,7 @@ func (this *IndexAction) RunGet(params struct {
return
}
if len(addressesResp.NodeIPAddresses) > 0 {
this.Data["hostIsAutoFilled"] = true
loginMap = maps.Map{
"id": 0,
"name": "",
@@ -100,6 +103,7 @@ func (this *IndexAction) RunGet(params struct {
return
}
if len(addressesResp.NodeIPAddresses) > 0 {
this.Data["hostIsAutoFilled"] = true
loginParams["host"] = addressesResp.NodeIPAddresses[0].Ip
}
}

View File

@@ -0,0 +1,106 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ddosProtection
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ddosconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
"net"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "")
this.SecondMenu("ddosProtection")
}
func (this *IndexAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["clusterId"] = params.ClusterId
protectionResp, err := this.RPC().NodeClusterRPC().FindNodeClusterDDoSProtection(this.AdminContext(), &pb.FindNodeClusterDDoSProtectionRequest{NodeClusterId: params.ClusterId})
if err != nil {
this.ErrorPage(err)
return
}
var ddosProtectionConfig = ddosconfigs.DefaultProtectionConfig()
if len(protectionResp.DdosProtectionJSON) > 0 {
err = json.Unmarshal(protectionResp.DdosProtectionJSON, ddosProtectionConfig)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["config"] = ddosProtectionConfig
this.Data["defaultConfigs"] = nodeconfigs.DefaultConfigs
this.Show()
}
func (this *IndexAction) RunPost(params struct {
ClusterId int64
DdosProtectionJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改集群 %d 的DDOS防护设置", params.ClusterId)
var ddosProtectionConfig = &ddosconfigs.ProtectionConfig{}
err := json.Unmarshal(params.DdosProtectionJSON, ddosProtectionConfig)
if err != nil {
this.ErrorPage(err)
return
}
err = ddosProtectionConfig.Init()
if err != nil {
this.Fail("配置校验失败:" + err.Error())
}
// 校验参数
if ddosProtectionConfig.TCP != nil {
var tcpConfig = ddosProtectionConfig.TCP
if tcpConfig.MaxConnectionsPerIP > 0 && tcpConfig.MaxConnectionsPerIP < nodeconfigs.DefaultTCPMinConnectionsPerIP {
this.FailField("tcpMaxConnectionsPerIP", "TCP: 单IP TCP最大连接数不能小于"+types.String(nodeconfigs.DefaultTCPMinConnectionsPerIP))
}
if tcpConfig.NewConnectionsRate > 0 && tcpConfig.NewConnectionsRate < nodeconfigs.DefaultTCPNewConnectionsMinRate {
this.FailField("tcpNewConnectionsRate", "TCP: 单IP连接速率不能小于"+types.String(nodeconfigs.DefaultTCPNewConnectionsMinRate))
}
// Port
for _, portConfig := range tcpConfig.Ports {
if portConfig.Port > 65535 {
this.Fail("端口号" + types.String(portConfig.Port) + "不能大于65535")
}
}
// IP
for _, ipConfig := range tcpConfig.AllowIPList {
if net.ParseIP(ipConfig.IP) == nil {
this.Fail("白名单IP '" + ipConfig.IP + "' 格式错误")
}
}
}
_, err = this.RPC().NodeClusterRPC().UpdateNodeClusterDDoSProtection(this.AdminContext(), &pb.UpdateNodeClusterDDoSProtectionRequest{
NodeClusterId: params.ClusterId,
DdosProtectionJSON: params.DdosProtectionJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,71 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ddosProtection
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/nodeutils"
"github.com/TeaOSLab/EdgeCommon/pkg/messageconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ddosconfigs"
"github.com/iwind/TeaGo/maps"
)
type StatusAction struct {
actionutils.ParentAction
}
func (this *StatusAction) Init() {
this.Nav("", "setting", "")
this.SecondMenu("ddosProtection")
}
func (this *StatusAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["clusterId"] = params.ClusterId
this.Show()
}
func (this *StatusAction) RunPost(params struct {
ClusterId int64
}) {
results, err := nodeutils.SendMessageToCluster(this.AdminContext(), params.ClusterId, messageconfigs.MessageCodeCheckLocalFirewall, &messageconfigs.CheckLocalFirewallMessage{
Name: "nftables",
}, 10)
if err != nil {
this.ErrorPage(err)
return
}
var resultMaps = []maps.Map{}
for _, result := range results {
var resultMap = maps.Map{
"isOk": result.IsOK,
"message": result.Message,
"nodeId": result.NodeId,
"nodeName": result.NodeName,
}
nodeResp, err := this.RPC().NodeRPC().FindNodeDDoSProtection(this.AdminContext(), &pb.FindNodeDDoSProtectionRequest{NodeId: result.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
if len(nodeResp.DdosProtectionJSON) > 0 {
var ddosProtection = ddosconfigs.DefaultProtectionConfig()
err = json.Unmarshal(nodeResp.DdosProtectionJSON, ddosProtection)
if err != nil {
this.ErrorPage(err)
return
}
resultMap["isPrior"] = !ddosProtection.IsPriorEmpty()
}
resultMaps = append(resultMaps, resultMap)
}
this.Data["results"] = resultMaps
this.Success()
}

View File

@@ -66,34 +66,31 @@ func (this *IndexAction) RunGet(params struct {
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(cluster.TimeZone)
this.Data["cluster"] = maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
"nodeMaxThreads": cluster.NodeMaxThreads,
"nodeTCPMaxConnections": cluster.NodeTCPMaxConnections,
"autoOpenPorts": cluster.AutoOpenPorts,
"id": cluster.Id,
"name": cluster.Name,
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
"nodeMaxThreads": cluster.NodeMaxThreads,
"autoOpenPorts": cluster.AutoOpenPorts,
}
// 默认值
this.Data["defaultNodeMaxThreads"] = nodeconfigs.DefaultMaxThreads
this.Data["defaultNodeMaxThreadsMin"] = nodeconfigs.DefaultMaxThreadsMin
this.Data["defaultNodeMaxThreadsMax"] = nodeconfigs.DefaultMaxThreadsMax
this.Data["defaultNodeTCPMaxConnections"] = nodeconfigs.DefaultTCPMaxConnections
this.Show()
}
// RunPost 保存设置
func (this *IndexAction) RunPost(params struct {
ClusterId int64
Name string
GrantId int64
InstallDir string
TimeZone string
NodeMaxThreads int32
NodeTCPMaxConnections int32
AutoOpenPorts bool
ClusterId int64
Name string
GrantId int64
InstallDir string
TimeZone string
NodeMaxThreads int32
AutoOpenPorts bool
Must *actions.Must
}) {
@@ -112,14 +109,13 @@ func (this *IndexAction) RunPost(params struct {
}
_, err := this.RPC().NodeClusterRPC().UpdateNodeCluster(this.AdminContext(), &pb.UpdateNodeClusterRequest{
NodeClusterId: params.ClusterId,
Name: params.Name,
NodeGrantId: params.GrantId,
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
NodeMaxThreads: params.NodeMaxThreads,
NodeTCPMaxConnections: params.NodeTCPMaxConnections,
AutoOpenPorts: params.AutoOpenPorts,
NodeClusterId: params.ClusterId,
Name: params.Name,
NodeGrantId: params.GrantId,
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
NodeMaxThreads: params.NodeMaxThreads,
AutoOpenPorts: params.AutoOpenPorts,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -3,6 +3,7 @@ package settings
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cache"
ddosProtection "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/ddos-protection"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/dns"
firewallActions "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/firewall-actions"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/health"
@@ -79,6 +80,13 @@ func init() {
// WebP
Prefix("/clusters/cluster/settings/webp").
GetPost("", new(webp.IndexAction)).
// DDOS Protection
Prefix("/clusters/cluster/settings/ddos-protection").
GetPost("", new(ddosProtection.IndexAction)).
GetPost("/status", new(ddosProtection.StatusAction)).
//
EndAll()
})
}

View File

@@ -117,6 +117,13 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, info *pb.F
"isOn": info != nil && info.HasFirewallActions,
})
items = append(items, maps.Map{
"name": "DDOS防护",
"url": "/clusters/cluster/settings/ddos-protection?clusterId=" + clusterId,
"isActive": selectedItem == "ddosProtection",
"isOn": info != nil && info.HasDDoSProtection,
})
items = append(items, maps.Map{
"name": "健康检查",
"url": "/clusters/cluster/settings/health?clusterId=" + clusterId,
@@ -136,6 +143,11 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, info *pb.F
"isActive": selectedItem == "webp",
"isOn": info != nil && info.WebpIsOn,
})
items = append(items, maps.Map{
"name": "-",
})
items = append(items, maps.Map{
"name": "统计指标",
"url": "/clusters/cluster/settings/metrics?clusterId=" + clusterId,

View File

@@ -0,0 +1,112 @@
Vue.component("ddos-protection-ip-list-config-box", {
props: ["v-ip-list"],
data: function () {
let list = this.vIpList
if (list == null) {
list = []
}
return {
list: list,
isAdding: false,
addingIP: {
ip: "",
description: ""
}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingIPInput.focus()
})
},
confirm: function () {
let ip = this.addingIP.ip
if (ip.length == 0) {
this.warn("请输入IP")
return
}
let exists = false
this.list.forEach(function (v) {
if (v.ip == ip) {
exists = true
}
})
if (exists) {
this.warn("IP '" + ip + "'已经存在")
return
}
let that = this
Tea.Vue.$post("/ui/validateIPs")
.params({
ips: [ip]
})
.success(function () {
that.list.push({
ip: ip,
description: that.addingIP.description
})
that.notifyChange()
that.cancel()
})
.fail(function () {
that.warn("请输入正确的IP")
})
},
cancel: function () {
this.isAdding = false
this.addingIP = {
ip: "",
description: ""
}
},
remove: function (index) {
this.list.$remove(index)
this.notifyChange()
},
warn: function (message) {
let that = this
teaweb.warn(message, function () {
that.$refs.addingIPInput.focus()
})
},
notifyChange: function () {
this.$emit("change", this.list)
}
},
template: `<div>
<div v-if="list.length > 0">
<div class="ui label basic tiny" v-for="(ipConfig, index) in list">
{{ipConfig.ip}} <span class="grey small" v-if="ipConfig.description.length > 0">{{ipConfig.description}}</span> <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
</div>
<div class="ui divider"></div>
</div>
<div v-if="isAdding">
<div class="ui fields inline">
<div class="ui field">
<div class="ui input left labeled">
<span class="ui label">IP</span>
<input type="text" v-model="addingIP.ip" ref="addingIPInput" maxlength="40" size="20" placeholder="IP" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
</div>
<div class="ui field">
<div class="ui input left labeled">
<span class="ui label">备注</span>
<input type="text" v-model="addingIP.description" maxlength="10" size="10" placeholder="备注(可选)" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
&nbsp;<a href="" @click.prevent="cancel()">取消</a>
</div>
</div>
</div>
<div v-if="!isAdding">
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
</div>
</div>`
})

View File

@@ -0,0 +1,115 @@
Vue.component("ddos-protection-ports-config-box", {
props: ["v-ports"],
data: function () {
let ports = this.vPorts
if (ports == null) {
ports = []
}
return {
ports: ports,
isAdding: false,
addingPort: {
port: "",
description: ""
}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingPortInput.focus()
})
},
confirm: function () {
let portString = this.addingPort.port
if (portString.length == 0) {
this.warn("请输入端口号")
return
}
if (!/^\d+$/.test(portString)) {
this.warn("请输入正确的端口号")
return
}
let port = parseInt(portString, 10)
if (port <= 0) {
this.warn("请输入正确的端口号")
return
}
if (port > 65535) {
this.warn("请输入正确的端口号")
return
}
let exists = false
this.ports.forEach(function (v) {
if (v.port == port) {
exists = true
}
})
if (exists) {
this.warn("端口号已经存在")
return
}
this.ports.push({
port: port,
description: this.addingPort.description
})
this.notifyChange()
this.cancel()
},
cancel: function () {
this.isAdding = false
this.addingPort = {
port: "",
description: ""
}
},
remove: function (index) {
this.ports.$remove(index)
this.notifyChange()
},
warn: function (message) {
let that = this
teaweb.warn(message, function () {
that.$refs.addingPortInput.focus()
})
},
notifyChange: function () {
this.$emit("change", this.ports)
}
},
template: `<div>
<div v-if="ports.length > 0">
<div class="ui label basic tiny" v-for="(portConfig, index) in ports">
{{portConfig.port}} <span class="grey small" v-if="portConfig.description.length > 0">{{portConfig.description}}</span> <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
</div>
<div class="ui divider"></div>
</div>
<div v-if="isAdding">
<div class="ui fields inline">
<div class="ui field">
<div class="ui input left labeled">
<span class="ui label">端口</span>
<input type="text" v-model="addingPort.port" ref="addingPortInput" maxlength="5" size="5" placeholder="端口号" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
</div>
<div class="ui field">
<div class="ui input left labeled">
<span class="ui label">备注</span>
<input type="text" v-model="addingPort.description" maxlength="12" size="12" placeholder="备注(可选)" @keyup.enter="confirm" @keypress.enter.prevent="1"/>
</div>
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button>
&nbsp;<a href="" @click.prevent="cancel()">取消</a>
</div>
</div>
</div>
<div v-if="!isAdding">
<button class="ui button tiny" type="button" @click.prevent="add">+</button>
</div>
</div>`
})

View File

@@ -0,0 +1,110 @@
Vue.component("node-ddos-protection-config-box", {
props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"],
data: function () {
let config = this.vDdosProtectionConfig
if (config == null) {
config = {
tcp: {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
allowIPList: [],
ports: []
}
}
}
// initialize
if (config.tcp == null) {
config.tcp = {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
allowIPList: [],
ports: []
}
}
return {
config: config,
defaultConfigs: this.vDefaultConfigs,
isNode: this.vIsNode,
isAddingPort: false
}
},
methods: {
changeTCPPorts: function (ports) {
this.config.tcp.ports = ports
},
changeTCPAllowIPList: function (ipList) {
this.config.tcp.allowIPList = ipList
}
},
template: `<div>
<input type="hidden" name="ddosProtectionJSON" :value="JSON.stringify(config)"/>
<p class="comment">功能说明:此功能为<strong>试验性质</strong>目前仅能防御简单的DDoS攻击试验期间建议仅在被攻击时启用仅支持已安装<code-label>nftables v0.9</code-label>以上的Linux系统。<pro-warning-label></pro-warning-label></p>
<div class="ui message" v-if="vClusterIsOn">当前节点所在集群已设置DDoS防护。</div>
<h4>TCP设置</h4>
<table class="ui table definition selectable">
<prior-checkbox :v-config="config.tcp" v-if="isNode"></prior-checkbox>
<tbody v-show="config.tcp.isPrior || !isNode">
<tr>
<td class="title">启用</td>
<td>
<checkbox v-model="config.tcp.isOn"></checkbox>
</td>
</tr>
</tbody>
<tbody v-show="config.tcp.isOn && (config.tcp.isPrior || !isNode)">
<tr>
<td class="title">单节点TCP最大连接数</td>
<td>
<digit-input name="tcpMaxConnections" v-model="config.tcp.maxConnections" maxlength="6" size="6" style="width: 6em"></digit-input>
<p class="comment">单个节点可以接受的TCP最大连接数。如果为0则默认为{{defaultConfigs.tcpMaxConnections}}。</p>
</td>
</tr>
<tr>
<td>单IP TCP最大连接数</td>
<td>
<digit-input name="tcpMaxConnectionsPerIP" v-model="config.tcp.maxConnectionsPerIP" maxlength="6" size="6" style="width: 6em"></digit-input>
<p class="comment">单个IP可以连接到节点的TCP最大连接数。如果为0则默认为{{defaultConfigs.tcpMaxConnectionsPerIP}};最小值为{{defaultConfigs.tcpMinConnectionsPerIP}}。</p>
</td>
</tr>
<tr>
<td>单IP TCP新连接速率</td>
<td>
<div class="ui input right labeled">
<digit-input name="tcpNewConnectionsRate" v-model="config.tcp.newConnectionsRate" maxlength="6" size="6" style="width: 6em" :min="defaultConfigs.tcpNewConnectionsMinRate"></digit-input>
<span class="ui label">个新连接/每分钟</span>
</div>
<p class="comment">单个IP可以创建TCP新连接的速率。如果为0则默认为{{defaultConfigs.tcpNewConnectionsRate}};最小值为{{defaultConfigs.tcpNewConnectionsMinRate}}。</p>
</td>
</tr>
<tr>
<td>TCP端口列表</td>
<td>
<ddos-protection-ports-config-box :v-ports="config.tcp.ports" @change="changeTCPPorts"></ddos-protection-ports-config-box>
<p class="comment">默认为80和443两个端口。</p>
</td>
</tr>
<tr>
<td>IP白名单</td>
<td>
<ddos-protection-ip-list-config-box :v-ip-list="config.tcp.allowIPList" @change="changeTCPAllowIPList"></ddos-protection-ip-list-config-box>
<p class="comment">在白名单中的IP不受当前设置的限制。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,63 @@
Vue.component("digit-input", {
props: ["value", "maxlength", "size", "min", "max", "required", "placeholder"],
mounted: function () {
let that = this
setTimeout(function () {
that.check()
})
},
data: function () {
let realMaxLength = this.maxlength
if (realMaxLength == null) {
realMaxLength = 20
}
let realSize = this.size
if (realSize == null) {
realSize = 6
}
return {
realValue: this.value,
realMaxLength: realMaxLength,
realSize: realSize,
isValid: true
}
},
watch: {
realValue: function (v) {
this.notifyChange()
}
},
methods: {
notifyChange: function () {
let v = parseInt(this.realValue.toString(), 10)
if (isNaN(v)) {
v = 0
}
this.check()
this.$emit("input", v)
},
check: function () {
if (this.realValue == null) {
return
}
let s = this.realValue.toString()
if (!/^\d+$/.test(s)) {
this.isValid = false
return
}
let v = parseInt(s, 10)
if (isNaN(v)) {
this.isValid = false
} else {
if (this.required) {
this.isValid = (this.min == null || this.min <= v) && (this.max == null || this.max >= v)
} else {
this.isValid = (v == 0 || (this.min == null || this.min <= v) && (this.max == null || this.max >= v))
}
}
}
},
template: `<input type="text" v-model="realValue" :maxlength="realMaxLength" :size="realSize" :class="{error: !this.isValid}" :placeholder="placeholder"/>`
})

View File

@@ -34,7 +34,7 @@ Vue.component("dns-resolver-config-box", {
<select class="ui dropdown auto-width" v-model="config.type">
<option v-for="t in types" :value="t.code">{{t.name}}</option>
</select>
<p class="comment">修改此项配置后,需要重启节点进程才会生效。<pro-warning-label></pro-warning-label></p>
<p class="comment">边缘节点使用的DNS解析库。修改此项配置后,需要重启节点进程才会生效。<pro-warning-label></pro-warning-label></p>
</td>
</tr>
</table>

View File

@@ -1,8 +1,13 @@
Vue.component("prior-checkbox", {
props: ["v-config"],
props: ["v-config", "description"],
data: function () {
let description = this.description
if (description == null) {
description = "打开后可以覆盖父级或子级配置"
}
return {
isPrior: this.vConfig.isPrior
isPrior: this.vConfig.isPrior,
realDescription: description
}
},
watch: {
@@ -18,7 +23,7 @@ Vue.component("prior-checkbox", {
<input type="checkbox" v-model="isPrior"/>
<label class="red"></label>
</div>
<p class="comment"><strong v-if="isPrior">[已打开]</strong> 打开后可以覆盖父级或子级配置。</p>
<p class="comment"><strong v-if="isPrior">[已打开]</strong> {{realDescription}}。</p>
</td>
</tr>
</tbody>`

View File

@@ -0,0 +1,17 @@
{$layout}
{$template "/clusters/cluster/node/node_menu"}
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="nodeId" :value="nodeId"/>
<node-ddos-protection-config-box :v-ddos-protection-config="config" :v-default-configs="defaultConfigs" :v-is-node="true" :v-cluster-is-on="clusterDDoSProtectionIsOn"></node-ddos-protection-config-box>
<p class="ui message green" v-if="checkResult != null && checkResult.isOk">节点检查结果:{{checkResult.message}}</p>
<p class="ui message red" v-if="checkResult != null && !checkResult.isOk">节点检查结果:有错误发生:{{checkResult.message}}</p>
<submit-btn></submit-btn>
</form>
</div>

View File

@@ -0,0 +1,15 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
this.checkResult = null
this.$post(".status")
.params({
nodeId: this.nodeId
})
.success(function (resp) {
let results = resp.data.results
if (results.length > 0) {
this.checkResult = results[0]
}
})
})

View File

@@ -13,7 +13,7 @@
<td class="title">SSH主机地址</td>
<td>
<input type="text" name="sshHost" maxlength="64" v-model="sshHost"/>
<p class="comment">比如192.168.1.100</p>
<p class="comment"><span v-if="hostIsAutoFilled"><strong>已自动填充,需要保存</strong></span>比如192.168.1.100</p>
</td>
</tr>
<tr>

View File

@@ -0,0 +1,19 @@
{$layout}
{$template "../menu"}
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
<first-menu>
<menu-item :href="'.?clusterId=' + clusterId" class="active">设置</menu-item>
<menu-item :href="'.status?clusterId=' + clusterId" >状态</menu-item>
</first-menu>
<form class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="clusterId" :value="clusterId"/>
<node-ddos-protection-config-box :v-ddos-protection-config="config" :v-default-configs="defaultConfigs"></node-ddos-protection-config-box>
<submit-btn></submit-btn>
</form>
</div>

View File

@@ -0,0 +1,3 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
})

View File

@@ -0,0 +1,44 @@
{$layout}
{$template "../menu"}
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
<first-menu>
<menu-item :href="'.?clusterId=' + clusterId">设置</menu-item>
<menu-item :href="'.status?clusterId=' + clusterId" class="active">状态</menu-item>
</first-menu>
<loading-message v-if="isLoading">检查中,请耐心等待完成...</loading-message>
<p class="comment" v-if="!isLoading && results.length == 0">当前集群下暂时还没有节点。</p>
<table class="ui table selectable" v-if="results.length > 0">
<thead>
<tr>
<th class="three wide">节点名称</th>
<th>检查结果</th>
<th class="two op">独立设置</th>
</tr>
</thead>
<tbody v-for="result in results">
<tr>
<td>
<a :href="'/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + result.nodeId">{{result.nodeName}}</a>
<div v-if="result.isPrior">
<a :href="'/clusters/cluster/node/settings/ddos-protection?clusterId=' + clusterId + '&nodeId=' + result.nodeId"><grey-label>定制</grey-label></a>
</div>
</td>
<td>
<span class="green" v-if="result.isOk">{{result.message}}</span>
<span class="red" v-if="!result.isOk">{{result.message}}</span>
</td>
<td>
<a :href="'/clusters/cluster/node/settings/ddos-protection?clusterId=' + clusterId + '&nodeId=' + result.nodeId">设置</a>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
<a href="" @click.prevent="reload">[刷新]</a>
</div>

View File

@@ -0,0 +1,20 @@
Tea.context(function () {
this.isLoading = true
this.results = []
this.$delay(function () {
this.reload()
})
this.reload = function () {
this.isLoading = true
this.$post("$")
.params({ clusterId: this.clusterId })
.success(function (resp) {
this.results = resp.data.results
})
.done(function () {
this.isLoading = false
})
}
})

View File

@@ -53,13 +53,6 @@
<p class="comment">取值在{{defaultNodeMaxThreadsMin}}和{{defaultNodeMaxThreadsMax}}之间如果为0则默认为{{defaultNodeMaxThreads}}。</p>
</td>
</tr>
<tr>
<td>单节点TCP最大连接数</td>
<td>
<input type="text" name="nodeTCPMaxConnections" maxlength="6" v-model="cluster.nodeTCPMaxConnections"/>
<p class="comment">如果为0则默认为{{defaultNodeTCPMaxConnections}}。</p>
</td>
</tr>
<tr>
<td>自动开放端口</td>
<td>

View File

@@ -70,8 +70,8 @@
<table class="ui table selectable celled" v-if="logs.length > 0">
<thead>
<tr>
<th>集群</th>
<th>节点</th>
<th class="two wide">集群</th>
<th class="two wide">节点</th>
<th>信息</th>
<th style="width: 5em">操作</th>
</tr>

View File

@@ -31,6 +31,7 @@
<span v-if="task.type == 'ipItemChanged'">同步IP名单</span>
<span v-if="task.type == 'scriptsChanged'">同步脚本</span>
<span v-if="task.type == 'nodeLevelChanged'">同步L2节点</span>
<span v-if="task.type == 'ddosProtectionChanged'">DDoS配置</span>
</td>
<td>
<span v-if="task.isDone" class="red">{{task.error}}</span>

View File

@@ -93,7 +93,7 @@
</td>
</tr>
<tr>
<td>记录日志</td>
<td>记录访问日志</td>
<td>
<span v-if="firewallPolicy.log == null || !firewallPolicy.log.isOn">默认</span>
<span v-else class="green">开启</span>

View File

@@ -51,7 +51,7 @@
</td>
</tr>
<tr>
<td>记录日志</td>
<td>记录访问日志</td>
<td>
<input type="hidden" name="logJSON" :value="JSON.stringify(firewallPolicy.log)"/>
<checkbox name="" v-model="firewallPolicy.log.isOn"></checkbox>