实现基础的DDoS防护

This commit is contained in:
刘祥超
2022-05-18 21:02:47 +08:00
parent d46bf37726
commit 6cd182b858
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

@@ -71,7 +71,6 @@ func (this *IndexAction) RunGet(params struct {
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
"nodeMaxThreads": cluster.NodeMaxThreads,
"nodeTCPMaxConnections": cluster.NodeTCPMaxConnections,
"autoOpenPorts": cluster.AutoOpenPorts,
}
@@ -79,7 +78,6 @@ func (this *IndexAction) RunGet(params struct {
this.Data["defaultNodeMaxThreads"] = nodeconfigs.DefaultMaxThreads
this.Data["defaultNodeMaxThreadsMin"] = nodeconfigs.DefaultMaxThreadsMin
this.Data["defaultNodeMaxThreadsMax"] = nodeconfigs.DefaultMaxThreadsMax
this.Data["defaultNodeTCPMaxConnections"] = nodeconfigs.DefaultTCPMaxConnections
this.Show()
}
@@ -92,7 +90,6 @@ func (this *IndexAction) RunPost(params struct {
InstallDir string
TimeZone string
NodeMaxThreads int32
NodeTCPMaxConnections int32
AutoOpenPorts bool
Must *actions.Must
@@ -118,7 +115,6 @@ func (this *IndexAction) RunPost(params struct {
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
NodeMaxThreads: params.NodeMaxThreads,
NodeTCPMaxConnections: params.NodeTCPMaxConnections,
AutoOpenPorts: params.AutoOpenPorts,
})
if err != nil {

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>