2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

refactor: 重构防火墙

This commit is contained in:
耗子
2024-10-18 02:08:55 +08:00
parent 905e206f9c
commit e9cbc8e945
19 changed files with 1399 additions and 373 deletions

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()))
}
}

View 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 })
}

View File

@@ -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

View 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>

View 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>

View File

@@ -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',

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>