1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 端口转发功能 (#5439)

This commit is contained in:
endymx 2024-06-15 22:28:03 +08:00 committed by GitHub
parent b36e2c5676
commit af0ecce25b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 814 additions and 48 deletions

View File

@ -94,6 +94,29 @@ func (b *BaseApi) OperatePortRule(c *gin.Context) {
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
// OperateForwardRule
// @Tags Firewall
// @Summary Create group
// @Description 更新防火墙端口转发规则
// @Accept json
// @Param request body dto.ForwardRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /hosts/firewall/forward [post]
// @x-panel-log {"bodyKeys":["source_port"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则 [source_port]","formatEN":"update port forward rules [source_port]"}
func (b *BaseApi) OperateForwardRule(c *gin.Context) {
var req dto.ForwardRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperateForwardRule(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Firewall // @Tags Firewall
// @Summary Create group // @Summary Create group
// @Description 创建防火墙 IP 规则 // @Description 创建防火墙 IP 规则

View File

@ -29,6 +29,17 @@ type PortRuleOperate struct {
Description string `json:"description"` Description string `json:"description"`
} }
type ForwardRuleOperate struct {
Rules []struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
} `json:"rules"`
}
type UpdateFirewallDescription struct { type UpdateFirewallDescription struct {
Type string `json:"type"` Type string `json:"type"`
Address string `json:"address"` Address string `json:"address"`

View File

@ -10,3 +10,12 @@ type Firewall struct {
Strategy string `gorm:"type:varchar(64);not null" json:"strategy"` Strategy string `gorm:"type:varchar(64);not null" json:"strategy"`
Description string `gorm:"type:varchar(64);not null" json:"description"` Description string `gorm:"type:varchar(64);not null" json:"description"`
} }
type Forward struct {
BaseModel
Protocol string `gorm:"type:varchar(64);not null" json:"protocol"`
Port string `gorm:"type:varchar(64);not null" json:"port"`
TargetIP string `gorm:"type:varchar(64);not null" json:"targetIP"`
TargetPort string `gorm:"type:varchar(64);not null" json:"targetPort"`
}

View File

@ -3,6 +3,7 @@ package service
import ( import (
"fmt" "fmt"
"os" "os"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -28,6 +29,7 @@ type IFirewallService interface {
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error) SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
OperateFirewall(operation string) error OperateFirewall(operation string) error
OperatePortRule(req dto.PortRuleOperate, reload bool) error OperatePortRule(req dto.PortRuleOperate, reload bool) error
OperateForwardRule(req dto.ForwardRuleOperate) error
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
UpdatePortRule(req dto.PortRuleUpdate) error UpdatePortRule(req dto.PortRuleUpdate) error
UpdateAddrRule(req dto.AddrRuleUpdate) error UpdateAddrRule(req dto.AddrRuleUpdate) error
@ -78,42 +80,36 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
if req.Type == "port" {
ports, err := client.ListPort() var rules []fireClient.FireInfo
if err != nil { switch req.Type {
return 0, nil, err case "port":
} rules, err = client.ListPort()
if len(req.Info) != 0 { case "forward":
for _, port := range ports { rules, err = client.ListForward()
if strings.Contains(port.Port, req.Info) { case "address":
datas = append(datas, port) rules, err = client.ListAddress()
} }
} if err != nil {
} else { return 0, nil, err
datas = ports
}
} else {
addrs, err := client.ListAddress()
if err != nil {
return 0, nil, err
}
if len(req.Info) != 0 {
for _, addr := range addrs {
if strings.Contains(addr.Address, req.Info) {
datas = append(datas, addr)
}
}
} else {
datas = addrs
}
} }
if len(req.Info) != 0 {
for _, addr := range rules {
if strings.Contains(addr.Address, req.Info) {
datas = append(datas, addr)
}
}
} else {
datas = rules
}
if req.Type == "port" { if req.Type == "port" {
apps := u.loadPortByApp() apps := u.loadPortByApp()
for i := 0; i < len(datas); i++ { for i := 0; i < len(datas); i++ {
datas[i].UsedStatus = checkPortUsed(datas[i].Port, datas[i].Protocol, apps) datas[i].UsedStatus = checkPortUsed(datas[i].Port, datas[i].Protocol, apps)
} }
} }
var datasFilterStatus []fireClient.FireInfo var datasFilterStatus []fireClient.FireInfo
if len(req.Status) != 0 { if len(req.Status) != 0 {
for _, data := range datas { for _, data := range datas {
@ -127,6 +123,7 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
} else { } else {
datasFilterStatus = datas datasFilterStatus = datas
} }
var datasFilterStrategy []fireClient.FireInfo var datasFilterStrategy []fireClient.FireInfo
if len(req.Strategy) != 0 { if len(req.Strategy) != 0 {
for _, data := range datasFilterStatus { for _, data := range datasFilterStatus {
@ -300,6 +297,37 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
return nil return nil
} }
func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
client, err := firewall.NewFirewallClient()
if err != nil {
return err
}
sort.SliceStable(req.Rules, func(i, j int) bool {
n1, _ := strconv.Atoi(req.Rules[i].Num)
n2, _ := strconv.Atoi(req.Rules[j].Num)
return n1 > n2
})
for _, r := range req.Rules {
for _, p := range strings.Split(r.Protocol, "/") {
if r.TargetIP == "" {
r.TargetIP = "127.0.0.1"
}
if err = client.PortForward(fireClient.Forward{
Num: r.Num,
Protocol: p,
Port: r.Port,
TargetIP: r.TargetIP,
TargetPort: r.TargetPort,
}, r.Operation); err != nil {
return err
}
}
}
return nil
}
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error { func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
client, err := firewall.NewFirewallClient() client, err := firewall.NewFirewallClient()
if err != nil { if err != nil {

View File

@ -2,6 +2,7 @@ package app
import ( import (
"github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/firewall"
"path" "path"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
@ -31,6 +32,10 @@ func Init() {
} }
_ = docker.CreateDefaultDockerNetwork() _ = docker.CreateDefaultDockerNetwork()
if f, err := firewall.NewFirewallClient(); err == nil {
_ = f.EnableForward()
}
} }
func createDir(fileOp files.FileOp, dirPath string) { func createDir(fileOp files.FileOp, dirPath string) {

View File

@ -89,6 +89,7 @@ func Init() {
migrations.AddFtp, migrations.AddFtp,
migrations.AddProxy, migrations.AddProxy,
migrations.AddCronJobColumn, migrations.AddCronJobColumn,
migrations.AddForward,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -239,6 +239,16 @@ var AddProxy = &gormigrate.Migration{
}, },
} }
var AddForward = &gormigrate.Migration{
ID: "202400611-add-forward",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Forward{}); err != nil {
return err
}
return nil
},
}
var AddCronJobColumn = &gormigrate.Migration{ var AddCronJobColumn = &gormigrate.Migration{
ID: "20240524-add-cronjob-command", ID: "20240524-add-cronjob-command",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {

View File

@ -29,6 +29,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule) hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall) hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
hostRouter.POST("/firewall/port", baseApi.OperatePortRule) hostRouter.POST("/firewall/port", baseApi.OperatePortRule)
hostRouter.POST("/firewall/forward", baseApi.OperateForwardRule)
hostRouter.POST("/firewall/ip", baseApi.OperateIPRule) hostRouter.POST("/firewall/ip", baseApi.OperateIPRule)
hostRouter.POST("/firewall/batch", baseApi.BatchOperateRule) hostRouter.POST("/firewall/batch", baseApi.BatchOperateRule)
hostRouter.POST("/firewall/update/port", baseApi.UpdatePortRule) hostRouter.POST("/firewall/update/port", baseApi.UpdatePortRule)

View File

@ -18,11 +18,14 @@ type FirewallClient interface {
Version() (string, error) Version() (string, error)
ListPort() ([]client.FireInfo, error) ListPort() ([]client.FireInfo, error)
ListForward() ([]client.FireInfo, error)
ListAddress() ([]client.FireInfo, error) ListAddress() ([]client.FireInfo, error)
Port(port client.FireInfo, operation string) error Port(port client.FireInfo, operation string) error
RichRules(rule client.FireInfo, operation string) error RichRules(rule client.FireInfo, operation string) error
PortForward(info client.Forward, operation string) error PortForward(info client.Forward, operation string) error
EnableForward() error
} }
func NewFirewallClient() (FirewallClient, error) { func NewFirewallClient() (FirewallClient, error) {

View File

@ -2,6 +2,7 @@ package client
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
@ -10,6 +11,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
) )
var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`)
type Firewall struct{} type Firewall struct{}
func NewFirewalld() (*Firewall, error) { func NewFirewalld() (*Firewall, error) {
@ -115,6 +118,29 @@ func (f *Firewall) ListPort() ([]FireInfo, error) {
return datas, nil return datas, nil
} }
func (f *Firewall) ListForward() ([]FireInfo, error) {
stdout, err := cmd.Exec("firewall-cmd --zone=public --list-forward-ports")
if err != nil {
return nil, err
}
var datas []FireInfo
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimFunc(line, func(r rune) bool {
return r <= 32
})
if ForwardListRegex.MatchString(line) {
match := ForwardListRegex.FindStringSubmatch(line)
datas = append(datas, FireInfo{
Port: match[1],
Protocol: match[2],
TargetIP: match[4],
TargetPort: match[3],
})
}
}
return datas, nil
}
func (f *Firewall) ListAddress() ([]FireInfo, error) { func (f *Firewall) ListAddress() ([]FireInfo, error) {
stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules")
if err != nil { if err != nil {
@ -175,15 +201,18 @@ func (f *Firewall) RichRules(rule FireInfo, operation string) error {
} }
func (f *Firewall) PortForward(info Forward, operation string) error { func (f *Firewall) PortForward(info Forward, operation string) error {
ruleStr := fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Target) ruleStr := fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetPort)
if len(info.Address) != 0 { if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
ruleStr = fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Address, info.Target) ruleStr = fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetIP, info.TargetPort)
} }
stdout, err := cmd.Exec(ruleStr) stdout, err := cmd.Exec(ruleStr)
if err != nil { if err != nil {
return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) return fmt.Errorf("%s port forward failed, err: %s", operation, stdout)
} }
if err = f.Reload(); err != nil {
return err
}
return nil return nil
} }
@ -208,3 +237,19 @@ func (f *Firewall) loadInfo(line string) FireInfo {
} }
return itemRule return itemRule
} }
func (f *Firewall) EnableForward() error {
stdout, err := cmd.Exec("firewall-cmd --zone=public --query-masquerade")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(stdout), "no") {
stdout, err = cmd.Exec("firewall-cmd --zone=public --add-masquerade --permanent")
if err != nil {
return fmt.Errorf("%s: %s", err, stdout)
}
return f.Reload()
}
return fmt.Errorf("%s: %s", err, stdout)
}
return nil
}

View File

@ -7,13 +7,29 @@ type FireInfo struct {
Protocol string `json:"protocol"` // tcp udp tcp/udp Protocol string `json:"protocol"` // tcp udp tcp/udp
Strategy string `json:"strategy"` // accept drop Strategy string `json:"strategy"` // accept drop
Num string `json:"num"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
UsedStatus string `json:"usedStatus"` UsedStatus string `json:"usedStatus"`
Description string `json:"description"` Description string `json:"description"`
} }
type Forward struct { type Forward struct {
Protocol string `json:"protocol"` Num string `json:"num"`
Address string `json:"address"` Protocol string `json:"protocol"`
Port string `json:"port"` Port string `json:"port"`
Target string `json:"target"` TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
}
type IptablesNatInfo struct {
Num string `json:"num"`
Target string `json:"target"`
Protocol string `json:"protocol"`
Opt string `json:"opt"`
Source string `json:"source"`
Destination string `json:"destination"`
SrcPort string `json:"srcPort"`
DestPort string `json:"destPort"`
} }

View File

@ -0,0 +1,132 @@
package client
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/pkg/errors"
"regexp"
"strings"
)
var NatListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?)$`)
type Iptables struct {
CmdStr string
}
func NewIptables() (*Iptables, error) {
iptables := new(Iptables)
if cmd.HasNoPasswordSudo() {
iptables.CmdStr = "sudo"
}
return iptables, nil
}
func (iptables *Iptables) Check() error {
stdout, err := cmd.Exec("cat /proc/sys/net/ipv4/ip_forward")
if err != nil {
return err
}
if stdout == "0" {
return fmt.Errorf("disable")
}
return nil
}
func (iptables *Iptables) NatList() ([]IptablesNatInfo, error) {
stdout, err := cmd.Execf("%s iptables -t nat -nL PREROUTING --line", iptables.CmdStr)
if err != nil {
return nil, err
}
var forwardList []IptablesNatInfo
for _, line := range strings.Split(stdout, "\n") {
line = strings.TrimFunc(line, func(r rune) bool {
return r <= 32
})
if NatListRegex.MatchString(line) {
match := NatListRegex.FindStringSubmatch(line)
if !strings.Contains(match[9], ":") {
match[9] = fmt.Sprintf(":%s", match[9])
}
forwardList = append(forwardList, IptablesNatInfo{
Num: match[1],
Target: match[2],
Protocol: match[7],
Opt: match[4],
Source: match[5],
Destination: match[6],
SrcPort: match[8],
DestPort: match[9],
})
}
}
return forwardList, nil
}
func (iptables *Iptables) NatAdd(protocol, src, destIp, destPort string, save bool) error {
rule := fmt.Sprintf("%s iptables -t nat -A PREROUTING -p %s --dport %s -j REDIRECT --to-port %s", iptables.CmdStr, protocol, src, destPort)
if destIp != "" && destIp != "127.0.0.1" && destIp != "localhost" {
rule = fmt.Sprintf("%s iptables -t nat -A PREROUTING -p %s --dport %s -j DNAT --to-destination %s:%s", iptables.CmdStr, protocol, src, destIp, destPort)
}
stdout, err := cmd.Exec(rule)
if err != nil {
return err
}
if stdout != "" {
return errors.New(stdout)
}
if save {
return global.DB.Save(&model.Forward{
Protocol: protocol,
Port: src,
TargetIP: destIp,
TargetPort: destPort,
}).Error
}
return nil
}
func (iptables *Iptables) NatRemove(num string, protocol, src, destIp, destPort string) error {
stdout, err := cmd.Execf("%s iptables -t nat -D PREROUTING %s", iptables.CmdStr, num)
if err != nil {
return err
}
if stdout != "" {
return fmt.Errorf(stdout)
}
global.DB.Where(
"protocol = ? AND port = ? AND target_ip = ? AND target_port = ?",
protocol,
src,
destIp,
destPort,
).Delete(&model.Forward{})
return nil
}
func (iptables *Iptables) Reload() error {
stdout, err := cmd.Execf("%s iptables -t nat -F && %s iptables -t nat -X", iptables.CmdStr, iptables.CmdStr)
if err != nil {
return err
}
if stdout != "" {
return fmt.Errorf(stdout)
}
var rules []model.Forward
global.DB.Find(&rules)
for _, forward := range rules {
if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, false); err != nil {
return err
}
}
return nil
}

View File

@ -103,6 +103,30 @@ func (f *Ufw) ListPort() ([]FireInfo, error) {
return datas, nil return datas, nil
} }
func (f *Ufw) ListForward() ([]FireInfo, error) {
iptables, err := NewIptables()
if err != nil {
return nil, err
}
rules, err := iptables.NatList()
if err != nil {
return nil, err
}
var list []FireInfo
for _, rule := range rules {
dest := strings.SplitN(rule.DestPort, ":", 2)
list = append(list, FireInfo{
Num: rule.Num,
Protocol: rule.Protocol,
Port: rule.SrcPort,
TargetIP: dest[0],
TargetPort: dest[1],
})
}
return list, nil
}
func (f *Ufw) ListAddress() ([]FireInfo, error) { func (f *Ufw) ListAddress() ([]FireInfo, error) {
stdout, err := cmd.Execf("%s status verbose", f.CmdStr) stdout, err := cmd.Execf("%s status verbose", f.CmdStr)
if err != nil { if err != nil {
@ -206,17 +230,18 @@ func (f *Ufw) RichRules(rule FireInfo, operation string) error {
} }
func (f *Ufw) PortForward(info Forward, operation string) error { func (f *Ufw) PortForward(info Forward, operation string) error {
ruleStr := fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Target) iptables, err := NewIptables()
if len(info.Address) != 0 { if err != nil {
ruleStr = fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Address, info.Target) return err
} }
stdout, err := cmd.Exec(ruleStr) if operation == "add" {
if err != nil { err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, true)
return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) } else {
err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort)
} }
if err := f.Reload(); err != nil { if err != nil {
return err return fmt.Errorf("%s port forward failed, err: %s", operation, err)
} }
return nil return nil
} }
@ -258,3 +283,12 @@ func (f *Ufw) loadInfo(line string, fireType string) FireInfo {
return itemInfo return itemInfo
} }
func (f *Ufw) EnableForward() error {
iptables, err := NewIptables()
if err != nil {
return err
}
return iptables.Reload()
}

View File

@ -74,12 +74,17 @@ export namespace Host {
export interface RuleInfo extends ReqPage { export interface RuleInfo extends ReqPage {
family: string; family: string;
address: string; address: string;
destination: string;
port: string; port: string;
srcPort: string;
destPort: string;
protocol: string; protocol: string;
strategy: string; strategy: string;
usedStatus: string; usedStatus: string;
description: string; description: string;
[key: string]: any;
} }
export interface UpdateDescription { export interface UpdateDescription {
address: string; address: string;
@ -97,6 +102,13 @@ export namespace Host {
strategy: string; strategy: string;
description: string; description: string;
} }
export interface RuleForward {
operation: string;
protocol: string;
port: string;
targetIP: string;
targetPort: string;
}
export interface RuleIP { export interface RuleIP {
operation: string; operation: string;
address: string; address: string;

View File

@ -98,6 +98,9 @@ export const operateFire = (operation: string) => {
export const operatePortRule = (params: Host.RulePort) => { export const operatePortRule = (params: Host.RulePort) => {
return http.post<Host.RulePort>(`/hosts/firewall/port`, params, TimeoutEnum.T_40S); return http.post<Host.RulePort>(`/hosts/firewall/port`, params, TimeoutEnum.T_40S);
}; };
export const operateForwardRule = (params: { rules: Host.RuleForward[] }) => {
return http.post<Host.RulePort>(`/hosts/firewall/forward`, params, TimeoutEnum.T_40S);
};
export const operateIPRule = (params: Host.RuleIP) => { export const operateIPRule = (params: Host.RuleIP) => {
return http.post<Host.RuleIP>(`/hosts/firewall/ip`, params, TimeoutEnum.T_40S); return http.post<Host.RuleIP>(`/hosts/firewall/ip`, params, TimeoutEnum.T_40S);
}; };

View File

@ -1,4 +1,5 @@
import fit2cloudEnLocale from 'fit2cloud-ui-plus/src/locale/lang/en'; import fit2cloudEnLocale from 'fit2cloud-ui-plus/src/locale/lang/en';
let xpackEnLocale = {}; let xpackEnLocale = {};
const xpackModules = import.meta.glob('../../xpack/lang/en.ts', { eager: true }); const xpackModules = import.meta.glob('../../xpack/lang/en.ts', { eager: true });
if (xpackModules['../../xpack/lang/en.ts']) { if (xpackModules['../../xpack/lang/en.ts']) {
@ -2204,8 +2205,14 @@ const message = {
addressHelper2: 'For multiple IPs or IP ranges, separate with commas: 172.16.10.11, 172.16.0.0/24', addressHelper2: 'For multiple IPs or IP ranges, separate with commas: 172.16.10.11, 172.16.0.0/24',
allIP: 'All IP', allIP: 'All IP',
portRule: 'Port rule', portRule: 'Port rule',
forwardRule: 'Forwarding',
ipRule: 'IP rule', ipRule: 'IP rule',
userAgent: 'User-Agent filter', userAgent: 'User-Agent filter',
sourcePort: 'Source Port',
targetIP: 'Destination IP',
targetPort: 'Destination Port',
forwardHelper1: 'In the case of local port forwarding, the destination IP is: 127.0.0.1',
forwardHelper2: 'If the destination IP is not filled in, it will be forwarded to the local port by default',
}, },
runtime: { runtime: {
runtime: 'Runtime', runtime: 'Runtime',

View File

@ -2048,8 +2048,14 @@ const message = {
addressHelper2: '多個 IP IP 請用 "," 隔開172.16.10.11,172.16.0.0/24', addressHelper2: '多個 IP IP 請用 "," 隔開172.16.10.11,172.16.0.0/24',
allIP: '所有 IP', allIP: '所有 IP',
portRule: '端口規則', portRule: '端口規則',
forwardRule: '端口轉發',
ipRule: 'IP 規則', ipRule: 'IP 規則',
userAgent: 'User-Agent 過濾', userAgent: 'User-Agent 過濾',
sourcePort: '來源端口',
targetIP: '目標 IP',
targetPort: '目標端口',
forwardHelper1: '如果是本機端口轉發目標 IP 127.0.0.1',
forwardHelper2: '如果目標 IP 不填寫默認為本機端口轉發',
}, },
runtime: { runtime: {
runtime: '運行環境', runtime: '運行環境',

View File

@ -92,6 +92,7 @@ const message = {
user: '用户', user: '用户',
title: '标题', title: '标题',
port: '端口', port: '端口',
forward: '转发',
protocol: '协议', protocol: '协议',
tableSetting: '列表设置', tableSetting: '列表设置',
refreshRate: '刷新频率', refreshRate: '刷新频率',
@ -2049,8 +2050,15 @@ const message = {
addressHelper2: '多个 IP IP 请用 "," 隔开172.16.10.11,172.16.0.0/24', addressHelper2: '多个 IP IP 请用 "," 隔开172.16.10.11,172.16.0.0/24',
allIP: '所有 IP', allIP: '所有 IP',
portRule: '端口规则', portRule: '端口规则',
forwardRule: '端口转发',
ipRule: 'IP 规则', ipRule: 'IP 规则',
userAgent: 'User-Agent 过滤', userAgent: 'User-Agent 过滤',
destination: '目的地',
sourcePort: '源端口',
targetIP: '目标 IP',
targetPort: '目标端口',
forwardHelper1: '如果是本机端口转发目标IP为127.0.0.1',
forwardHelper2: '如果目标IP不填写则默认为本机端口转发',
}, },
runtime: { runtime: {
runtime: '运行环境', runtime: '运行环境',

View File

@ -60,6 +60,16 @@ const hostRouter = {
requiresAuth: false, requiresAuth: false,
}, },
}, },
{
path: '/hosts/firewall/forward',
name: 'FirewallForward',
component: () => import('@/views/host/firewall/forward/index.vue'),
hidden: true,
meta: {
activeMenu: '/hosts/firewall/port',
requiresAuth: false,
},
},
{ {
path: '/hosts/firewall/ip', path: '/hosts/firewall/ip',
name: 'FirewallIP', name: 'FirewallIP',

View File

@ -327,11 +327,7 @@ export function checkPort(value: string): boolean {
return true; return true;
} }
const reg = /^([1-9](\d{0,3}))$|^([1-5]\d{4})$|^(6[0-4]\d{3})$|^(65[0-4]\d{2})$|^(655[0-2]\d)$|^(6553[0-5])$/; const reg = /^([1-9](\d{0,3}))$|^([1-5]\d{4})$|^(6[0-4]\d{3})$|^(65[0-4]\d{2})$|^(655[0-2]\d)$|^(6553[0-5])$/;
if (!reg.test(value) && value !== '') { return !reg.test(value) && value !== '';
return true;
} else {
return false;
}
} }
export function getProvider(provider: string): string { export function getProvider(provider: string): string {

View File

@ -0,0 +1,226 @@
<template>
<div>
<FireRouter />
<div v-loading="loading">
<FireStatus
v-show="fireName !== '-'"
ref="fireStatusRef"
@search="search"
v-model:loading="loading"
v-model:mask-show="maskShow"
v-model:status="fireStatus"
v-model:name="fireName"
/>
<div v-if="fireName !== '-'">
<el-card v-if="fireStatus != 'running' && maskShow" class="mask-prompt">
<span>{{ $t('firewall.firewallNotStart') }}</span>
</el-card>
<LayoutContent :title="$t('firewall.forwardRule')" :class="{ mask: fireStatus != 'running' }">
<template #toolbar>
<el-row>
<el-col :span="16">
<el-button type="primary" @click="onOpenDialog('create')">
{{ $t('commons.button.create') }}{{ $t('firewall.forwardRule') }}
</el-button>
<el-button @click="onDelete(null)" plain :disabled="selects.length === 0">
{{ $t('commons.button.delete') }}
</el-button>
</el-col>
<el-col :span="8">
<TableSetting @search="search()" />
<TableSearch @search="search()" v-model:searchName="searchName" />
</el-col>
</el-row>
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
@search="search"
:data="data"
>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.protocol')" :min-width="70" prop="protocol" />
<el-table-column :label="$t('firewall.sourcePort')" :min-width="70" prop="port" />
<el-table-column :min-width="80" :label="$t('firewall.targetIP')" prop="targetIP">
<template #default="{ row }">
<span v-if="row.targetIP">{{ row.targetIP }}</span>
<span v-else>127.0.0.1</span>
</template>
</el-table-column>
<el-table-column :label="$t('firewall.targetPort')" :min-width="70" prop="targetPort" />
<fu-table-operations
width="200px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
</div>
<div v-else>
<LayoutContent :title="$t('firewall.firewall')" :divider="true">
<template #main>
<div class="app-warn">
<div>
<span>{{ $t('firewall.notSupport') }}</span>
<span @click="toDoc">
<el-icon class="ml-2"><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</span>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</div>
</template>
</LayoutContent>
</div>
</div>
<OpDialog ref="opRef" @search="search" />
<OperateDialog @search="search" ref="dialogRef" />
</div>
</template>
<script lang="ts" setup>
import FireRouter from '@/views/host/firewall/index.vue';
import OperateDialog from './operate/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { operateForwardRule, searchFireRule } from '@/api/modules/host';
import { Host } from '@/api/interface/host';
import i18n from '@/lang';
const loading = ref();
const activeTag = ref('forward');
const selects = ref<any>([]);
const searchName = ref();
const searchStatus = ref('');
const searchStrategy = ref('');
const maskShow = ref(true);
const fireStatus = ref('running');
const fireName = ref();
const fireStatusRef = ref();
const opRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'firewall-forward-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
const search = async () => {
if (fireStatus.value !== 'running') {
loading.value = false;
data.value = [];
paginationConfig.total = 0;
return;
}
let params = {
type: activeTag.value,
status: searchStatus.value,
strategy: searchStrategy.value,
info: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
loading.value = true;
await searchFireRule(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const dialogRef = ref();
const onOpenDialog = async (
title: string,
rowData: Partial<Host.RuleForward> = {
protocol: 'tcp',
port: '8080',
targetIP: '',
targetPort: '',
},
) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/hosts/firewall/', '_blank', 'noopener,noreferrer');
};
const onDelete = async (row: Host.RuleForward | null) => {
let names = [];
let rules = [];
if (row) {
rules.push({
...row,
operation: 'remove',
});
names = [row.port + ' (' + row.protocol + ')'];
} else {
for (const item of selects.value) {
names.push(item.port + ' (' + item.protocol + ')');
rules.push({
...item,
operation: 'remove',
});
}
}
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('firewall.forwardRule'),
i18n.global.t('commons.button.delete'),
]),
api: operateForwardRule,
params: { rules: rules },
});
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: Host.RuleForward) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: Host.RuleForward) => {
onDelete(row);
},
},
];
onMounted(() => {
if (fireName.value !== '-') {
loading.value = true;
fireStatusRef.value.acceptParams();
}
});
</script>
<style lang="scss" scoped>
.svg-icon {
font-size: 8px;
margin-bottom: -4px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader :header="title" :back="handleClose" />
</template>
<div v-loading="loading">
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('commons.table.protocol')" prop="protocol">
<el-select style="width: 100%" v-model="dialogData.rowData!.protocol">
<el-option value="tcp" label="tcp" />
<el-option value="udp" label="udp" />
<el-option value="tcp/udp" label="tcp/udp" />
</el-select>
</el-form-item>
<el-form-item :label="$t('firewall.sourcePort')" prop="port">
<el-input clearable v-model.trim="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('firewall.targetIP')" prop="targetIP">
<el-input v-model.trim="dialogData.rowData!.targetIP" />
<span class="input-help">{{ $t('firewall.forwardHelper1') }}</span>
<span class="input-help">{{ $t('firewall.forwardHelper2') }}</span>
</el-form-item>
<el-form-item :label="$t('firewall.targetPort')" prop="targetPort">
<el-input clearable v-model.trim="dialogData.rowData!.targetPort" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, FormItemRule } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { Host } from '@/api/interface/host';
import { operateForwardRule } from '@/api/modules/host';
import { checkCidr, checkIpV4V6, deepCopy } from '@/utils/util';
const loading = ref();
const oldRule = ref<Host.RuleForward>();
interface DialogProps {
title: string;
rowData?: Host.RuleForward;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
if (dialogData.value.title === 'edit') {
oldRule.value = deepCopy(params.rowData);
}
title.value = i18n.global.t('firewall.' + dialogData.value.title);
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
const strPortValidator: FormItemRule = {
required: true,
trigger: 'blur',
validator: (_, value: string) => {
const port = parseInt(value);
return port >= 1 && port <= 65535;
},
message: i18n.global.t('commons.rule.port'),
};
const rules = reactive({
protocol: [Rules.requiredSelect],
port: [Rules.requiredInput, strPortValidator],
targetPort: [Rules.requiredInput, strPortValidator],
targetIP: [{ validator: checkAddress, trigger: 'blur' }],
});
function checkAddress(rule: any, value: string, callback: any) {
if (!value) {
return callback();
}
let addrs = value.split(',');
for (const item of addrs) {
if (item.indexOf('/') !== -1) {
if (checkCidr(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else {
if (checkIpV4V6(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
}
callback();
}
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
const { rowData } = dialogData.value;
let rules = [];
if (!rowData) return;
rowData.operation = 'add';
if (rowData.targetIP === '') {
rowData.targetIP = '127.0.0.1';
}
rules.push(rowData);
loading.value = true;
if (dialogData.value.title === 'create') {
await operateForwardRule({ rules: rules })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
return;
}
rules = [];
oldRule.value.operation = 'remove';
dialogData.value.rowData.operation = 'add';
rules.push(oldRule.value);
rules.push(dialogData.value.rowData);
await operateForwardRule({ rules: rules })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -15,6 +15,10 @@ const buttons = [
label: i18n.global.t('firewall.portRule'), label: i18n.global.t('firewall.portRule'),
path: '/hosts/firewall/port', path: '/hosts/firewall/port',
}, },
{
label: i18n.global.t('firewall.forwardRule'),
path: '/hosts/firewall/forward',
},
{ {
label: i18n.global.t('firewall.ipRule'), label: i18n.global.t('firewall.ipRule'),
path: '/hosts/firewall/ip', path: '/hosts/firewall/ip',