Skip to content

Commit

Permalink
feat: 端口转发功能 (#5439)
Browse files Browse the repository at this point in the history
  • Loading branch information
endymx authored Jun 15, 2024
1 parent b36e2c5 commit af0ecce
Show file tree
Hide file tree
Showing 23 changed files with 812 additions and 46 deletions.
23 changes: 23 additions & 0 deletions backend/app/api/v1/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ func (b *BaseApi) OperatePortRule(c *gin.Context) {
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
// @Summary Create group
// @Description 创建防火墙 IP 规则
Expand Down
11 changes: 11 additions & 0 deletions backend/app/dto/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ type PortRuleOperate struct {
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 string `json:"type"`
Address string `json:"address"`
Expand Down
9 changes: 9 additions & 0 deletions backend/app/model/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ type Firewall struct {
Strategy string `gorm:"type:varchar(64);not null" json:"strategy"`
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"`
}
80 changes: 54 additions & 26 deletions backend/app/service/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"sync"
Expand All @@ -28,6 +29,7 @@ type IFirewallService interface {
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
OperateFirewall(operation string) error
OperatePortRule(req dto.PortRuleOperate, reload bool) error
OperateForwardRule(req dto.ForwardRuleOperate) error
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
UpdatePortRule(req dto.PortRuleUpdate) error
UpdateAddrRule(req dto.AddrRuleUpdate) error
Expand Down Expand Up @@ -78,42 +80,36 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
if err != nil {
return 0, nil, err
}
if req.Type == "port" {
ports, err := client.ListPort()
if err != nil {
return 0, nil, err
}
if len(req.Info) != 0 {
for _, port := range ports {
if strings.Contains(port.Port, req.Info) {
datas = append(datas, port)
}

var rules []fireClient.FireInfo
switch req.Type {
case "port":
rules, err = client.ListPort()
case "forward":
rules, err = client.ListForward()
case "address":
rules, err = client.ListAddress()
}
if err != nil {
return 0, nil, err
}

if len(req.Info) != 0 {
for _, addr := range rules {
if strings.Contains(addr.Address, req.Info) {
datas = append(datas, addr)
}
} else {
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
}
datas = rules
}

if req.Type == "port" {
apps := u.loadPortByApp()
for i := 0; i < len(datas); i++ {
datas[i].UsedStatus = checkPortUsed(datas[i].Port, datas[i].Protocol, apps)
}
}

var datasFilterStatus []fireClient.FireInfo
if len(req.Status) != 0 {
for _, data := range datas {
Expand All @@ -127,6 +123,7 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
} else {
datasFilterStatus = datas
}

var datasFilterStrategy []fireClient.FireInfo
if len(req.Strategy) != 0 {
for _, data := range datasFilterStatus {
Expand Down Expand Up @@ -300,6 +297,37 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
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 {
client, err := firewall.NewFirewallClient()
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions backend/init/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/firewall"
"path"

"github.com/1Panel-dev/1Panel/backend/constant"
Expand Down Expand Up @@ -31,6 +32,10 @@ func Init() {
}

_ = docker.CreateDefaultDockerNetwork()

if f, err := firewall.NewFirewallClient(); err == nil {
_ = f.EnableForward()
}
}

func createDir(fileOp files.FileOp, dirPath string) {
Expand Down
1 change: 1 addition & 0 deletions backend/init/migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func Init() {
migrations.AddFtp,
migrations.AddProxy,
migrations.AddCronJobColumn,
migrations.AddForward,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
Expand Down
10 changes: 10 additions & 0 deletions backend/init/migration/migrations/v_1_10.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
ID: "20240524-add-cronjob-command",
Migrate: func(tx *gorm.DB) error {
Expand Down
1 change: 1 addition & 0 deletions backend/router/ro_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
hostRouter.POST("/firewall/port", baseApi.OperatePortRule)
hostRouter.POST("/firewall/forward", baseApi.OperateForwardRule)
hostRouter.POST("/firewall/ip", baseApi.OperateIPRule)
hostRouter.POST("/firewall/batch", baseApi.BatchOperateRule)
hostRouter.POST("/firewall/update/port", baseApi.UpdatePortRule)
Expand Down
3 changes: 3 additions & 0 deletions backend/utils/firewall/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ type FirewallClient interface {
Version() (string, error)

ListPort() ([]client.FireInfo, error)
ListForward() ([]client.FireInfo, error)
ListAddress() ([]client.FireInfo, error)

Port(port client.FireInfo, operation string) error
RichRules(rule client.FireInfo, operation string) error
PortForward(info client.Forward, operation string) error

EnableForward() error
}

func NewFirewallClient() (FirewallClient, error) {
Expand Down
51 changes: 48 additions & 3 deletions backend/utils/firewall/client/firewalld.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"fmt"
"regexp"
"strings"
"sync"

Expand All @@ -10,6 +11,8 @@ import (
"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{}

func NewFirewalld() (*Firewall, error) {
Expand Down Expand Up @@ -115,6 +118,29 @@ func (f *Firewall) ListPort() ([]FireInfo, error) {
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) {
stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules")
if err != nil {
Expand Down Expand Up @@ -175,15 +201,18 @@ func (f *Firewall) RichRules(rule FireInfo, 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)
if len(info.Address) != 0 {
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:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetPort)
if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" {
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)
if err != nil {
return fmt.Errorf("%s port forward failed, err: %s", operation, stdout)
}
if err = f.Reload(); err != nil {
return err
}
return nil
}

Expand All @@ -208,3 +237,19 @@ func (f *Firewall) loadInfo(line string) FireInfo {
}
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
}
24 changes: 20 additions & 4 deletions backend/utils/firewall/client/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@ type FireInfo struct {
Protocol string `json:"protocol"` // tcp udp tcp/udp
Strategy string `json:"strategy"` // accept drop

Num string `json:"num"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`

UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
}

type Forward struct {
Protocol string `json:"protocol"`
Address string `json:"address"`
Port string `json:"port"`
Target string `json:"target"`
Num string `json:"num"`
Protocol string `json:"protocol"`
Port string `json:"port"`
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"`
}
Loading

0 comments on commit af0ecce

Please sign in to comment.