mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
refactor: 重构防火墙
This commit is contained in:
@@ -60,7 +60,7 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
|
||||
}
|
||||
|
||||
func (r *safeRepo) GetPingStatus() (bool, error) {
|
||||
out, err := shell.Execf(`firewall-cmd --list-all`)
|
||||
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
|
||||
if err != nil {
|
||||
return true, errors.New(out)
|
||||
}
|
||||
|
||||
@@ -13,3 +13,18 @@ type FirewallRule struct {
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type FirewallIPRule struct {
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
|
||||
Address string `json:"address"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type FirewallForward struct {
|
||||
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
|
||||
Port uint `json:"port" validate:"required,gte=1,lte=65535"`
|
||||
TargetIP string `json:"target_ip" validate:"required"`
|
||||
TargetPort uint `json:"target_port" validate:"required,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
@@ -148,6 +148,12 @@ func Http(r chi.Router) {
|
||||
r.Get("/rule", firewall.GetRules)
|
||||
r.Post("/rule", firewall.CreateRule)
|
||||
r.Delete("/rule", firewall.DeleteRule)
|
||||
r.Get("/ipRule", firewall.GetIPRules)
|
||||
r.Post("/ipRule", firewall.CreateIPRule)
|
||||
r.Delete("/ipRule", firewall.DeleteIPRule)
|
||||
r.Get("/forward", firewall.GetForwards)
|
||||
r.Post("/forward", firewall.CreateForward)
|
||||
r.Delete("/forward", firewall.DeleteForward)
|
||||
})
|
||||
|
||||
r.Route("/ssh", func(r chi.Router) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
|
||||
@@ -2,11 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
@@ -64,7 +66,38 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, rules)
|
||||
var filledRules []map[string]any
|
||||
for rule := range slices.Values(rules) {
|
||||
// 去除IP规则
|
||||
if rule.PortStart == 1 && rule.PortEnd == 65535 {
|
||||
continue
|
||||
}
|
||||
isUse := false
|
||||
for port := rule.PortStart; port <= rule.PortEnd; port++ {
|
||||
if rule.Protocol == firewall.ProtocolTCP {
|
||||
isUse = os.TCPPortInUse(port)
|
||||
} else if rule.Protocol == firewall.ProtocolUDP {
|
||||
isUse = os.UDPPortInUse(port)
|
||||
} else {
|
||||
isUse = os.TCPPortInUse(port) || os.UDPPortInUse(port)
|
||||
}
|
||||
if isUse {
|
||||
break
|
||||
}
|
||||
}
|
||||
filledRules = append(filledRules, map[string]any{
|
||||
"family": rule.Family,
|
||||
"port_start": rule.PortStart,
|
||||
"port_end": rule.PortEnd,
|
||||
"protocol": rule.Protocol,
|
||||
"address": rule.Address,
|
||||
"strategy": rule.Strategy,
|
||||
"direction": rule.Direction,
|
||||
"in_use": isUse,
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, filledRules)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
@@ -105,3 +138,116 @@ func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) GetIPRules(w http.ResponseWriter, r *http.Request) {
|
||||
rules, err := s.firewall.ListRule()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var filledRules []map[string]any
|
||||
for rule := range slices.Values(rules) {
|
||||
// 保留IP规则
|
||||
if rule.PortStart != 1 || rule.PortEnd != 65535 || rule.Address == "" {
|
||||
continue
|
||||
}
|
||||
filledRules = append(filledRules, map[string]any{
|
||||
"family": rule.Family,
|
||||
"protocol": rule.Protocol,
|
||||
"address": rule.Address,
|
||||
"strategy": rule.Strategy,
|
||||
"direction": rule.Direction,
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, filledRules)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallService) CreateIPRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallIPRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.RichRules(firewall.FireInfo{
|
||||
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationAdd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) DeleteIPRule(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallIPRule](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.RichRules(firewall.FireInfo{
|
||||
Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction),
|
||||
}, firewall.OperationRemove); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) GetForwards(w http.ResponseWriter, r *http.Request) {
|
||||
forwards, err := s.firewall.ListForward()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, forwards)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FirewallService) CreateForward(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallForward](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Forward(firewall.Forward{
|
||||
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
|
||||
}, firewall.OperationAdd); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FirewallService) DeleteForward(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FirewallForward](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.firewall.Forward(firewall.Forward{
|
||||
Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort,
|
||||
}, firewall.OperationRemove); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -41,16 +41,15 @@ type FireInfo struct {
|
||||
}
|
||||
|
||||
type FireForwardInfo struct {
|
||||
Address string `json:"address"` // 源地址
|
||||
Port uint `json:"port"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
TargetIP string `json:"targetIP"` // 目标地址
|
||||
TargetPort string `json:"targetPort"` // 1-65535
|
||||
Port uint `json:"port"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
TargetIP string `json:"target_ip"` // 目标地址
|
||||
TargetPort uint `json:"target_port"` // 1-65535
|
||||
}
|
||||
|
||||
type Forward struct {
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
Port uint `json:"port"` // 1-65535
|
||||
TargetIP string `json:"targetIP"` // 目标地址
|
||||
TargetPort uint `json:"targetPort"` // 1-65535
|
||||
Protocol Protocol `json:"protocol"` // tcp udp tcp/udp
|
||||
Port uint `json:"port"` // 1-65535
|
||||
TargetIP string `json:"target_ip"` // 目标地址
|
||||
TargetPort uint `json:"target_port"` // 1-65535
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package firewall
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -22,7 +23,7 @@ type Firewall struct {
|
||||
func NewFirewall() *Firewall {
|
||||
firewall := &Firewall{
|
||||
forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`),
|
||||
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol="([^"]+)")?.*?(accept|drop|reject)$`),
|
||||
richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol(?: value)?="([^"]+)")?.*?(accept|drop|reject)$`),
|
||||
}
|
||||
|
||||
return firewall
|
||||
@@ -107,7 +108,7 @@ func (r *Firewall) ListForward() ([]FireForwardInfo, error) {
|
||||
Port: cast.ToUint(match[1]),
|
||||
Protocol: Protocol(match[2]),
|
||||
TargetIP: match[4],
|
||||
TargetPort: match[3],
|
||||
TargetPort: cast.ToUint(match[3]),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -182,10 +183,18 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd))
|
||||
}
|
||||
if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
|
||||
ruleBuilder.WriteString(`protocol`)
|
||||
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
|
||||
ruleBuilder.WriteString(` value`)
|
||||
}
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
|
||||
}
|
||||
if operation == OperationAdd && protocol != "" {
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol))
|
||||
ruleBuilder.WriteString(`protocol`)
|
||||
if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value
|
||||
ruleBuilder.WriteString(` value`)
|
||||
}
|
||||
ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol))
|
||||
}
|
||||
|
||||
ruleBuilder.WriteString(string(rule.Strategy))
|
||||
@@ -199,26 +208,29 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Firewall) PortForward(info Forward, operation Operation) error {
|
||||
func (r *Firewall) Forward(rule Forward, operation Operation) error {
|
||||
if err := r.enableForward(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ruleStr strings.Builder
|
||||
ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol))
|
||||
if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
|
||||
ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort))
|
||||
} else {
|
||||
ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort))
|
||||
}
|
||||
ruleStr.WriteString(" --permanent")
|
||||
protocols := strings.Split(string(rule.Protocol), "/")
|
||||
for protocol := range slices.Values(protocols) {
|
||||
var ruleBuilder strings.Builder
|
||||
ruleBuilder.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, rule.Port, protocol))
|
||||
if rule.TargetIP != "" && !r.isLocalAddress(rule.TargetIP) {
|
||||
ruleBuilder.WriteString(fmt.Sprintf("toport=%d:toaddr=%s", rule.TargetPort, rule.TargetIP))
|
||||
} else {
|
||||
ruleBuilder.WriteString(fmt.Sprintf("toport=%d", rule.TargetPort))
|
||||
}
|
||||
ruleBuilder.WriteString(" --permanent")
|
||||
|
||||
_, err := shell.Execf(ruleStr.String()) // nolint: govet
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
|
||||
_, err := shell.Execf(ruleBuilder.String()) // nolint: govet
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s port forward failed, err: %v", operation, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = shell.Execf("firewall-cmd --reload")
|
||||
_, err := shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -270,15 +282,31 @@ func (r *Firewall) enableForward() error {
|
||||
if out == "no" {
|
||||
out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", err, out)
|
||||
return fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
|
||||
_, err = shell.Execf("firewall-cmd --reload")
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Firewall) isLocalAddress(ip string) bool {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return false
|
||||
}
|
||||
if parsed.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
if parsed.IsUnspecified() {
|
||||
return true
|
||||
}
|
||||
if strings.ToLower(ip) == "localhost" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
22
pkg/os/os.go
22
pkg/os/os.go
@@ -2,6 +2,8 @@ package os
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -58,3 +60,23 @@ func IsUbuntu() bool {
|
||||
id, idLike := osRelease["ID"], osRelease["ID_LIKE"]
|
||||
return id == "ubuntu" || strings.Contains(idLike, "ubuntu")
|
||||
}
|
||||
|
||||
func TCPPortInUse(port uint) bool {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
conn, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
defer conn.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
func UDPPortInUse(port uint) bool {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
conn, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
defer conn.Close()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func Execf(shell string, args ...any) (string, error) {
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", errors.New(strings.TrimSpace(stderr.String()))
|
||||
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), err
|
||||
@@ -71,10 +71,10 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string,
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
_ = cmd.Process.Kill()
|
||||
return "", errors.New("执行超时")
|
||||
return strings.TrimSpace(stdout.String()), errors.New("执行超时")
|
||||
case err = <-done:
|
||||
if err != nil {
|
||||
return "", errors.New(strings.TrimSpace(stderr.String()))
|
||||
return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
web/src/api/panel/firewall/index.ts
Normal file
36
web/src/api/panel/firewall/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取防火墙状态
|
||||
status: (): Promise<AxiosResponse<any>> => request.get('/firewall/status'),
|
||||
// 设置防火墙状态
|
||||
updateStatus: (status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/status', { status }),
|
||||
// 获取防火墙规则
|
||||
rules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/rule', { params: { page, limit } }),
|
||||
// 创建防火墙规则
|
||||
createRule: (rule: any): Promise<AxiosResponse<any>> => request.post('/firewall/rule', rule),
|
||||
// 删除防火墙规则
|
||||
deleteRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/rule', { data: rule }),
|
||||
// 获取防火墙IP规则
|
||||
ipRules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/ipRule', { params: { page, limit } }),
|
||||
// 创建防火墙IP规则
|
||||
createIpRule: (rule: any): Promise<AxiosResponse<any>> => request.post('/firewall/ipRule', rule),
|
||||
// 删除防火墙IP规则
|
||||
deleteIpRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/ipRule', { data: rule }),
|
||||
// 获取防火墙转发规则
|
||||
forwards: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/forward', { params: { page, limit } }),
|
||||
// 创建防火墙转发规则
|
||||
createForward: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/forward', rule),
|
||||
// 删除防火墙转发规则
|
||||
deleteForward: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/forward', { data: rule })
|
||||
}
|
||||
@@ -3,20 +3,6 @@ import type { AxiosResponse } from 'axios'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取防火墙状态
|
||||
firewallStatus: (): Promise<AxiosResponse<any>> => request.get('/firewall/status'),
|
||||
// 设置防火墙状态
|
||||
setFirewallStatus: (status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/status', { status }),
|
||||
// 获取防火墙规则
|
||||
firewallRules: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/firewall/rule', { params: { page, limit } }),
|
||||
// 创建防火墙规则
|
||||
createFirewallRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.post('/firewall/rule', rule),
|
||||
// 删除防火墙规则
|
||||
deleteFirewallRule: (rule: any): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/firewall/rule', { data: rule }),
|
||||
// 获取SSH
|
||||
ssh: (): Promise<AxiosResponse<any>> => request.get('/safe/ssh'),
|
||||
// 设置SSH
|
||||
|
||||
95
web/src/views/safe/CreateForwardModal.vue
Normal file
95
web/src/views/safe/CreateForwardModal.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const loading = ref(false)
|
||||
|
||||
const protocols = [
|
||||
{
|
||||
label: 'TCP',
|
||||
value: 'tcp'
|
||||
},
|
||||
{
|
||||
label: 'UDP',
|
||||
value: 'udp'
|
||||
},
|
||||
{
|
||||
label: 'TCP/UDP',
|
||||
value: 'tcp/udp'
|
||||
}
|
||||
]
|
||||
|
||||
const createModel = ref({
|
||||
protocol: 'tcp',
|
||||
port: 8080,
|
||||
target_ip: '127.0.0.1',
|
||||
target_port: 80
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
await firewall.createForward(createModel.value).then(() => {
|
||||
window.$message.success(`创建成功`)
|
||||
})
|
||||
createModel.value = {
|
||||
protocol: 'tcp',
|
||||
port: 8080,
|
||||
target_ip: '127.0.0.1',
|
||||
target_port: 80
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
title="创建转发"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item path="protocols" label="传输协议">
|
||||
<n-select v-model:value="createModel.protocol" :options="protocols" />
|
||||
</n-form-item>
|
||||
<n-form-item path="address" label="目标 IP">
|
||||
<n-input v-model:value="createModel.target_ip" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="12">
|
||||
<n-form-item path="address" label="源端口">
|
||||
<n-input-number
|
||||
v-model:value="createModel.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
placeholder="8080"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
<n-col :span="12">
|
||||
<n-form-item path="address" label="目标端口">
|
||||
<n-input-number
|
||||
v-model:value="createModel.target_port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
placeholder="80"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" :loading="loading" :disabled="loading" block @click="handleCreate">
|
||||
提交
|
||||
</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
132
web/src/views/safe/CreateIpModal.vue
Normal file
132
web/src/views/safe/CreateIpModal.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const loading = ref(false)
|
||||
|
||||
const protocols = [
|
||||
{
|
||||
label: 'TCP',
|
||||
value: 'tcp'
|
||||
},
|
||||
{
|
||||
label: 'UDP',
|
||||
value: 'udp'
|
||||
},
|
||||
{
|
||||
label: 'TCP/UDP',
|
||||
value: 'tcp/udp'
|
||||
}
|
||||
]
|
||||
|
||||
const families = [
|
||||
{
|
||||
label: 'IPv4',
|
||||
value: 'ipv4'
|
||||
},
|
||||
{
|
||||
label: 'IPv6',
|
||||
value: 'ipv6'
|
||||
}
|
||||
]
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
label: '接受',
|
||||
value: 'accept'
|
||||
},
|
||||
{
|
||||
label: '丢弃',
|
||||
value: 'drop'
|
||||
},
|
||||
{
|
||||
label: '拒绝',
|
||||
value: 'reject'
|
||||
}
|
||||
]
|
||||
|
||||
const directions = [
|
||||
{
|
||||
label: '传入',
|
||||
value: 'in'
|
||||
},
|
||||
{
|
||||
label: '传出',
|
||||
value: 'out'
|
||||
}
|
||||
]
|
||||
|
||||
const createModel = ref({
|
||||
family: 'ipv4',
|
||||
protocol: 'tcp',
|
||||
address: [],
|
||||
strategy: 'accept',
|
||||
direction: 'in'
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
for (const address of createModel.value.address) {
|
||||
await firewall
|
||||
.createIpRule({
|
||||
...createModel.value,
|
||||
address
|
||||
})
|
||||
.then(() => {
|
||||
window.$message.success(`${address} 创建成功`)
|
||||
})
|
||||
}
|
||||
createModel.value = {
|
||||
family: 'ipv4',
|
||||
protocol: 'tcp',
|
||||
address: [],
|
||||
strategy: 'accept',
|
||||
direction: 'in'
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
title="创建规则"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item path="protocols" label="传输协议">
|
||||
<n-select v-model:value="createModel.protocol" :options="protocols" />
|
||||
</n-form-item>
|
||||
<n-form-item path="family" label="网络协议">
|
||||
<n-select v-model:value="createModel.family" :options="families" />
|
||||
</n-form-item>
|
||||
<n-form-item path="address" label="IP 地址">
|
||||
<n-dynamic-input
|
||||
v-model:value="createModel.address"
|
||||
show-sort-button
|
||||
placeholder="可选输入 IP 或 IP 段:127.0.0.1 或 172.16.0.0/24(多个以英文逗号隔开)"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="strategy" label="策略">
|
||||
<n-select v-model:value="createModel.strategy" :options="strategies" />
|
||||
</n-form-item>
|
||||
<n-form-item path="strategy" label="方向">
|
||||
<n-select v-model:value="createModel.direction" :options="directions" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" :loading="loading" :disabled="loading" block @click="handleCreate">
|
||||
提交
|
||||
</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import safe from '@/api/panel/safe'
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
@@ -68,7 +68,7 @@ const createModel = ref({
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
await safe.createFirewallRule(createModel.value).then(() => {
|
||||
await firewall.createRule(createModel.value).then(() => {
|
||||
window.$message.success('创建成功')
|
||||
createModel.value = {
|
||||
family: 'ipv4',
|
||||
|
||||
230
web/src/views/safe/ForwardView.vue
Normal file
230
web/src/views/safe/ForwardView.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NPopconfirm, NTag } from 'naive-ui'
|
||||
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { renderIcon } from '@/utils'
|
||||
import CreateForwardModal from '@/views/safe/CreateForwardModal.vue'
|
||||
import type { FirewallRule } from '@/views/safe/types'
|
||||
|
||||
const createModalShow = ref(false)
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '传输协议',
|
||||
key: 'protocol',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.protocol !== '') {
|
||||
return row.protocol
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
return row.port
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标 IP',
|
||||
key: 'target_ip',
|
||||
minWidth: 200,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return row.target_ip
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标端口',
|
||||
key: 'target_port',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return row.target_port
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定要删除吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<FirewallRule[]>([] as FirewallRule[])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await firewall.deleteForward(row).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
})
|
||||
fetchFirewallForwards(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFirewallForwards = async (page: number, limit: number) => {
|
||||
const { data } = await firewall.forwards(page, limit)
|
||||
return data
|
||||
}
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
window.$message.info('请选择要删除的规则')
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of selectedRowKeys.value) {
|
||||
// 解析json
|
||||
const rule = JSON.parse(key)
|
||||
await firewall.deleteForward(rule).then(() => {
|
||||
window.$message.success(`${rule.protocol} ${rule.target_ip}:${rule.target_port} 删除成功`)
|
||||
})
|
||||
}
|
||||
|
||||
fetchFirewallForwards(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onChecked = (rowKeys: any) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
}
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
fetchFirewallForwards(page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
onPageChange(1)
|
||||
}
|
||||
|
||||
watch(createModalShow, () => {
|
||||
onPageChange(1)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onPageChange(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-card flex-1 rounded-10>
|
||||
<n-flex items-center>
|
||||
<n-button type="primary" @click="createModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建转发
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="batchDelete">
|
||||
<template #trigger>
|
||||
<n-button type="warning">
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
批量删除
|
||||
</n-button>
|
||||
</template>
|
||||
确定要批量删除吗?
|
||||
</n-popconfirm>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => JSON.stringify(row)"
|
||||
:pagination="pagination"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</n-flex>
|
||||
<create-forward-modal v-model:show="createModalShow" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,332 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import ForwardView from '@/views/safe/ForwardView.vue'
|
||||
import IpRuleView from '@/views/safe/IpRuleView.vue'
|
||||
import RuleView from '@/views/safe/RuleView.vue'
|
||||
import SettingView from '@/views/safe/SettingView.vue'
|
||||
|
||||
import safe from '@/api/panel/safe'
|
||||
import { renderIcon } from '@/utils'
|
||||
import CreateModal from '@/views/safe/CreateModal.vue'
|
||||
import type { FirewallRule } from '@/views/safe/types'
|
||||
|
||||
const createModalShow = ref(false)
|
||||
|
||||
const model = ref({
|
||||
firewallStatus: false,
|
||||
sshStatus: false,
|
||||
pingStatus: false,
|
||||
sshPort: 22
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '传输协议',
|
||||
key: 'protocol',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.protocol !== '') {
|
||||
return row.protocol
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '网络协议',
|
||||
key: 'family',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.family !== '') {
|
||||
return row.family
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
minWidth: 300,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
if (row.port_start == row.port_end) {
|
||||
return row.port_start
|
||||
}
|
||||
return `${row.port_start}-${row.port_end}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '策略',
|
||||
key: 'strategy',
|
||||
minWidth: 100,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type:
|
||||
row.strategy === 'accept' ? 'success' : row.strategy === 'drop' ? 'warning' : 'error'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
switch (row.strategy) {
|
||||
case 'accept':
|
||||
return '接受'
|
||||
case 'drop':
|
||||
return '丢弃'
|
||||
case 'reject':
|
||||
return '拒绝'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
minWidth: 100,
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
switch (row.direction) {
|
||||
case 'in':
|
||||
return '传入'
|
||||
case 'out':
|
||||
return '传出'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
key: 'address',
|
||||
minWidth: 100,
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.address === '') {
|
||||
return '所有'
|
||||
}
|
||||
return row.address
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定要删除吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<FirewallRule[]>([] as FirewallRule[])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await safe.deleteFirewallRule(row).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
})
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFirewallRules = async (page: number, limit: number) => {
|
||||
const { data } = await safe.firewallRules(page, limit)
|
||||
return data
|
||||
}
|
||||
|
||||
const fetchSetting = async () => {
|
||||
safe.firewallStatus().then((res) => {
|
||||
model.value.firewallStatus = res.data
|
||||
})
|
||||
safe.ssh().then((res) => {
|
||||
model.value.sshStatus = res.data.status
|
||||
model.value.sshPort = res.data.port
|
||||
})
|
||||
safe.pingStatus().then((res) => {
|
||||
model.value.pingStatus = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleFirewallStatus = () => {
|
||||
safe.setFirewallStatus(model.value.firewallStatus).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSsh = () => {
|
||||
safe.setSsh(model.value.sshStatus, model.value.sshPort).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handlePingStatus = () => {
|
||||
safe.setPingStatus(model.value.pingStatus).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
window.$message.info('请选择要删除的规则')
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of selectedRowKeys.value) {
|
||||
// 解析json
|
||||
const rule = JSON.parse(key)
|
||||
await safe.deleteFirewallRule(rule).then(() => {
|
||||
let port =
|
||||
rule.port_start == rule.port_end ? rule.port_start : `${rule.port_start}-${rule.port_end}`
|
||||
window.$message.success(`${rule.family} 规则 ${port}/${rule.protocol} 删除成功`)
|
||||
})
|
||||
}
|
||||
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onChecked = (rowKeys: any) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
}
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
fetchFirewallRules(page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
onPageChange(1)
|
||||
}
|
||||
|
||||
watch(createModalShow, () => {
|
||||
onPageChange(1)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchSetting()
|
||||
onPageChange(1)
|
||||
})
|
||||
const currentTab = ref('rule')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-space vertical>
|
||||
<n-card flex-1 rounded-10>
|
||||
<n-form inline>
|
||||
<n-form-item label="防火墙">
|
||||
<n-switch v-model:value="model.firewallStatus" @update:value="handleFirewallStatus" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SSH">
|
||||
<n-switch v-model:value="model.sshStatus" @update:value="handleSsh" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Ping">
|
||||
<n-switch v-model:value="model.pingStatus" @update:value="handlePingStatus" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SSH端口">
|
||||
<n-input-number v-model:value="model.sshPort" @blur="handleSsh" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
<n-space flex items-center>
|
||||
<n-button type="primary" @click="createModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建规则
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="batchDelete">
|
||||
<template #trigger>
|
||||
<n-button type="warning">
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
批量删除
|
||||
</n-button>
|
||||
</template>
|
||||
确定要批量删除吗?
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => JSON.stringify(row)"
|
||||
:pagination="pagination"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</n-space>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated size="large">
|
||||
<n-tab-pane name="rule" tab="端口规则">
|
||||
<rule-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="ip-rule" tab="IP 规则">
|
||||
<ip-rule-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="forward" tab="端口转发">
|
||||
<forward-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="setting" tab="设置">
|
||||
<setting-view />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<create-modal v-model:show="createModalShow" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
264
web/src/views/safe/IpRuleView.vue
Normal file
264
web/src/views/safe/IpRuleView.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NPopconfirm, NTag } from 'naive-ui'
|
||||
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { renderIcon } from '@/utils'
|
||||
import CreateIpModal from '@/views/safe/CreateIpModal.vue'
|
||||
import type { FirewallRule } from '@/views/safe/types'
|
||||
|
||||
const createModalShow = ref(false)
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '传输协议',
|
||||
key: 'protocol',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.protocol !== '') {
|
||||
return row.protocol
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '网络协议',
|
||||
key: 'family',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.family !== '') {
|
||||
return row.family
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '策略',
|
||||
key: 'strategy',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type:
|
||||
row.strategy === 'accept' ? 'success' : row.strategy === 'drop' ? 'warning' : 'error'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
switch (row.strategy) {
|
||||
case 'accept':
|
||||
return '接受'
|
||||
case 'drop':
|
||||
return '丢弃'
|
||||
case 'reject':
|
||||
return '拒绝'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.direction === 'in' ? 'info' : 'default'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
switch (row.direction) {
|
||||
case 'in':
|
||||
return '传入'
|
||||
case 'out':
|
||||
return '传出'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
key: 'address',
|
||||
minWidth: 200,
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
return row.address
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定要删除吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<FirewallRule[]>([] as FirewallRule[])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await firewall.deleteIpRule(row).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
})
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFirewallRules = async (page: number, limit: number) => {
|
||||
const { data } = await firewall.ipRules(page, limit)
|
||||
return data
|
||||
}
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
window.$message.info('请选择要删除的规则')
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of selectedRowKeys.value) {
|
||||
// 解析json
|
||||
const rule = JSON.parse(key)
|
||||
await firewall.deleteIpRule(rule).then(() => {
|
||||
window.$message.success(`${rule.address} 删除成功`)
|
||||
})
|
||||
}
|
||||
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onChecked = (rowKeys: any) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
}
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
fetchFirewallRules(page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
onPageChange(1)
|
||||
}
|
||||
|
||||
watch(createModalShow, () => {
|
||||
onPageChange(1)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onPageChange(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-card flex-1 rounded-10>
|
||||
<n-flex items-center>
|
||||
<n-button type="primary" @click="createModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建规则
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="batchDelete">
|
||||
<template #trigger>
|
||||
<n-button type="warning">
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
批量删除
|
||||
</n-button>
|
||||
</template>
|
||||
确定要批量删除吗?
|
||||
</n-popconfirm>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => JSON.stringify(row)"
|
||||
:pagination="pagination"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</n-flex>
|
||||
<create-ip-modal v-model:show="createModalShow" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
303
web/src/views/safe/RuleView.vue
Normal file
303
web/src/views/safe/RuleView.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NPopconfirm, NTag } from 'naive-ui'
|
||||
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import { renderIcon } from '@/utils'
|
||||
import CreateModal from '@/views/safe/CreateModal.vue'
|
||||
import type { FirewallRule } from '@/views/safe/types'
|
||||
|
||||
const createModalShow = ref(false)
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '传输协议',
|
||||
key: 'protocol',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.protocol !== '') {
|
||||
return row.protocol
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '网络协议',
|
||||
key: 'family',
|
||||
width: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.family !== '') {
|
||||
return row.family
|
||||
}
|
||||
return '无'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): any {
|
||||
if (row.port_start == row.port_end) {
|
||||
return row.port_start
|
||||
}
|
||||
return `${row.port_start}-${row.port_end}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'in_use',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.in_use ? 'success' : 'default'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
if (row.in_use) {
|
||||
return '使用中'
|
||||
}
|
||||
return '未使用'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '策略',
|
||||
key: 'strategy',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type:
|
||||
row.strategy === 'accept' ? 'success' : row.strategy === 'drop' ? 'warning' : 'error'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
switch (row.strategy) {
|
||||
case 'accept':
|
||||
return '接受'
|
||||
case 'drop':
|
||||
return '丢弃'
|
||||
case 'reject':
|
||||
return '拒绝'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
key: 'direction',
|
||||
width: 150,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.direction === 'in' ? 'info' : 'default'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
switch (row.direction) {
|
||||
case 'in':
|
||||
return '传入'
|
||||
case 'out':
|
||||
return '传出'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
key: 'address',
|
||||
minWidth: 200,
|
||||
render(row: any): any {
|
||||
return h(NTag, null, {
|
||||
default: () => {
|
||||
if (row.address === '') {
|
||||
return '所有'
|
||||
}
|
||||
return row.address
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定要删除吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<FirewallRule[]>([] as FirewallRule[])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await firewall.deleteRule(row).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
})
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFirewallRules = async (page: number, limit: number) => {
|
||||
const { data } = await firewall.rules(page, limit)
|
||||
return data
|
||||
}
|
||||
|
||||
const batchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
window.$message.info('请选择要删除的规则')
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of selectedRowKeys.value) {
|
||||
// 解析json
|
||||
const rule = JSON.parse(key)
|
||||
await firewall.deleteRule(rule).then(() => {
|
||||
let port =
|
||||
rule.port_start == rule.port_end ? rule.port_start : `${rule.port_start}-${rule.port_end}`
|
||||
window.$message.success(`${rule.family} 规则 ${port}/${rule.protocol} 删除成功`)
|
||||
})
|
||||
}
|
||||
|
||||
fetchFirewallRules(pagination.page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onChecked = (rowKeys: any) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
}
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
fetchFirewallRules(page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
onPageChange(1)
|
||||
}
|
||||
|
||||
watch(createModalShow, () => {
|
||||
onPageChange(1)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onPageChange(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-card flex-1 rounded-10>
|
||||
<n-flex items-center>
|
||||
<n-button type="primary" @click="createModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建规则
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="batchDelete">
|
||||
<template #trigger>
|
||||
<n-button type="warning">
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
批量删除
|
||||
</n-button>
|
||||
</template>
|
||||
确定要批量删除吗?
|
||||
</n-popconfirm>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1200"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => JSON.stringify(row)"
|
||||
:pagination="pagination"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</n-flex>
|
||||
<create-modal v-model:show="createModalShow" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
67
web/src/views/safe/SettingView.vue
Normal file
67
web/src/views/safe/SettingView.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import firewall from '@/api/panel/firewall'
|
||||
import safe from '@/api/panel/safe'
|
||||
|
||||
const model = ref({
|
||||
firewallStatus: false,
|
||||
sshStatus: false,
|
||||
pingStatus: false,
|
||||
sshPort: 22
|
||||
})
|
||||
|
||||
const fetchSetting = async () => {
|
||||
firewall.status().then((res) => {
|
||||
model.value.firewallStatus = res.data
|
||||
})
|
||||
safe.ssh().then((res) => {
|
||||
model.value.sshStatus = res.data.status
|
||||
model.value.sshPort = res.data.port
|
||||
})
|
||||
safe.pingStatus().then((res) => {
|
||||
model.value.pingStatus = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleFirewallStatus = () => {
|
||||
firewall.updateStatus(model.value.firewallStatus).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSsh = () => {
|
||||
safe.setSsh(model.value.sshStatus, model.value.sshPort).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handlePingStatus = () => {
|
||||
safe.setPingStatus(model.value.pingStatus).then(() => {
|
||||
window.$message.success('设置成功')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSetting()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card flex-1 rounded-10>
|
||||
<n-form :model="model" label-placement="left" label-width="auto">
|
||||
<n-form-item path="firewall" label="系统防火墙">
|
||||
<n-switch v-model:value="model.firewallStatus" @update:value="handleFirewallStatus" />
|
||||
</n-form-item>
|
||||
<n-form-item path="ssh" label="SSH 开关">
|
||||
<n-switch v-model:value="model.sshStatus" @update:value="handleSsh" />
|
||||
</n-form-item>
|
||||
<n-form-item path="ping" label="允许 Ping">
|
||||
<n-switch v-model:value="model.pingStatus" @update:value="handlePingStatus" />
|
||||
</n-form-item>
|
||||
<n-form-item path="sshPort" label="SSH 端口">
|
||||
<n-input-number v-model:value="model.sshPort" @blur="handleSsh" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user