feat: support iptables firewall control (#10903)

This commit is contained in:
ssongliu
2025-11-10 21:17:53 +08:00
committed by GitHub
parent 977a4be3c1
commit 6db79f4b7b
46 changed files with 2691 additions and 378 deletions

View File

@@ -38,6 +38,7 @@ var (
fileService = service.NewIFileService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()

View File

@@ -8,12 +8,19 @@ import (
// @Tags Firewall
// @Summary Load firewall base info
// @Accept json
// @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.FirewallBaseInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/base [get]
// @Router /hosts/firewall/base [post]
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
data, err := firewallService.LoadBaseInfo()
var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := firewallService.LoadBaseInfo(req.Name)
if err != nil {
helper.InternalServer(c, err)
return
@@ -221,3 +228,112 @@ func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
}
helper.Success(c)
}
// @Tags Firewall
// @Summary search iptables filter rules
// @Accept json
// @Param request body dto.SearchPageWithType true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/search [post]
func (b *BaseApi) SearchFilterRules(c *gin.Context) {
var req dto.SearchPageWithType
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := iptablesService.Search(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Firewall
// @Summary Operate iptables filter rule
// @Accept json
// @Param request body dto.IptablesRuleOp true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/rule/operate [post]
// @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
func (b *BaseApi) OperateFilterRule(c *gin.Context) {
var req dto.IptablesRuleOp
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := iptablesService.OperateRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Batch operate iptables filter rules
// @Accept json
// @Param request body dto.IptablesBatchOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/rule/batch [post]
func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
var req dto.IptablesBatchOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := iptablesService.BatchOperate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Apply/Unload/Init iptables filter
// @Accept json
// @Param request body dto.IptablesOp true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/operate [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] iptables filter 防火墙","formatEN":"[operate] iptables filter firewall"}
func (b *BaseApi) OperateFilterChain(c *gin.Context) {
var req dto.IptablesOp
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := iptablesService.Operate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary load chain status with name
// @Accept json
// @Param request body dto.OperationWithName true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/chain/status [post]
func (b *BaseApi) LoadChainStatus(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, iptablesService.LoadChainStatus(req))
}

View File

@@ -4,6 +4,8 @@ type FirewallBaseInfo struct {
Name string `json:"name"`
IsExist bool `json:"isExist"`
IsActive bool `json:"isActive"`
IsInit bool `json:"isInit"`
IsBind bool `json:"isBind"`
Version string `json:"version"`
PingStatus string `json:"pingStatus"`
}
@@ -22,6 +24,7 @@ type FirewallOperation struct {
}
type PortRuleOperate struct {
ID uint `json:"id"`
Operation string `json:"operation" validate:"required,oneof=add remove"`
Address string `json:"address"`
Port string `json:"port" validate:"required"`
@@ -55,6 +58,7 @@ type UpdateFirewallDescription struct {
}
type AddrRuleOperate struct {
ID uint `json:"id"`
Operation string `json:"operation" validate:"required,oneof=add remove"`
Address string `json:"address" validate:"required"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
@@ -76,3 +80,30 @@ type BatchRuleOperate struct {
Type string `json:"type" validate:"required"`
Rules []PortRuleOperate `json:"rules"`
}
type IptablesOp struct {
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
}
type IptablesRuleOp struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
ID uint `json:"id"`
Chain string `json:"chain" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT"`
Protocol string `json:"protocol"`
SrcIP string `json:"srcIP"`
SrcPort uint `json:"srcPort"`
DstIP string `json:"dstIP"`
DstPort uint `json:"dstPort"`
Strategy string `json:"strategy" validate:"required,oneof=ACCEPT DROP REJECT"`
Description string `json:"description"`
}
type IptablesBatchOperate struct {
Rules []IptablesRuleOp `json:"rules"`
}
type IptablesChainStatus struct {
IsBind bool `json:"isBind"`
DefaultStrategy string `json:"defaultStrategy"`
}

View File

@@ -3,20 +3,16 @@ package model
type Firewall struct {
BaseModel
Type string `gorm:"not null" json:"type"`
Port string `gorm:"not null" json:"port"`
Protocol string `gorm:"not null" json:"protocol"`
Address string `gorm:"not null" json:"address"`
Type string `json:"type"`
Port string `json:"port"` // Deprecated
Address string `json:"address"` // Deprecated
Chain string `json:"chain"`
Protocol string `json:"protocol"`
SrcIP string `json:"srcIP"`
SrcPort string `json:"srcPort"`
DstIP string `json:"dstIP"`
DstPort string `json:"dstPort"`
Strategy string `gorm:"not null" json:"strategy"`
Description string `gorm:"not null" json:"description"`
}
type Forward struct {
BaseModel
Protocol string `gorm:"not null" json:"protocol"`
Port string `gorm:"not null" json:"port"`
TargetIP string `gorm:"not null" json:"targetIP"`
TargetPort string `gorm:"not null" json:"targetPort"`
Interface string `json:"interface"`
Description string `json:"description"`
}

View File

@@ -4,16 +4,16 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)
type HostRepo struct{}
type IHostRepo interface {
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
ListFirewallRecord() ([]model.Firewall, error)
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
SaveFirewallRecord(firewall *model.Firewall) error
DeleteFirewallRecordByID(id uint) error
DeleteFirewallRecord(fType, port, protocol, address, strategy string) error
SyncCert(data []model.RootCert) error
GetCert(opts ...DBOption) (model.RootCert, error)
@@ -22,6 +22,8 @@ type IHostRepo interface {
SaveCert(cert *model.RootCert) error
UpdateCert(id uint, vars map[string]interface{}) error
DeleteCert(opts ...DBOption) error
WithByChain(chain string) DBOption
}
func NewIHostRepo() IHostRepo {
@@ -38,12 +40,16 @@ func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
return firewall, err
}
func (h *HostRepo) ListFirewallRecord() ([]model.Firewall, error) {
var datas []model.Firewall
if err := global.DB.Find(&datas).Error; err != nil {
return datas, nil
func (h *HostRepo) ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error) {
var firewalls []model.Firewall
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return datas, nil
if err := global.DB.Find(&firewalls).Error; err != nil {
return firewalls, nil
}
return firewalls, nil
}
func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
@@ -52,12 +58,12 @@ func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
}
var data model.Firewall
if firewall.Type == "port" {
_ = global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", "port", firewall.Port, firewall.Protocol, firewall.Address, firewall.Strategy).First(&data)
_ = global.DB.Where("type = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND strategy = ?", "port", firewall.DstPort, firewall.Protocol, firewall.SrcIP, firewall.Strategy).First(&data)
if data.ID != 0 {
firewall.ID = data.ID
}
} else {
_ = global.DB.Where("type = ? AND address = ? AND strategy = ?", "address", firewall.Address, firewall.Strategy).First(&data)
_ = global.DB.Where("type = ? AND src_ip = ? AND strategy = ?", "address", firewall.SrcIP, firewall.Strategy).First(&data)
if data.ID != 0 {
firewall.ID = data.ID
}
@@ -69,10 +75,6 @@ func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error
}
func (h *HostRepo) DeleteFirewallRecord(fType, port, protocol, address, strategy string) error {
return global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete(&model.Firewall{}).Error
}
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
var cert model.RootCert
db := global.DB
@@ -151,3 +153,9 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
tx.Commit()
return nil
}
func (u *HostRepo) WithByChain(chain string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("chain = ?", chain)
}
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
"github.com/jinzhu/copier"
)
@@ -27,7 +28,7 @@ const confPath = "/etc/sysctl.conf"
type FirewallService struct{}
type IFirewallService interface {
LoadBaseInfo() (dto.FirewallBaseInfo, error)
LoadBaseInfo(tab string) (dto.FirewallBaseInfo, error)
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
OperateFirewall(req dto.FirewallOperation) error
OperatePortRule(req dto.PortRuleOperate, reload bool) error
@@ -43,7 +44,7 @@ func NewIFirewallService() IFirewallService {
return &FirewallService{}
}
func (u *FirewallService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
func (u *FirewallService) LoadBaseInfo(tab string) (dto.FirewallBaseInfo, error) {
var baseInfo dto.FirewallBaseInfo
baseInfo.Version = "-"
baseInfo.Name = "-"
@@ -57,7 +58,7 @@ func (u *FirewallService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
baseInfo.Name = client.Name()
var wg sync.WaitGroup
wg.Add(3)
wg.Add(4)
go func() {
defer wg.Done()
baseInfo.PingStatus = u.pingStatus()
@@ -70,6 +71,10 @@ func (u *FirewallService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
defer wg.Done()
baseInfo.Version, _ = client.Version()
}()
go func() {
defer wg.Done()
baseInfo.IsInit, baseInfo.IsBind = loadInitStatus(baseInfo.Name, tab)
}()
wg.Wait()
return baseInfo, nil
}
@@ -158,15 +163,17 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
if req.Type != des.Type {
continue
}
if backDatas[i].Port == des.Port &&
if backDatas[i].Port == des.DstPort &&
req.Type == "port" &&
backDatas[i].Protocol == des.Protocol &&
backDatas[i].Strategy == des.Strategy &&
backDatas[i].Address == des.Address {
backDatas[i].Address == des.SrcIP {
backDatas[i].ID = des.ID
backDatas[i].Description = des.Description
break
}
if req.Type == "address" && backDatas[i].Strategy == des.Strategy && backDatas[i].Address == des.Address {
if req.Type == "address" && backDatas[i].Strategy == des.Strategy && backDatas[i].Address == des.SrcIP {
backDatas[i].ID = des.ID
backDatas[i].Description = des.Description
break
}
@@ -224,6 +231,10 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
if err != nil {
return err
}
chain := ""
if client.Name() == "iptables" {
chain = iptables.Chain1PanelBasic
}
protos := strings.Split(req.Protocol, "/")
itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",")
@@ -241,7 +252,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
return err
}
req.Port = strings.ReplaceAll(req.Port, ":", "-")
if err := u.addPortRecord(req); err != nil {
if err := u.addPortRecord(chain, req); err != nil {
return err
}
}
@@ -262,7 +273,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
if len(req.Protocol) == 0 {
req.Protocol = "tcp/udp"
}
if err := u.addPortRecord(req); err != nil {
if err := u.addPortRecord(chain, req); err != nil {
return err
}
}
@@ -278,7 +289,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
if err := u.operatePort(client, req); err != nil {
return err
}
if err := u.addPortRecord(req); err != nil {
if err := u.addPortRecord(chain, req); err != nil {
return err
}
}
@@ -295,7 +306,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
if err := u.operatePort(client, req); err != nil {
return err
}
if err := u.addPortRecord(req); err != nil {
if err := u.addPortRecord(chain, req); err != nil {
return err
}
}
@@ -404,7 +415,10 @@ func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload boo
if err != nil {
return err
}
chain := ""
if client.Name() == "iptables" {
chain = iptables.Chain1PanelBasic
}
var fireInfo fireClient.FireInfo
if err := copier.Copy(&fireInfo, &req); err != nil {
return err
@@ -420,7 +434,7 @@ func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload boo
return err
}
req.Address = addressList[i]
if err := u.addAddressRecord(req); err != nil {
if err := u.addAddressRecord(chain, req); err != nil {
return err
}
}
@@ -459,10 +473,16 @@ func (u *FirewallService) UpdateAddrRule(req dto.AddrRuleUpdate) error {
}
func (u *FirewallService) UpdateDescription(req dto.UpdateFirewallDescription) error {
var firewall model.Firewall
if err := copier.Copy(&firewall, &req); err != nil {
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
firewall := model.Firewall{
Type: req.Type,
Chain: iptables.Chain1PanelBasic,
SrcIP: req.Address,
DstPort: req.Port,
Protocol: req.Protocol,
Strategy: req.Strategy,
Description: req.Description,
}
return hostRepo.SaveFirewallRecord(&firewall)
}
@@ -562,7 +582,7 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
}
for _, item := range list {
for i := 0; i < len(records); i++ {
if records[i].Port == item.Port && records[i].Protocol == item.Protocol && records[i].Strategy == item.Strategy && records[i].Address == item.Address {
if records[i].DstPort == item.Port && records[i].Protocol == item.Protocol && records[i].Strategy == item.Strategy && records[i].SrcIP == item.Address {
records = append(records[:i], records[i+1:]...)
}
}
@@ -659,16 +679,19 @@ func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) er
return client.Reload()
}
func (u *FirewallService) addPortRecord(req dto.PortRuleOperate) error {
func (u *FirewallService) addPortRecord(chain string, req dto.PortRuleOperate) error {
if req.Operation == "remove" {
return hostRepo.DeleteFirewallRecord("port", req.Port, req.Protocol, req.Address, req.Strategy)
if req.ID != 0 {
return hostRepo.DeleteFirewallRecordByID(req.ID)
}
}
if err := hostRepo.SaveFirewallRecord(&model.Firewall{
Type: "port",
Port: req.Port,
Chain: chain,
DstPort: req.Port,
Protocol: req.Protocol,
Address: req.Address,
SrcIP: req.Address,
Strategy: req.Strategy,
Description: req.Description,
}); err != nil {
@@ -678,13 +701,17 @@ func (u *FirewallService) addPortRecord(req dto.PortRuleOperate) error {
return nil
}
func (u *FirewallService) addAddressRecord(req dto.AddrRuleOperate) error {
func (u *FirewallService) addAddressRecord(chain string, req dto.AddrRuleOperate) error {
if req.Operation == "remove" {
return hostRepo.DeleteFirewallRecord("address", "", "", req.Address, req.Strategy)
if req.ID != 0 {
return hostRepo.DeleteFirewallRecordByID(req.ID)
}
}
if err := hostRepo.SaveFirewallRecord(&model.Firewall{
Type: "address",
Address: req.Address,
Chain: chain,
SrcIP: req.Address,
Strategy: req.Strategy,
Description: req.Description,
}); err != nil {
@@ -751,3 +778,90 @@ func checkPortUsed(ports, proto string, apps []portOfApp) string {
}
return ""
}
func loadInitStatus(clientName, tab string) (bool, bool) {
if clientName != "iptables" && tab != "forward" {
return true, true
}
switch tab {
case "base":
if isExist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasicBefore); !isExist {
return false, false
}
if exist := iptables.CheckRuleExist(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.IoRuleIn); !exist {
return false, false
}
if exist := iptables.CheckRuleExist(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.EstablishedRule); !exist {
return false, false
}
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasic); !exist {
return false, false
}
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasicAfter); !exist {
return false, false
}
if exist := iptables.CheckRuleExist(iptables.FilterTab, iptables.Chain1PanelBasicAfter, iptables.DropAll); !exist {
return false, false
}
if bind, _ := iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore); !bind {
return true, false
}
if bind, _ := iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic); !bind {
return true, false
}
if bind, _ := iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter); !bind {
return true, false
}
return true, true
case "advance":
isExist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelInput)
if !isExist {
return false, false
}
isExist, _ = iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelOutput)
if !isExist {
return false, false
}
isBind, _ := iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelInput)
if !isBind {
return true, false
}
isBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, iptables.Chain1PanelOutput)
return true, isBind
case "forward":
stdout, err := cmd.RunDefaultWithStdoutBashC("cat /proc/sys/net/ipv4/ip_forward")
if err != nil {
global.LOG.Errorf("check /proc/sys/net/ipv4/ip_forward failed, err: %v", err)
return false, false
}
if strings.TrimSpace(stdout) == "0" {
return false, false
}
exist, _ := iptables.CheckChainExist(iptables.NatTab, iptables.Chain1PanelPreRouting)
if !exist {
return false, false
}
exist, _ = iptables.CheckChainExist(iptables.NatTab, iptables.Chain1PanelPostRouting)
if !exist {
return false, false
}
exist, _ = iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelForward)
if !exist {
return false, false
}
isBind, _ := iptables.CheckChainBind(iptables.NatTab, "PREROUTING", iptables.Chain1PanelPreRouting)
if !isBind {
return false, false
}
isBind, _ = iptables.CheckChainBind(iptables.NatTab, "POSTROUTING", iptables.Chain1PanelPostRouting)
if !isBind {
return false, false
}
isBind, _ = iptables.CheckChainBind(iptables.FilterTab, "FORWARD", iptables.Chain1PanelForward)
return true, isBind
default:
return false, false
}
}

View File

@@ -0,0 +1,323 @@
package service
import (
"errors"
"fmt"
"net"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
type IIptablesService interface {
Search(req dto.SearchPageWithType) (int64, interface{}, error)
OperateRule(req dto.IptablesRuleOp) error
BatchOperate(req dto.IptablesBatchOperate) error
LoadChainStatus(req dto.OperationWithName) dto.IptablesChainStatus
Operate(req dto.IptablesOp) error
}
type IptablesService struct{}
func NewIIptablesService() IIptablesService {
return &IptablesService{}
}
func (s *IptablesService) Search(req dto.SearchPageWithType) (int64, interface{}, error) {
rules, err := iptables.ReadFilterRulesByChain(req.Type)
if err != nil {
return 0, nil, fmt.Errorf("failed to read iptables rules: %w", err)
}
var records []iptables.FilterRules
total, start, end := len(rules), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]iptables.FilterRules, 0)
} else {
if end >= total {
end = total
}
records = rules[start:end]
}
rulesInDB, _ := hostRepo.ListFirewallRecord(hostRepo.WithByChain(req.Type))
for i := 0; i < len(records); i++ {
for _, item := range rulesInDB {
if records[i].Strategy == item.Strategy &&
records[i].DstIP == item.DstIP &&
fmt.Sprintf("%v", records[i].DstPort) == item.DstPort &&
records[i].Protocol == item.Protocol &&
records[i].SrcIP == item.SrcIP &&
fmt.Sprintf("%v", records[i].SrcPort) == item.SrcPort {
records[i].ID = item.ID
records[i].Description = item.Description
}
}
}
return int64(total), records, nil
}
func (s *IptablesService) OperateRule(req dto.IptablesRuleOp) error {
policy := iptables.FilterRules{
Protocol: req.Protocol,
SrcIP: req.SrcIP,
SrcPort: req.SrcPort,
DstIP: req.DstIP,
DstPort: req.DstPort,
Strategy: req.Strategy,
}
name := iptables.InputFileName
if req.Chain == iptables.Chain1PanelOutput {
name = iptables.OutputFileName
}
switch req.Operation {
case "add":
if err := s.validateRuleInput(&req); err != nil {
return err
}
if err := iptables.AddFilterRule(req.Chain, policy); err != nil {
return fmt.Errorf("failed to add iptables rule: %w", err)
}
rule := &model.Firewall{
Chain: req.Chain,
Protocol: req.Protocol,
SrcIP: req.SrcIP,
SrcPort: fmt.Sprintf("%v", req.SrcPort),
DstIP: req.DstIP,
DstPort: fmt.Sprintf("%v", req.DstPort),
Strategy: req.Strategy,
Description: req.Description,
}
if err := hostRepo.SaveFirewallRecord(rule); err != nil {
return fmt.Errorf("failed to save rule to database: %w", err)
}
case "remove":
if err := iptables.DeleteFilterRule(req.Chain, policy); err != nil {
return fmt.Errorf("failed to remove iptables rule: %w", err)
}
if req.ID != 0 {
if err := hostRepo.DeleteFirewallRecordByID(req.ID); err != nil {
return fmt.Errorf("failed to delete rule from database: %w", err)
}
}
}
if err := iptables.SaveRulesToFile(iptables.FilterTab, req.Chain, name); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
}
return nil
}
func (s *IptablesService) BatchOperate(req dto.IptablesBatchOperate) error {
for _, rule := range req.Rules {
if err := s.OperateRule(rule); err != nil {
return err
}
}
return nil
}
func (s *IptablesService) Operate(req dto.IptablesOp) error {
targetChain := iptables.ChainInput
if req.Name == iptables.Chain1PanelOutput {
targetChain = iptables.ChainOutput
}
switch req.Operate {
case "init-base":
if ok := cmd.Which("iptables"); !ok {
return fmt.Errorf("failed to find iptables")
}
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasicBefore); err != nil {
return err
}
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasic); err != nil {
return err
}
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasicAfter); err != nil {
return err
}
if err := initPreRules(); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore, 1); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic, 2); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter, 3); err != nil {
return err
}
return nil
case "init-forward":
return client.EnableIptablesForward()
case "init-advance":
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelInput); err != nil {
return err
}
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelOutput); err != nil {
return err
}
number := loadBindNumber()
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainOutput, iptables.Chain1PanelOutput, number); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelInput, number); err != nil {
return err
}
return nil
case "bind-base":
if err := initPreRules(); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore, 1); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic, 2); err != nil {
return err
}
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter, 3); err != nil {
return err
}
return nil
case "unbind-base":
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter); err != nil {
return err
}
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore); err != nil {
return err
}
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic); err != nil {
return err
}
return nil
case "bind":
if err := iptables.BindChain(iptables.FilterTab, targetChain, req.Name, loadBindNumber()); err != nil {
return err
}
return nil
case "unbind":
if err := iptables.UnbindChain(iptables.FilterTab, targetChain, req.Name); err != nil {
return err
}
return nil
}
return nil
}
func (s *IptablesService) LoadChainStatus(req dto.OperationWithName) dto.IptablesChainStatus {
var data dto.IptablesChainStatus
var err error
data.DefaultStrategy, err = iptables.LoadDefaultStrategy(req.Name)
if err != nil {
global.LOG.Error(err)
}
switch req.Name {
case iptables.Chain1PanelBasic:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
case iptables.Chain1PanelInput:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
case iptables.Chain1PanelOutput:
data.IsBind, err = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
}
return data
}
func (s *IptablesService) validateRuleInput(req *dto.IptablesRuleOp) error {
if req.Protocol != "" {
validProtocols := map[string]bool{"tcp": true, "udp": true, "icmp": true, "all": true}
if !validProtocols[strings.ToLower(req.Protocol)] {
return fmt.Errorf("invalid protocol: %s, must be tcp, udp, icmp or all", req.Protocol)
}
}
if req.SrcIP != "" {
if err := s.validateIPOrCIDR(req.SrcIP); err != nil {
return fmt.Errorf("invalid source IP: %w", err)
}
}
if req.DstIP != "" {
if err := s.validateIPOrCIDR(req.DstIP); err != nil {
return fmt.Errorf("invalid destination IP: %w", err)
}
}
if req.SrcPort > 65535 {
return fmt.Errorf("invalid source port: %d, must be between 1 and 65535", req.SrcPort)
}
if req.DstPort > 65535 {
return fmt.Errorf("invalid destination port: %d, must be between 1 and 65535", req.DstPort)
}
if (req.SrcPort > 0 || req.DstPort > 0) && req.Protocol == "" {
return fmt.Errorf("port specification requires protocol (tcp/udp)")
}
return nil
}
func (s *IptablesService) validateIPOrCIDR(ipStr string) error {
if strings.Contains(ipStr, "/") {
_, _, err := net.ParseCIDR(ipStr)
if err != nil {
return fmt.Errorf("invalid CIDR format: %w", err)
}
return nil
}
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("invalid IP address format")
}
return nil
}
func loadBindNumber() int {
number := 1
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasicBefore); exist {
number++
}
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasic); exist {
number++
}
return number
}
func initPreRules() error {
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.IoRuleIn); err != nil {
return err
}
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.EstablishedRule); err != nil {
return err
}
panelPort := ""
if !global.IsMaster {
panelPort = global.CONF.Base.Port
} else {
var portSetting model.Setting
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
if len(portSetting.Value) != 0 {
panelPort = portSetting.Value
}
}
if len(panelPort) == 0 {
return errors.New("find 1panel service port failed")
}
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.AllowSSH); err != nil {
return err
}
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, fmt.Sprintf("-p tcp -m tcp --dport %s -j ACCEPT", panelPort)); err != nil {
return err
}
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicAfter, iptables.DropAll); err != nil {
return err
}
return nil
}

View File

@@ -47,6 +47,7 @@ type SystemDir struct {
McpDir string
ConvertLogDir string
TensorRTLLMDir string
FirewallDir string
}
type LogConfig struct {

View File

@@ -2,15 +2,10 @@ package app
import (
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
)
func Init() {
go func() {
_ = docker.CreateDefaultDockerNetwork()
if f, err := firewall.NewFirewallClient(); err == nil {
_ = f.EnableForward()
}
}()
}

View File

@@ -34,4 +34,5 @@ func Init() {
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
global.Dir.ConvertLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/convert"))
global.Dir.TensorRTLLMDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/ai/tensorrt_llm"))
global.Dir.FirewallDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/firewall"))
}

View File

@@ -0,0 +1,24 @@
package firewall
import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
func Init() {
client, err := firewall.NewFirewallClient()
if err != nil {
return
}
clientName := client.Name()
if clientName == "ufw" || clientName == "iptables" {
_ = iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelForward, iptables.ForwardFileName)
_ = iptables.LoadRulesFromFile(iptables.NatTab, iptables.Chain1PanelPreRouting, iptables.ForwardFileName1)
_ = iptables.LoadRulesFromFile(iptables.NatTab, iptables.Chain1PanelPostRouting, iptables.ForwardFileName2)
}
if clientName == "iptables" {
_ = iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName)
_ = iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelInput, iptables.InputFileName)
_ = iptables.LoadRulesFromFile(iptables.FilterTab, iptables.Chain1PanelOutput, iptables.OutputFileName)
}
}

View File

@@ -51,6 +51,7 @@ func InitAgentDB() {
migrations.AddMonitorProcess,
migrations.UpdateCronJob,
migrations.UpdateTensorrtLLM,
migrations.AddIptablesFilterRuleTable,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@@ -19,6 +19,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
@@ -48,7 +49,6 @@ var AddTable = &gormigrate.Migration{
&model.DatabaseMysql{},
&model.DatabasePostgresql{},
&model.Favorite{},
&model.Forward{},
&model.Firewall{},
&model.Ftp{},
&model.ImageRepo{},
@@ -660,6 +660,31 @@ var UpdateMonitorInterval = &gormigrate.Migration{
},
}
var AddIptablesFilterRuleTable = &gormigrate.Migration{
ID: "20251106-add-iptables-filter-rule-table",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Firewall{}); err != nil {
return err
}
var firewalls []model.Firewall
_ = tx.Where("1 = 1").Find(&firewalls).Error
firewallType := ""
client, err := firewall.NewFirewallClient()
if err == nil {
firewallType = client.Name()
}
for _, item := range firewalls {
if err := tx.Model(&model.Firewall{}).
Where("id = ?", item.ID).
Updates(map[string]interface{}{"dst_port": item.Port, "src_ip": item.Address, "firewall_type": firewallType}); err != nil {
global.LOG.Errorf("update firewall failed, err: %v", err)
}
}
return nil
},
}
var AddMonitorProcess = &gormigrate.Migration{
ID: "20251030-add-monitor-process",
Migrate: func(tx *gorm.DB) error {

View File

@@ -11,7 +11,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter := Router.Group("hosts")
baseApi := v2.ApiGroupApp.BaseApi
{
hostRouter.GET("/firewall/base", baseApi.LoadFirewallBaseInfo)
hostRouter.POST("/firewall/base", baseApi.LoadFirewallBaseInfo)
hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule)
hostRouter.POST("/firewall/operate", baseApi.OperateFirewall)
hostRouter.POST("/firewall/port", baseApi.OperatePortRule)
@@ -22,6 +22,12 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/update/addr", baseApi.UpdateAddrRule)
hostRouter.POST("/firewall/update/description", baseApi.UpdateFirewallDescription)
hostRouter.POST("/firewall/filter/rule/search", baseApi.SearchFilterRules)
hostRouter.POST("/firewall/filter/rule/operate", baseApi.OperateFilterRule)
hostRouter.POST("/firewall/filter/rule/batch", baseApi.BatchOperateFilterRule)
hostRouter.POST("/firewall/filter/operate", baseApi.OperateFilterChain)
hostRouter.POST("/firewall/filter/chain/status", baseApi.LoadChainStatus)
hostRouter.POST("/monitor/search", baseApi.LoadMonitor)
hostRouter.POST("/monitor/clean", baseApi.CleanMonitor)
hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions)

View File

@@ -4,11 +4,12 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/gin-gonic/gin"
"net"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/cron"
@@ -19,6 +20,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/init/cache"
"github.com/1Panel-dev/1Panel/agent/init/db"
"github.com/1Panel-dev/1Panel/agent/init/dir"
"github.com/1Panel-dev/1Panel/agent/init/firewall"
"github.com/1Panel-dev/1Panel/agent/init/hook"
"github.com/1Panel-dev/1Panel/agent/init/lang"
"github.com/1Panel-dev/1Panel/agent/init/log"
@@ -38,8 +40,12 @@ func Start() {
i18n.Init()
cache.Init()
app.Init()
firewall.Init()
lang.Init()
validator.Init()
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode)
}
cron.Run()
hook.Init()
InitOthers()

View File

@@ -185,6 +185,7 @@ func loadFromPredefined(mgr Controller, keyword string) string {
"1panel-core": {"1panel-core.service"},
"1panel-agent": {"1panel-agent.service"},
"docker": {"docker.service", "dockerd"},
"iptables": {"iptables", "iptables-services"},
}
if val, ok := predefinedMap[keyword]; ok {
for _, item := range val {

View File

@@ -5,14 +5,15 @@ import (
"bytes"
"context"
"fmt"
"path"
"regexp"
"strings"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
"path"
"regexp"
"strings"
)
type ComposeService struct {

View File

@@ -34,12 +34,16 @@ func NewFirewallClient() (FirewallClient, error) {
if firewalld && ufw {
return nil, errors.New("It is detected that the system has both firewalld and ufw services. To avoid conflicts, please uninstall and try again!")
}
if firewalld {
return client.NewFirewalld()
}
if ufw {
return client.NewUfw()
}
return nil, errors.New("No system firewalld or ufw service detected, please check and try again!")
iptables := cmd.Which("iptables")
if iptables {
return client.NewIptables()
}
return nil, errors.New("No system firewall service detected (firewalld/ufw/iptables), please check and try again!")
}

View File

@@ -1,6 +1,7 @@
package client
type FireInfo struct {
ID uint `json:"uint"`
Family string `json:"family"` // ipv4 ipv6
Address string `json:"address"` // Anywhere
Port string `json:"port"`
@@ -24,25 +25,3 @@ type Forward struct {
TargetPort string `json:"targetPort"`
Interface string `json:"interface"`
}
type IptablesNatInfo struct {
Num string `json:"num"`
Target string `json:"target"`
Protocol string `json:"protocol"`
InIface string `json:"inIface"`
OutIface string `json:"outIface"`
Opt string `json:"opt"`
Source string `json:"source"`
Destination string `json:"destination"`
SrcPort string `json:"srcPort"`
DestPort string `json:"destPort"`
}
type IptablesFilterInfo 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"`
}

View File

@@ -3,252 +3,404 @@ package client
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
const (
PreRoutingChain = "1PANEL_PREROUTING"
PostRoutingChain = "1PANEL_POSTROUTING"
ForwardChain = "1PANEL_FORWARD"
)
var portRuleRegex = regexp.MustCompile(`-A\s+INPUT\s+-p\s+(\w+)(?:\s+-m\s+\w+)*\s+--dport\s+(\d+(?::\d+)?)\s+-j\s+(\w+)`)
var addressRuleRegex = regexp.MustCompile(`-A\s+(INPUT|OUTPUT)\s+-s\s+(\S+)\s+-j\s+(\w+)`)
const (
FilterTab = "filter"
NatTab = "nat"
)
const NatChain = "1PANEL"
var (
natListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`)
)
type Iptables struct {
CmdStr string
}
type Iptables struct{}
func NewIptables() (*Iptables, error) {
iptables := new(Iptables)
iptables.CmdStr = cmd.SudoHandleCmd()
return iptables, nil
return &Iptables{}, nil
}
func (iptables *Iptables) out(tab, rule string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -t %s %s", iptables.CmdStr, tab, rule)
func (i *Iptables) Name() string {
return "iptables"
}
func (i *Iptables) Status() (bool, error) {
stdout, err := cmd.RunDefaultWithStdoutBashC("iptables -L -n | head -1")
if err != nil {
global.LOG.Errorf("iptables failed, %v", err)
return false, err
}
return stdout, nil
return strings.Contains(stdout, "Chain"), nil
}
func (iptables *Iptables) run(tab, rule string) error {
if _, err := iptables.out(tab, rule); err != nil {
return err
}
func (i *Iptables) Start() error {
return nil
}
func (iptables *Iptables) Check() error {
stdout, err := cmd.RunDefaultWithStdoutBashC("cat /proc/sys/net/ipv4/ip_forward")
if err != nil {
return fmt.Errorf("check ip_forward failed, %v", err)
}
if strings.TrimSpace(stdout) == "0" {
return fmt.Errorf("ipv4 forward disabled")
}
chain, err := iptables.out(NatTab, fmt.Sprintf("-L -n | grep 'Chain %s'", PreRoutingChain))
if err != nil {
return fmt.Errorf("failed to check chain: %w", err)
}
if strings.TrimSpace(chain) != "" {
return fmt.Errorf("chain %s already exists", PreRoutingChain)
}
func (i *Iptables) Stop() error {
return nil
}
func (iptables *Iptables) NewChain(tab, chain string) error {
return iptables.run(tab, "-N "+chain)
func (i *Iptables) Restart() error {
return nil
}
func (iptables *Iptables) AppendChain(tab string, chain, chain1 string) error {
return iptables.run(tab, fmt.Sprintf("-A %s -j %s", chain, chain1))
func (i *Iptables) Reload() error {
return nil
}
func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) {
if len(chain) == 0 {
chain = append(chain, PreRoutingChain)
}
stdout, err := iptables.out(NatTab, fmt.Sprintf("-nvL %s --line-numbers", chain[0]))
func (i *Iptables) Version() (string, error) {
stdout, err := cmd.RunDefaultWithStdoutBashC("iptables --version")
if err != nil {
return nil, err
return "", fmt.Errorf("failed to get iptables version: %w", err)
}
parts := strings.Fields(stdout)
if len(parts) >= 2 {
return strings.TrimPrefix(parts[1], "v"), nil
}
return strings.TrimSpace(stdout), nil
}
func (i *Iptables) ListPort() ([]FireInfo, error) {
stdout, err := iptables.RunWithStd(iptables.FilterTab, fmt.Sprintf("-S %s", iptables.Chain1PanelBasic))
if err != nil {
return nil, fmt.Errorf("failed to list 1PANEL_BASIC rules: %w", 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[13], ":") {
match[13] = fmt.Sprintf(":%s", match[13])
var datas []FireInfo
lines := strings.Split(stdout, "\n")
chainPortRegex := regexp.MustCompile(fmt.Sprintf(`-A\s+%s\s+(?:-s\s+(\S+)\s+)?-p\s+(\w+)(?:\s+-m\s+\w+)*\s+--dport\s+(\d+(?::\d+)?)\s+-j\s+(\w+)`, iptables.Chain1PanelBasic))
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, fmt.Sprintf("-A %s", iptables.Chain1PanelBasic)) {
continue
}
if matches := chainPortRegex.FindStringSubmatch(line); len(matches) == 5 {
address := matches[1]
protocol := matches[2]
port := strings.ReplaceAll(matches[3], ":", "-")
action := strings.ToLower(matches[4])
strategy := "accept"
if action == "drop" || action == "reject" {
strategy = "drop"
}
forwardList = append(forwardList, IptablesNatInfo{
Num: match[1],
Target: match[4],
Protocol: match[11],
InIface: match[7],
OutIface: match[8],
Opt: match[6],
Source: match[9],
Destination: match[10],
SrcPort: match[12],
DestPort: match[13],
datas = append(datas, FireInfo{
Address: address,
Protocol: protocol,
Port: port,
Strategy: strategy,
Family: "ipv4",
})
}
}
return forwardList, nil
return datas, nil
}
func (iptables *Iptables) NatAdd(protocol, srcPort, dest, destPort, iface string, save bool) error {
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
iptablesArg := fmt.Sprintf("-A %s", PreRoutingChain)
if iface != "" {
iptablesArg += fmt.Sprintf(" -i %s", iface)
}
iptablesArg += fmt.Sprintf(" -p %s --dport %s -j DNAT --to-destination %s:%s", protocol, srcPort, dest, destPort)
if err := iptables.run(NatTab, iptablesArg); err != nil {
return err
func (i *Iptables) ListAddress() ([]FireInfo, error) {
stdout, err := iptables.RunWithStd(iptables.FilterTab, fmt.Sprintf("-S %s", iptables.Chain1PanelBasic))
if err != nil {
return nil, fmt.Errorf("failed to list 1PANEL_BASIC address rules: %w", err)
}
var datas []FireInfo
lines := strings.Split(stdout, "\n")
addressMap := make(map[string]FireInfo)
chainAddressRegex := regexp.MustCompile(fmt.Sprintf(`-A\s+%s\s+(?:-s\s+(\S+)|(?:-d\s+(\S+)))?\s+-j\s+(\w+)`, iptables.Chain1PanelBasic))
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, fmt.Sprintf("-A %s", iptables.Chain1PanelBasic)) {
continue
}
if err := iptables.run(NatTab, fmt.Sprintf(
"-A %s -d %s -p %s --dport %s -j MASQUERADE",
PostRoutingChain,
dest,
protocol,
destPort,
)); err != nil {
return err
}
if matches := chainAddressRegex.FindStringSubmatch(line); len(matches) >= 4 {
address := matches[1]
if address == "" {
address = matches[2]
}
if address == "" {
continue
}
action := strings.ToLower(matches[3])
if err := iptables.run(FilterTab, fmt.Sprintf(
"-A %s -d %s -p %s --dport %s -j ACCEPT",
ForwardChain,
dest,
protocol,
destPort,
)); err != nil {
return err
}
strategy := "accept"
if action == "drop" || action == "reject" {
strategy = "drop"
}
if err := iptables.run(FilterTab, fmt.Sprintf(
"-A %s -s %s -p %s --sport %s -j ACCEPT",
ForwardChain,
dest,
protocol,
destPort,
)); err != nil {
if _, exists := addressMap[address]; !exists {
addressMap[address] = FireInfo{
Address: address,
Strategy: strategy,
Family: "ipv4",
}
}
}
}
for _, info := range addressMap {
datas = append(datas, info)
}
return datas, nil
}
func (i *Iptables) Port(port FireInfo, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
portSpec, err := normalizePortSpec(port.Port)
if err != nil {
return err
}
protocol := port.Protocol
if protocol == "" {
protocol = "tcp"
}
action := "ACCEPT"
if port.Strategy == "drop" {
action = "DROP"
}
ruleArgs := []string{fmt.Sprintf("-p %s", protocol)}
if protocol == "tcp" || protocol == "udp" {
ruleArgs = append(ruleArgs, fmt.Sprintf("-m %s", protocol))
}
ruleArgs = append(ruleArgs, fmt.Sprintf("--dport %s", portSpec), fmt.Sprintf("-j %s", action))
ruleSpec := strings.Join(ruleArgs, " ")
if operation == "add" {
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasic, ruleSpec); err != nil {
return err
}
} else {
iptablesArg := fmt.Sprintf("-A %s", PreRoutingChain)
if iface != "" {
iptablesArg += fmt.Sprintf(" -i %s", iface)
}
iptablesArg += fmt.Sprintf(" -p %s --dport %s -j REDIRECT --to-port %s", protocol, srcPort, destPort)
if err := iptables.run(NatTab, iptablesArg); err != nil {
if err := iptables.DeleteRule(iptables.FilterTab, iptables.Chain1PanelBasic, ruleSpec); err != nil {
return err
}
}
if save {
return global.DB.Save(&model.Forward{
Protocol: protocol,
Port: srcPort,
TargetIP: dest,
TargetPort: destPort,
Interface: iface,
}).Error
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
}
return nil
}
func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPort, iface string) error {
if err := iptables.run(NatTab, fmt.Sprintf("-D %s %s", PreRoutingChain, num)); err != nil {
return err
func (i *Iptables) RichRules(rule FireInfo, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
if err := iptables.run(NatTab, fmt.Sprintf(
"-D %s -d %s -p %s --dport %s -j MASQUERADE",
PostRoutingChain,
dest,
protocol,
destPort,
)); err != nil {
address := strings.TrimSpace(rule.Address)
if strings.EqualFold(address, "Anywhere") {
address = ""
}
action := "ACCEPT"
if rule.Strategy == "drop" {
action = "DROP"
}
var ruleArgs []string
if address != "" {
ruleArgs = append(ruleArgs, fmt.Sprintf("-s %s", address))
}
protocol := strings.TrimSpace(rule.Protocol)
if rule.Port != "" && protocol == "" {
protocol = "tcp"
}
if protocol != "" {
ruleArgs = append(ruleArgs, fmt.Sprintf("-p %s", protocol))
}
if rule.Port != "" {
portSegment, err := normalizePortSpec(rule.Port)
if err != nil {
return err
}
if protocol == "" {
return fmt.Errorf("protocol is required when specifying a port")
}
if protocol == "tcp" || protocol == "udp" {
ruleArgs = append(ruleArgs, fmt.Sprintf("-m %s", protocol))
}
ruleArgs = append(ruleArgs, fmt.Sprintf("--dport %s", portSegment))
}
if err := iptables.run(FilterTab, fmt.Sprintf(
"-D %s -d %s -p %s --dport %s -j ACCEPT",
ForwardChain,
dest,
protocol,
destPort,
)); err != nil {
ruleArgs = append(ruleArgs, fmt.Sprintf("-j %s", action))
ruleSpec := strings.Join(ruleArgs, " ")
if operation == "add" {
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasic, ruleSpec); err != nil {
return err
}
if err := iptables.run(FilterTab, fmt.Sprintf(
"-D %s -s %s -p %s --sport %s -j ACCEPT",
ForwardChain,
dest,
protocol,
destPort,
)); err != nil {
} else {
if err := iptables.DeleteRule(iptables.FilterTab, iptables.Chain1PanelBasic, ruleSpec); err != nil {
return err
}
}
global.DB.Where(
"protocol = ? AND port = ? AND target_ip = ? AND target_port = ? AND (interface = ? OR (interface IS NULL AND ? = ''))",
protocol,
srcPort,
dest,
destPort,
iface,
iface,
).Delete(&model.Forward{})
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
}
return nil
}
func (iptables *Iptables) Reload() error {
if err := iptables.run(NatTab, "-F "+PreRoutingChain); err != nil {
func (i *Iptables) PortForward(info Forward, operation string) error {
return iptablesPortForward(info, operation)
}
func (i *Iptables) EnableForward() error {
return EnableIptablesForward()
}
func (i *Iptables) ListForward() ([]FireInfo, error) {
return iptablesListForward()
}
func EnableIptablesForward() error {
if err := cmd.RunDefaultBashC("echo 1 > /proc/sys/net/ipv4/ip_forward"); err != nil {
return fmt.Errorf("failed to enable IP forwarding: %w", err)
}
_ = cmd.RunDefaultBashC("grep -q '^net.ipv4.ip_forward' /etc/sysctl.conf || echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf")
_ = cmd.RunDefaultBashC("sysctl -p")
if err := iptables.AddChainWithAppend(iptables.NatTab, "PREROUTING", iptables.Chain1PanelPreRouting); err != nil {
return err
}
if err := iptables.run(NatTab, "-F "+PostRoutingChain); err != nil {
if err := iptables.AddChainWithAppend(iptables.NatTab, "POSTROUTING", iptables.Chain1PanelPostRouting); err != nil {
return err
}
if err := iptables.run(FilterTab, "-F "+ForwardChain); err != nil {
if err := iptables.AddChainWithAppend(iptables.FilterTab, "FORWARD", iptables.Chain1PanelForward); err != nil {
return err
}
var rules []model.Forward
global.DB.Find(&rules)
for _, forward := range rules {
if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, forward.Interface, false); err != nil {
return err
}
}
return nil
}
func iptablesPortForward(info Forward, operation string) error {
if operation != "add" && operation != "remove" {
return buserr.New("ErrCmdIllegal")
}
if info.Protocol == "" || info.Port == "" || info.TargetPort == "" {
return fmt.Errorf("protocol, port, and target port are required")
}
if operation == "add" {
if err := iptables.AddForward(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true); err != nil {
return err
}
forwardPersistence()
}
natList, err := iptables.ListForward(iptables.Chain1PanelPreRouting)
if err != nil {
return fmt.Errorf("failed to list NAT rules: %w", err)
}
for _, nat := range natList {
if nat.Protocol == info.Protocol &&
strings.TrimPrefix(nat.SrcPort, ":") == info.Port &&
strings.TrimPrefix(nat.DestPort, ":") == info.TargetPort {
targetIP := info.TargetIP
if targetIP == "" {
targetIP = "127.0.0.1"
}
if err := iptables.DeleteForward(nat.Num, info.Protocol, info.Port, targetIP, info.TargetPort, info.Interface); err != nil {
return err
}
forwardPersistence()
}
}
return fmt.Errorf("forward rule not found")
}
func forwardPersistence() {
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelForward, iptables.ForwardFileName); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelForward, err)
}
if err := iptables.SaveRulesToFile(iptables.NatTab, iptables.Chain1PanelPreRouting, iptables.ForwardFileName1); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelPreRouting, err)
}
if err := iptables.SaveRulesToFile(iptables.NatTab, iptables.Chain1PanelPostRouting, iptables.ForwardFileName2); err != nil {
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelPostRouting, err)
}
}
func iptablesListForward() ([]FireInfo, error) {
natList, err := iptables.ListForward(iptables.Chain1PanelPreRouting)
if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
}
var datas []FireInfo
for _, nat := range natList {
datas = append(datas, FireInfo{
Num: nat.Num,
Protocol: nat.Protocol,
Port: strings.TrimPrefix(nat.SrcPort, ":"),
TargetIP: nat.Destination,
TargetPort: strings.TrimPrefix(nat.DestPort, ":"),
Interface: nat.InIface,
})
}
return datas, nil
}
func parsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port number: %s", portStr)
}
if port < 1 || port > 65535 {
return 0, fmt.Errorf("port out of range: %d", port)
}
return port, nil
}
func normalizePortSpec(port string) (string, error) {
value := strings.TrimSpace(port)
if value == "" {
return "", fmt.Errorf("port is required")
}
separator := ""
if strings.Contains(value, "-") {
separator = "-"
} else if strings.Contains(value, ":") {
separator = ":"
}
if separator != "" {
parts := strings.Split(value, separator)
if len(parts) != 2 {
return "", fmt.Errorf("invalid port range: %s", port)
}
start, err := parsePort(strings.TrimSpace(parts[0]))
if err != nil {
return "", err
}
end, err := parsePort(strings.TrimSpace(parts[1]))
if err != nil {
return "", err
}
if start > end {
return "", fmt.Errorf("invalid port range: %d-%d", start, end)
}
return fmt.Sprintf("%d:%d", start, end), nil
}
single, err := parsePort(value)
if err != nil {
return "", err
}
return fmt.Sprintf("%d", single), nil
}

View File

@@ -0,0 +1,198 @@
package iptables
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
const (
Chain1PanelPreRouting = "1PANEL_PREROUTING"
Chain1PanelPostRouting = "1PANEL_POSTROUTING"
Chain1PanelForward = "1PANEL_FORWARD"
ChainInput = "INPUT"
ChainOutput = "OUTPUT"
Chain1PanelInput = "1PANEL_INPUT"
Chain1PanelOutput = "1PANEL_OUTPUT"
Chain1PanelBasicBefore = "1PANEL_BASIC_BEFORE"
Chain1PanelBasic = "1PANEL_BASIC"
Chain1PanelBasicAfter = "1PANEL_BASIC_AFTER"
)
const (
EstablishedRule = "-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -m comment --comment 'ESTABLISHED Whitelist'"
IoRuleIn = "-i lo -j ACCEPT -m comment --comment 'Loopback Whitelist'"
DropAll = "-j DROP"
AllowSSH = "-p tcp --dport ssh -j ACCEPT"
)
var (
natListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`)
)
const (
ACCEPT = "ACCEPT"
DROP = "DROP"
REJECT = "REJECT"
ANYWHERE = "anywhere"
)
const (
FilterTab = "filter"
NatTab = "nat"
)
func RunWithStd(tab, rule string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -t %s %s", cmd.SudoHandleCmd(), tab, rule)
if err != nil {
global.LOG.Errorf("iptables command failed [table=%s, rule=%s]: %v", tab, rule, err)
return stdout, err
}
return stdout, nil
}
func RunWithoutIgnrore(tab, rule string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -t %s %s", cmd.SudoHandleCmd(), tab, rule)
if err != nil {
return stdout, err
}
return stdout, nil
}
func Run(tab, rule string) error {
if _, err := RunWithStd(tab, rule); err != nil {
return err
}
return nil
}
func NewChain(tab, chain string) error {
return Run(tab, "-N "+chain)
}
func ClearChain(tab, chain string) error {
return Run(tab, "-F "+chain)
}
func AddRule(tab, chain, rule string) error {
if CheckRuleExist(tab, chain, rule) {
return nil
}
return Run(tab, fmt.Sprintf("-A %s %s", chain, rule))
}
func DeleteRule(tab, chain, rule string) error {
return Run(tab, fmt.Sprintf("-D %s %s", chain, rule))
}
func CheckChainExist(tab, chain string) (bool, error) {
stdout, err := RunWithStd(tab, fmt.Sprintf("-S | grep -w 'N %s'", chain))
if err != nil {
global.LOG.Errorf("check chain %s from tab %s exist failed, err: %v", chain, tab, err)
return false, fmt.Errorf("check chain %s from tab %s exist failed, err: %v", chain, tab, err)
}
if strings.TrimSpace(stdout) == "" {
return false, nil
}
return true, nil
}
func CheckChainBind(tab, parentChain, chain string) (bool, error) {
stdout, err := RunWithStd(tab, fmt.Sprintf("-L %s | grep -w %s", parentChain, chain))
if err != nil {
global.LOG.Errorf("check chain %s from tab %s is bind to %s failed, err: %v", chain, tab, parentChain, err)
return false, fmt.Errorf("check chain %s from tab %s is bind to %s failed, err: %v", chain, tab, parentChain, err)
}
if strings.TrimSpace(stdout) == "" {
return false, nil
}
return true, nil
}
func CheckRuleExist(tab, chain, rule string) bool {
_, err := RunWithoutIgnrore(tab, fmt.Sprintf("-C %s %s", chain, rule))
return err == nil
}
func AddChain(tab, chain string) error {
exists, err := CheckChainExist(tab, chain)
if err != nil {
return fmt.Errorf("check chain %s exist from tab %s failed, err: %w", chain, tab, err)
}
if !exists {
if err := NewChain(tab, chain); err != nil {
return fmt.Errorf("add chain %s for tab %s failed, err: %w", tab, chain, err)
}
}
return nil
}
func BindChain(tab, targetChain, chain string, position int) error {
line, err := FindChainNum(tab, targetChain, chain)
if err != nil {
return fmt.Errorf("find chain %s number from %s failed, err: %w", chain, targetChain, err)
}
if line == 0 {
if err := Run(tab, fmt.Sprintf("-I %s %d -j %s", targetChain, position, chain)); err != nil {
return fmt.Errorf("bind chain %s to %s failed, err: %w", chain, targetChain, err)
}
}
return nil
}
func UnbindChain(tab, targetChain, chain string) error {
line, err := FindChainNum(tab, targetChain, chain)
if err != nil {
return fmt.Errorf("find chain %s number from %s failed, err: %w", chain, targetChain, err)
}
if line != 0 {
return Run(tab, fmt.Sprintf("-D %s %v", targetChain, line))
}
return nil
}
func FindChainNum(tab, targetChain, chain string) (int, error) {
stdout, err := RunWithStd(tab, fmt.Sprintf("-L %s --line-numbers -n | grep -w %s", targetChain, chain))
if err != nil {
return 0, fmt.Errorf("failed to list rules in chain %s: %w", targetChain, err)
}
lineItem := strings.TrimSpace(stdout)
lines := strings.Split(lineItem, "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
if fields[1] == chain {
itemNum, err := strconv.Atoi(fields[0])
return itemNum, err
}
}
return 0, nil
}
func AddChainWithAppend(tab, parentChain, chain string) error {
exists, err := CheckChainExist(tab, chain)
if err != nil {
return fmt.Errorf("failed to check chain %s: %w", chain, err)
}
if !exists {
if err := NewChain(tab, chain); err != nil {
return fmt.Errorf("failed to create chain %s: %w", chain, err)
}
}
isBind, err := CheckChainBind(tab, parentChain, chain)
if err != nil {
return fmt.Errorf("check chain %s bind to %s failed, err: %w", parentChain, chain, err)
}
if !isBind {
if err := AppendChain(tab, parentChain, chain); err != nil {
return fmt.Errorf("failed to append %s to %s: %w", chain, parentChain, err)
}
}
return nil
}
func AppendChain(tab string, parentChain, chain string) error {
return Run(tab, fmt.Sprintf("-A %s -j %s", parentChain, chain))
}

View File

@@ -0,0 +1,155 @@
package iptables
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type FilterRules struct {
ID uint `json:"id"`
Protocol string `json:"protocol"`
SrcPort uint `json:"srcPort"`
DstPort uint `json:"dstPort"`
SrcIP string `json:"srcIP"`
DstIP string `json:"dstIP"`
Strategy string `json:"strategy"`
Description string `json:"description"`
}
func AddFilterRule(chain string, policy FilterRules) error {
if err := validateRuleSafety(policy, chain); err != nil {
return err
}
iptablesArg := fmt.Sprintf("-A %s", chain)
if policy.Protocol != "" {
iptablesArg += fmt.Sprintf(" -p %s", policy.Protocol)
}
if policy.SrcPort != 0 {
iptablesArg += fmt.Sprintf(" --sport %d", policy.SrcPort)
}
if policy.DstPort != 0 {
iptablesArg += fmt.Sprintf(" --dport %d", policy.DstPort)
}
if policy.SrcIP != "" {
iptablesArg += fmt.Sprintf(" -s %s", policy.SrcIP)
}
if policy.DstIP != "" {
iptablesArg += fmt.Sprintf(" -d %s", policy.DstIP)
}
iptablesArg += fmt.Sprintf(" -j %s", policy.Strategy)
return Run(FilterTab, iptablesArg)
}
func DeleteFilterRule(chain string, policy FilterRules) error {
iptablesArg := fmt.Sprintf("-D %s", chain)
if policy.Protocol != "" {
iptablesArg += fmt.Sprintf(" -p %s", policy.Protocol)
}
if policy.SrcPort != 0 {
iptablesArg += fmt.Sprintf(" --sport %d", policy.SrcPort)
}
if policy.DstPort != 0 {
iptablesArg += fmt.Sprintf(" --dport %d", policy.DstPort)
}
if policy.SrcIP != "" {
iptablesArg += fmt.Sprintf(" -s %s", policy.SrcIP)
}
if policy.DstIP != "" {
iptablesArg += fmt.Sprintf(" -d %s", policy.DstIP)
}
iptablesArg += fmt.Sprintf(" -j %s", policy.Strategy)
return Run(FilterTab, iptablesArg)
}
func ReadFilterRulesByChain(chain string) ([]FilterRules, error) {
var rules []FilterRules
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -t %s -L %s", cmd.SudoHandleCmd(), FilterTab, chain)
if err != nil {
return rules, fmt.Errorf("load filter fules by chain %s failed, %v", chain, err)
}
lines := strings.Split(stdout, "\n")
for i := 0; i < len(lines); i++ {
fields := strings.Fields(lines[i])
if len(fields) < 7 {
continue
}
itemRule := FilterRules{
Protocol: fields[1],
SrcPort: loadPort("src", fields[6]),
DstPort: loadPort("dst", fields[6]),
SrcIP: loadIP(fields[3]),
DstIP: loadIP(fields[4]),
Strategy: fields[0],
}
rules = append(rules, itemRule)
}
return rules, nil
}
func LoadDefaultStrategy(chain string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -t %s -L %s", cmd.SudoHandleCmd(), FilterTab, chain)
if err != nil {
return "", fmt.Errorf("load filter fules by chain %s failed, %v", chain, err)
}
lines := strings.Split(stdout, "\n")
for i := len(lines) - 1; i > 0; i-- {
fields := strings.Fields(lines[i])
if len(fields) < 5 {
continue
}
if fields[0] == "DROP" && fields[1] == "all" && fields[3] == ANYWHERE && fields[4] == ANYWHERE {
return DROP, nil
}
}
return ACCEPT, nil
}
func loadPort(position, portStr string) uint {
var portItem string
if strings.Contains(portStr, "spt:") && position == "src" {
portItem = strings.ReplaceAll(portStr, "spt:", "")
}
if strings.Contains(portStr, "dpt:") && position == "dst" {
portItem = strings.ReplaceAll(portStr, "dpt:", "")
}
if len(portItem) == 0 {
return 0
}
port, _ := strconv.Atoi(portItem)
return uint(port)
}
func loadIP(ipStr string) string {
if ipStr == ANYWHERE {
return ""
}
return ipStr
}
func validateRuleSafety(rule FilterRules, chain string) error {
if strings.ToUpper(rule.Strategy) != "DROP" {
return nil
}
if chain == ChainInput || chain == Chain1PanelInput || chain == Chain1PanelBasic {
if rule.SrcIP == "0.0.0.0/0" && rule.SrcPort == 0 && rule.DstPort == 0 {
return fmt.Errorf("unsafe DROP is not allowed")
}
}
if chain == ChainOutput || chain == Chain1PanelOutput || chain == Chain1PanelBasicAfter {
if rule.DstIP == "0.0.0.0/0" && rule.DstPort == 0 && rule.SrcPort == 0 {
return fmt.Errorf("unsafe DROP is not allowed")
}
}
return nil
}

View File

@@ -0,0 +1,112 @@
package iptables
import (
"fmt"
"strings"
)
func AddForward(protocol, srcPort, dest, destPort, iface string, save bool) error {
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
iptablesArg := fmt.Sprintf("-A %s", Chain1PanelPreRouting)
if iface != "" {
iptablesArg += fmt.Sprintf(" -i %s", iface)
}
iptablesArg += fmt.Sprintf(" -p %s --dport %s -j DNAT --to-destination %s:%s", protocol, srcPort, dest, destPort)
if err := Run(NatTab, iptablesArg); err != nil {
return err
}
if err := Run(NatTab, fmt.Sprintf("-A %s -d %s -p %s --dport %s -j MASQUERADE", Chain1PanelPostRouting, dest, protocol, destPort)); err != nil {
return err
}
if err := Run(FilterTab, fmt.Sprintf("-A %s -d %s -p %s --dport %s -j ACCEPT", Chain1PanelForward, dest, protocol, destPort)); err != nil {
return err
}
if err := Run(FilterTab, fmt.Sprintf("-A %s -s %s -p %s --sport %s -j ACCEPT", Chain1PanelForward, dest, protocol, destPort)); err != nil {
return err
}
} else {
iptablesArg := fmt.Sprintf("-A %s", Chain1PanelPreRouting)
if iface != "" {
iptablesArg += fmt.Sprintf(" -i %s", iface)
}
iptablesArg += fmt.Sprintf(" -p %s --dport %s -j REDIRECT --to-port %s", protocol, srcPort, destPort)
if err := Run(NatTab, iptablesArg); err != nil {
return err
}
}
return nil
}
func DeleteForward(num string, protocol, srcPort, dest, destPort, iface string) error {
if err := Run(NatTab, fmt.Sprintf("-D %s %s", Chain1PanelPreRouting, num)); err != nil {
return err
}
if dest != "" && dest != "127.0.0.1" && dest != "localhost" {
if err := Run(NatTab, fmt.Sprintf("-D %s -d %s -p %s --dport %s -j MASQUERADE", Chain1PanelPostRouting, dest, protocol, destPort)); err != nil {
return err
}
if err := Run(FilterTab, fmt.Sprintf("-D %s -d %s -p %s --dport %s -j ACCEPT", Chain1PanelForward, dest, protocol, destPort)); err != nil {
return err
}
if err := Run(FilterTab, fmt.Sprintf("-D %s -s %s -p %s --sport %s -j ACCEPT", Chain1PanelForward, dest, protocol, destPort)); err != nil {
return err
}
}
return nil
}
func ListForward(chain ...string) ([]IptablesNatInfo, error) {
if len(chain) == 0 {
chain = append(chain, Chain1PanelPreRouting)
}
stdout, err := RunWithStd(NatTab, fmt.Sprintf("-nvL %s --line-numbers", chain[0]))
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[13], ":") {
match[13] = fmt.Sprintf(":%s", match[13])
}
forwardList = append(forwardList, IptablesNatInfo{
Num: match[1],
Target: match[4],
Protocol: match[11],
InIface: match[7],
OutIface: match[8],
Opt: match[6],
Source: match[9],
Destination: match[10],
SrcPort: match[12],
DestPort: match[13],
})
}
}
return forwardList, nil
}
type IptablesNatInfo struct {
Num string `json:"num"`
Target string `json:"target"`
Protocol string `json:"protocol"`
InIface string `json:"inIface"`
OutIface string `json:"outIface"`
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,103 @@
package iptables
import (
"bufio"
"fmt"
"os"
"path"
"strings"
"github.com/1Panel-dev/1Panel/agent/global"
)
const (
BasicFileName = "1panel_basic.rules"
InputFileName = "1panel_input.rules"
OutputFileName = "1panel_out.rules"
ForwardFileName = "1panel_forward.rules"
ForwardFileName1 = "1panel_forward_pre.rules"
ForwardFileName2 = "1panel_forward_post.rules"
)
func SaveRulesToFile(tab, chain, fileName string) error {
rulesFile := path.Join(global.Dir.FirewallDir, fileName)
stdout, err := RunWithStd(tab, fmt.Sprintf("-S %s", chain))
if err != nil {
return fmt.Errorf("failed to list %s rules: %w", chain, err)
}
var rules []string
lines := strings.Split(stdout, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, fmt.Sprintf("-A %s", chain)) {
rules = append(rules, line)
}
}
file, err := os.Create(rulesFile)
if err != nil {
return fmt.Errorf("failed to create rules file: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, rule := range rules {
_, err := writer.WriteString(rule + "\n")
if err != nil {
return fmt.Errorf("failed to write rule to file: %w", err)
}
}
if err := writer.Flush(); err != nil {
return fmt.Errorf("failed to flush rules to file: %w", err)
}
global.LOG.Infof("persistence rules to %s successful", rulesFile)
return nil
}
func LoadRulesFromFile(tab, chain, fileName string) error {
rulesFile := path.Join(global.Dir.FirewallDir, fileName)
if _, err := os.Stat(rulesFile); os.IsNotExist(err) {
return nil
}
file, err := os.Open(rulesFile)
if err != nil {
return fmt.Errorf("failed to open rules file: %w", err)
}
defer file.Close()
var rules []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
rules = append(rules, line)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to read rules file: %w", err)
}
if err := ClearChain(tab, chain); err != nil {
global.LOG.Warnf("Failed to clear existing rules from %s: %v", chain, err)
}
appliedCount := 0
for _, rule := range rules {
if strings.HasPrefix(rule, fmt.Sprintf("-A %s", chain)) {
ruleArgs := strings.TrimPrefix(rule, "-A ")
if err := Run(tab, "-A "+ruleArgs); err != nil {
global.LOG.Errorf("Failed to apply rule '%s': %v", rule, err)
continue
}
appliedCount++
}
}
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -97,40 +96,6 @@ func (f *Ufw) ListPort() ([]FireInfo, error) {
return datas, nil
}
func (f *Ufw) ListForward() ([]FireInfo, error) {
if err := f.EnableForward(); err != nil {
global.LOG.Errorf("init port forward failed, err: %v", err)
}
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.Split(rule.DestPort, ":")
if len(dest) < 2 {
continue
}
if len(dest[0]) == 0 {
dest[0] = "127.0.0.1"
}
list = append(list, FireInfo{
Num: rule.Num,
Protocol: rule.Protocol,
Interface: rule.InIface,
Port: rule.SrcPort,
TargetIP: dest[0],
TargetPort: dest[1],
})
}
return list, nil
}
func (f *Ufw) ListAddress() ([]FireInfo, error) {
stdout, err := cmd.RunDefaultWithStdoutBashCf("%s status verbose", f.CmdStr)
if err != nil {
@@ -232,20 +197,15 @@ func (f *Ufw) RichRules(rule FireInfo, operation string) error {
}
func (f *Ufw) PortForward(info Forward, operation string) error {
iptables, err := NewIptables()
if err != nil {
return err
}
return iptablesPortForward(info, operation)
}
if operation == "add" {
err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true)
} else {
err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface)
}
if err != nil {
return fmt.Errorf("%s port forward failed, err: %s", operation, err)
}
return nil
func (f *Ufw) EnableForward() error {
return EnableIptablesForward()
}
func (f *Ufw) ListForward() ([]FireInfo, error) {
return iptablesListForward()
}
func (f *Ufw) loadInfo(line string, fireType string) FireInfo {
@@ -292,39 +252,3 @@ func (f *Ufw) loadInfo(line string, fireType string) FireInfo {
return itemInfo
}
func (f *Ufw) EnableForward() error {
iptables, err := NewIptables()
if err != nil {
return err
}
if err = iptables.Check(); err != nil {
return err
}
_ = iptables.NewChain(NatTab, PreRoutingChain)
_ = iptables.NewChain(NatTab, PostRoutingChain)
_ = iptables.NewChain(FilterTab, ForwardChain)
if err = f.enableChain(iptables); err != nil {
return err
}
return iptables.Reload()
}
func (f *Ufw) enableChain(iptables *Iptables) error {
rules, err := iptables.NatList("PREROUTING")
if err != nil {
return err
}
for _, rule := range rules {
if rule.Target == PreRoutingChain {
return nil
}
}
_ = iptables.AppendChain(NatTab, "PREROUTING", PreRoutingChain)
_ = iptables.AppendChain(NatTab, "POSTROUTING", PostRoutingChain)
_ = iptables.AppendChain(FilterTab, "FORWARD", ForwardChain)
return nil
}

View File

@@ -4,8 +4,6 @@ import (
"crypto/tls"
"encoding/gob"
"fmt"
"github.com/1Panel-dev/1Panel/core/init/proxy"
"github.com/gin-gonic/gin"
"net"
"net/http"
"os"
@@ -15,7 +13,9 @@ import (
"github.com/1Panel-dev/1Panel/core/init/geo"
"github.com/1Panel-dev/1Panel/core/init/log"
"github.com/1Panel-dev/1Panel/core/init/migration"
"github.com/1Panel-dev/1Panel/core/init/proxy"
"github.com/1Panel-dev/1Panel/core/init/run"
"github.com/gin-gonic/gin"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
@@ -40,6 +40,9 @@ func Start() {
gob.Register(psession.SessionUser{})
cron.Init()
session.Init()
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode)
}
hook.Init()
InitOthers()

View File

@@ -68,6 +68,8 @@ export namespace Host {
name: string;
isExist: boolean;
isActive: boolean;
isInit: boolean;
isBind: boolean;
version: string;
pingStatus: string;
}
@@ -263,4 +265,41 @@ export namespace Host {
path: string;
error: string;
}
// Iptables Filter
export interface IptablesFilterRuleSearch extends ReqPage {
info: string;
type: string;
}
export interface IptablesData {
items: IptablesRules[];
total: number;
defaultStrategy: string;
}
export interface IptablesRules {
id: number;
protocol: string;
srcPort: number;
dstPort: number;
srcIP: string;
dstIP: string;
strategy: string;
description: string;
}
export interface ChainStatus {
isBind: boolean;
defaultStrategy: string;
}
export interface IptablesFilterRuleOp {
operation: string;
id?: number;
chain: string;
protocol: string;
srcIP?: string;
srcPort?: number;
dstIP?: string;
dstPort?: number;
strategy: string;
description?: string;
}
}

View File

@@ -6,8 +6,8 @@ import { deepCopy } from '@/utils/util';
import { Base64 } from 'js-base64';
// firewall
export const loadFireBaseInfo = () => {
return http.get<Host.FirewallBase>(`/hosts/firewall/base`);
export const loadFireBaseInfo = (tab: string) => {
return http.post<Host.FirewallBase>(`/hosts/firewall/base`, { name: tab }, TimeoutEnum.T_40S);
};
export const searchFireRule = (params: Host.RuleSearch) => {
return http.post<ResPage<Host.RuleInfo>>(`/hosts/firewall/search`, params, TimeoutEnum.T_40S);
@@ -44,6 +44,23 @@ export const batchOperateRule = (params: Host.BatchRule) => {
return http.post(`/hosts/firewall/batch`, params, TimeoutEnum.T_60S);
};
// Iptables Filter
export const searchFilterRules = (params: Host.IptablesFilterRuleSearch) => {
return http.post<Host.IptablesData>(`/hosts/firewall/filter/rule/search`, params);
};
export const loadChainStatus = (name: string) => {
return http.post<Host.ChainStatus>(`/hosts/firewall/filter/chain/status`, { name: name }, TimeoutEnum.T_60S);
};
export const operateFilterRule = (params: Host.IptablesFilterRuleOp) => {
return http.post(`/hosts/firewall/filter/rule/operate`, params, TimeoutEnum.T_40S);
};
export const batchOperateFilterRule = (params: { rules: Host.IptablesFilterRuleOp[] }) => {
return http.post(`/hosts/firewall/filter/rule/batch`, params, TimeoutEnum.T_40S);
};
export const operateFilterChain = (name: string, op: string) => {
return http.post(`/hosts/firewall/filter/operate`, { name: name, operate: op }, TimeoutEnum.T_60S);
};
// monitors
export const loadMonitor = (param: Host.MonitorSearch) => {
return http.post<Array<Host.MonitorData>>(`/hosts/monitor/search`, param);

View File

@@ -374,6 +374,7 @@ const message = {
config: 'Configuration | Configurations',
ssh: 'SSH Settings',
firewall: 'Firewall',
filter: 'Filter',
ssl: 'Certificate | Certificates',
database: 'Database | Databases',
aiTools: 'AI',
@@ -2845,6 +2846,8 @@ const message = {
firewall: {
create: 'Create rule',
edit: 'Edit rule',
advancedControl: 'Advanced Control',
advancedControlNotAvailable: 'Currently using {0} firewall, advanced rules only support iptables',
ccDeny: 'CC Protection',
ipWhiteList: 'IP allowlist',
ipBlockList: 'IP blocklist',
@@ -2885,7 +2888,7 @@ const message = {
portHelper2: 'Range port, e.g. 8080-8089',
changeStrategyHelper:
'Change [{1}] {0} strategy to [{2}]. After setting, {0} will access {2} externally. Do you want to continue?',
portHelper: 'Multiple ports can be entered, e.g. 80,81, or range ports, e.g. 80-88',
strategy: 'Strategy',
accept: 'Accept',
drop: 'Drop',
@@ -2915,6 +2918,37 @@ const message = {
exportHelper: 'About to export {0} firewall rules. Continue?',
importSuccess: 'Successfully imported {0} rules',
importPartialSuccess: 'Import completed: {0} succeeded, {1} failed',
basicStatus: 'Current chain {0} status is unbound. Added firewall rules will take effect after binding!',
baseIptables: 'Iptables Service',
forwardIptables: 'Iptables Port Forwarding Service',
advanceIptables: 'Iptables Advanced Configuration Service',
initMsg: 'About to initialize {0}, continue?',
initHelper:
'Detected that {0} is not initialized. Please click the initialization button in the top status bar to configure!',
bindHelper: 'Bind - Firewall rules will only take effect when the status is bound. Confirm?',
unbindHelper:
'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?',
defaultStrategy: 'Default policy for current chain {0} is {1}',
defaultStrategy2:
'Default policy for current chain {0} is {1}, current status is unbound. Added firewall rules will take effect after binding!',
filterRule: 'Filter Rule',
filterHelper:
'Filter rules allow you to control network traffic at the INPUT/OUTPUT level. Configure carefully to avoid locking the system.',
chain: 'Chain',
targetChain: 'Target Chain',
sourceIP: 'Source IP',
destIP: 'Destination IP',
inboundDirection: 'Inbound Direction',
outboundDirection: 'Outbound Direction',
destPort: 'Destination Port',
action: 'Action',
reject: 'Reject',
sourceIPHelper: 'CIDR format, e.g., 192.168.1.0/24. Leave empty for all addresses',
destIPHelper: 'CIDR format, e.g., 10.0.0.0/8. Leave empty for all addresses',
portHelper: '0 means any port',
allPorts: 'All Ports',
deleteRuleConfirm: 'Will delete {0} rules. Continue?',
},
runtime: {
runtime: 'Runtime',

View File

@@ -2861,7 +2861,7 @@ const message = {
portHelper2: 'Rango de puertos, ej. 8080-8089',
changeStrategyHelper:
'Cambiar estrategia de {0} [{1}] a [{2}]. Después de configurarla, {0} tendrá acceso externo como {2}. ¿Deseas continuar?',
portHelper: 'Se pueden ingresar múltiples puertos, ej. 80,81, o rangos, ej. 80-88',
strategy: 'Estrategia',
accept: 'Aceptar',
drop: 'Rechazar',
@@ -2891,6 +2891,39 @@ const message = {
exportHelper: 'A punto de exportar {0} reglas de firewall. ¿Continuar?',
importSuccess: 'Se importaron correctamente {0} reglas',
importPartialSuccess: 'Importación completada: {0} correctas, {1} fallidas',
basicStatus:
'El estado actual de la cadena {0} es no vinculado. ¡Las reglas de firewall agregadas surtirán efecto después de la vinculación!',
baseIptables: 'Servicio Iptables',
forwardIptables: 'Servicio de Reenvío de Puertos Iptables',
advanceIptables: 'Servicio de Configuración Avanzada de Iptables',
initMsg: 'A punto de inicializar {0}, ¿continuar?',
initHelper:
'Se detectó que {0} no está inicializado. ¡Haga clic en el botón de inicialización en la barra de estado superior para configurar!',
bindHelper:
'Vincular: las reglas de firewall solo surtirán efecto cuando el estado esté vinculado. ¿Confirmar?',
unbindHelper:
'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?',
defaultStrategy: 'La política predeterminada para la cadena actual {0} es {1}',
defaultStrategy2:
'La política predeterminada para la cadena actual {0} es {1}, el estado actual es no vinculado. ¡Las reglas de firewall agregadas surtirán efecto después de la vinculación!',
filterRule: 'Regla de Filtro',
filterHelper:
'Las reglas de filtro le permiten controlar el tráfico de red a nivel INPUT/OUTPUT. Configure con cuidado para evitar bloquear el sistema.',
chain: 'Cadena',
targetChain: 'Cadena de Destino',
sourceIP: 'IP de Origen',
destIP: 'IP de Destino',
inboundDirection: 'Dirección de Entrada',
outboundDirection: 'Dirección de Salida',
destPort: 'Puerto de Destino',
action: 'Acción',
reject: 'Rechazar',
sourceIPHelper: 'Formato CIDR, ej. 192.168.1.0/24. Dejar vacío para todas las direcciones',
destIPHelper: 'Formato CIDR, ej. 10.0.0.0/8. Dejar vacío para todas las direcciones',
portHelper: '0 significa cualquier puerto',
allPorts: 'Todos los Puertos',
deleteRuleConfirm: 'Se eliminarán {0} reglas. ¿Continuar?',
},
runtime: {
runtime: 'Runtime',

View File

@@ -2804,7 +2804,7 @@ const message = {
portHelper2: '範囲ポート例えば8080-8089',
changeStrategyHelper:
'[{1}] {0}戦略を[{2}]に変更します設定後{0}は外部から{2}にアクセスします続けたいですか',
portHelper: '複数のポートを入力できます80,81または範囲ポート例えば80-88',
strategy: '戦略',
accept: '受け入れる',
drop: '落とす',
@@ -2834,6 +2834,38 @@ const message = {
exportHelper: '{0} 件のファイアウォールルールをエクスポートします続行しますか',
importSuccess: '{0} 件のルールを正常にインポートしました',
importPartialSuccess: 'インポート完了: {0} 件成功{1} 件失敗',
basicStatus:
'現在のチェーン {0} の状態は未バインドです追加されたファイアウォールルールはバインド後に有効になります',
baseIptables: 'Iptables サービス',
forwardIptables: 'Iptables ポート転送サービス',
advanceIptables: 'Iptables 高度な設定サービス',
initMsg: '{0} を初期化します続行しますか',
initHelper:
'{0} が初期化されていないことを検出しました上部ステータスバーの初期化ボタンをクリックして設定してください',
bindHelper: 'バインド - ファイアウォールルールは状態がバインドされている場合のみ有効になります確認しますか',
unbindHelper:
'アンバインド - アンバインドすると追加されたすべてのファイアウォールルールが無効になります注意して操作してください確認しますか',
defaultStrategy: '現在のチェーン {0} のデフォルトポリシーは {1} です',
defaultStrategy2:
'現在のチェーン {0} のデフォルトポリシーは {1} です現在の状態は未バインドです追加されたファイアウォールルールはバインド後に有効になります',
filterRule: 'フィルタールール',
filterHelper:
'フィルタールールを使用するとINPUT/OUTPUT レベルでネットワークトラフィックを制御できますシステムをロックしないように注意して設定してください',
chain: 'チェーン',
targetChain: 'ターゲットチェーン',
sourceIP: '送信元 IP',
destIP: '宛先 IP',
inboundDirection: 'インバウンド方向',
outboundDirection: 'アウトバウンド方向',
destPort: '宛先ポート',
action: 'アクション',
reject: '拒否',
sourceIPHelper: 'CIDR 形式: 192.168.1.0/24すべてのアドレスの場合は空のまま',
destIPHelper: 'CIDR 形式: 10.0.0.0/8すべてのアドレスの場合は空のまま',
portHelper: '0 は任意のポートを意味します',
allPorts: 'すべてのポート',
deleteRuleConfirm: '{0} 個のルールを削除します続行しますか',
},
runtime: {
runtime: 'ランタイム',

View File

@@ -2755,7 +2755,7 @@ const message = {
portHelper2: '포트 범위, 예: 8080-8089',
changeStrategyHelper:
'[{1}] {0} 전략을 [{2}]로 변경합니다. 설정 후 {0}은(는) {2}로 외부 접근을 허용합니다. 계속하시겠습니까?',
portHelper: '여러 포트를 입력할 수 있습니다. 예: 80, 81 또는 포트 범위, 예: 80-88',
strategy: '전략',
accept: '허용',
drop: '차단',
@@ -2785,6 +2785,36 @@ const message = {
exportHelper: '{0}개의 방화벽 규칙을 내보내려고 합니다. 계속하시겠습니까?',
importSuccess: '{0}개의 규칙을 성공적으로 가져왔습니다',
importPartialSuccess: '가져오기 완료: 성공 {0}건, 실패 {1}건',
basicStatus: '현재 체인 {0} 상태가 바인딩되지 않았습니다. 추가된 방화벽 규칙은 바인딩 후에 효과가 발생합니다!',
baseIptables: 'Iptables 서비스',
forwardIptables: 'Iptables 포트 포워딩 서비스',
advanceIptables: 'Iptables 고급 구성 서비스',
initMsg: '{0}을(를) 초기화하려고 합니다. 계속하시겠습니까?',
initHelper: '{0}이(가) 초기화되지 않았습니다. 상단 상태 표시줄의 초기화 버튼을 클릭하여 구성하세요!',
bindHelper: '바인딩 - 방화벽 규칙은 상태가 바인딩된 경우에만 효과가 있습니다. 확인하시겠습니까?',
unbindHelper:
'바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?',
defaultStrategy: '현재 체인 {0}의 기본 정책은 {1}입니다',
defaultStrategy2:
'현재 체인 {0}의 기본 정책은 {1}입니다. 현재 상태는 바인딩되지 않았습니다. 추가된 방화벽 규칙은 바인딩 후에 효과가 발생합니다!',
filterRule: '필터 규칙',
filterHelper:
'필터 규칙을 사용하면 INPUT/OUTPUT 수준에서 네트워크 트래픽을 제어할 수 있습니다. 시스템 잠금을 방지하기 위해 주의하여 구성하세요.',
chain: '체인',
targetChain: '대상 체인',
sourceIP: '소스 IP',
destIP: '대상 IP',
inboundDirection: '인바운드 방향',
outboundDirection: '아웃바운드 방향',
destPort: '대상 포트',
action: '동작',
reject: '거부',
sourceIPHelper: 'CIDR 형식, 예: 192.168.1.0/24. 모든 주소의 경우 비워 둠',
destIPHelper: 'CIDR 형식, 예: 10.0.0.0/8. 모든 주소의 경우 비워 둠',
portHelper: '0은 모든 포트를 의미합니다',
allPorts: '모든 포트',
deleteRuleConfirm: '{0}개의 규칙을 삭제합니다. 계속하시겠습니까?',
},
runtime: {
runtime: '실행 환경',

View File

@@ -2869,7 +2869,7 @@ const message = {
portHelper2: 'Port rentang, contohnya 8080-8089',
changeStrategyHelper:
'Tukar strategi {0} [{1}] kepada [{2}]. Selepas tetapan, {0} akan mengakses {2} secara luaran. Adakah anda mahu meneruskan?',
portHelper: 'Pelbagai port boleh dimasukkan, contohnya 80,81, atau rentang port, contohnya 80-88',
strategy: 'Strategi',
accept: 'Terima',
drop: 'Lumpuhkan',
@@ -2899,6 +2899,37 @@ const message = {
exportHelper: 'Akan mengeksport {0} peraturan firewall. Teruskan?',
importSuccess: '{0} peraturan berjaya diimport',
importPartialSuccess: 'Import selesai: {0} berjaya, {1} gagal',
basicStatus:
'Status rantaian semasa {0} adalah tidak terikat. Peraturan firewall yang ditambah akan berkuat kuasa selepas pengikatan!',
baseIptables: 'Perkhidmatan Iptables',
forwardIptables: 'Perkhidmatan Penerusan Port Iptables',
advanceIptables: 'Perkhidmatan Konfigurasi Lanjutan Iptables',
initMsg: 'Akan memulakan {0}, teruskan?',
initHelper: 'Mengesan {0} tidak dimulakan. Sila klik butang pemulaan di bar status atas untuk mengkonfigurasi!',
bindHelper: 'Ikat - Peraturan firewall hanya akan berkuat kuasa apabila status terikat. Sahkan?',
unbindHelper:
'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?',
defaultStrategy: 'Dasar lalai untuk rantaian semasa {0} adalah {1}',
defaultStrategy2:
'Dasar lalai untuk rantaian semasa {0} adalah {1}, status semasa adalah tidak terikat. Peraturan firewall yang ditambah akan berkuat kuasa selepas pengikatan!',
filterRule: 'Peraturan Penapis',
filterHelper:
'Peraturan penapis membolehkan anda mengawal trafik rangkaian pada tahap INPUT/OUTPUT. Konfigurasikan dengan berhati-hati untuk mengelakkan mengunci sistem.',
chain: 'Rantai',
targetChain: 'Rantai Sasaran',
sourceIP: 'IP Sumber',
destIP: 'IP Destinasi',
inboundDirection: 'Arah Masuk',
outboundDirection: 'Arah Keluar',
destPort: 'Port Destinasi',
action: 'Tindakan',
reject: 'Tolak',
sourceIPHelper: 'Format CIDR, cth. 192.168.1.0/24. Biarkan kosong untuk semua alamat',
destIPHelper: 'Format CIDR, cth. 10.0.0.0/8. Biarkan kosong untuk semua alamat',
portHelper: '0 bermaksud mana-mana port',
allPorts: 'Semua Port',
deleteRuleConfirm: 'Akan memadam {0} peraturan. Teruskan?',
},
runtime: {
runtime: 'Runtime',

View File

@@ -2874,7 +2874,7 @@ const message = {
portHelper2: 'Faixa de portas, ex.: 8080-8089',
changeStrategyHelper:
'Alterar a estratégia [{1}] {0} para [{2}]. Após a definição, {0} acessará {2} externamente. Deseja continuar?',
portHelper: 'Várias portas podem ser inseridas, ex.: 80,81, ou faixas de portas, ex.: 80-88',
strategy: 'Estratégia',
accept: 'Aceitar',
drop: 'Bloquear',
@@ -2905,6 +2905,39 @@ const message = {
exportHelper: 'Prestes a exportar {0} regras de firewall. Continuar?',
importSuccess: '{0} regras importadas com sucesso',
importPartialSuccess: 'Importação concluída: {0} sucesso, {1} falha',
basicStatus:
'O status atual da cadeia {0} é não vinculado. As regras de firewall adicionadas entrarão em vigor após a vinculação!',
baseIptables: 'Serviço Iptables',
forwardIptables: 'Serviço de Encaminhamento de Porta Iptables',
advanceIptables: 'Serviço de Configuração Avançada do Iptables',
initMsg: 'Prestes a inicializar {0}, continuar?',
initHelper:
'Detectado que {0} não está inicializado. Clique no botão de inicialização na barra de status superior para configurar!',
bindHelper:
'Vincular - As regras de firewall só entrarão em vigor quando o status estiver vinculado. Confirmar?',
unbindHelper:
'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?',
defaultStrategy: 'A política padrão para a cadeia atual {0} é {1}',
defaultStrategy2:
'A política padrão para a cadeia atual {0} é {1}, o status atual é não vinculado. As regras de firewall adicionadas entrarão em vigor após a vinculação!',
filterRule: 'Regra de Filtro',
filterHelper:
'As regras de filtro permitem controlar o tráfego de rede no nível INPUT/OUTPUT. Configure com cuidado para evitar bloquear o sistema.',
chain: 'Cadeia',
targetChain: 'Cadeia de Destino',
sourceIP: 'IP de Origem',
destIP: 'IP de Destino',
inboundDirection: 'Direção de Entrada',
outboundDirection: 'Direção de Saída',
destPort: 'Porta de Destino',
action: 'Ação',
reject: 'Rejeitar',
sourceIPHelper: 'Formato CIDR, ex. 192.168.1.0/24. Deixe vazio para todos os endereços',
destIPHelper: 'Formato CIDR, ex. 10.0.0.0/8. Deixe vazio para todos os endereços',
portHelper: '0 significa qualquer porta',
allPorts: 'Todas as Portas',
deleteRuleConfirm: 'Excluirá {0} regras. Continuar?',
},
runtime: {
runtime: 'Runtime',

View File

@@ -2868,7 +2868,7 @@ const message = {
portHelper2: 'Диапазон портов, например 8080-8089',
changeStrategyHelper:
'Изменить стратегию {0} [{1}] на [{2}]. После установки {0} будет иметь внешний доступ {2}. Хотите продолжить?',
portHelper: 'Можно ввести несколько портов, например 80,81, или диапазон портов, например 80-88',
strategy: 'Стратегия',
accept: 'Принять',
drop: 'Отбросить',
@@ -2899,6 +2899,38 @@ const message = {
exportHelper: 'Собираюсь экспортировать {0} правил брандмауэра. Продолжить?',
importSuccess: 'Успешно импортировано {0} правил',
importPartialSuccess: 'Импорт завершён: {0} успешно, {1} с ошибкой',
basicStatus:
'Текущий статус цепочки {0} - не привязан. Добавленные правила брандмауэра вступят в силу после привязки!',
baseIptables: 'Сервис Iptables',
forwardIptables: 'Сервис Переадресации Порта Iptables',
advanceIptables: 'Сервис Расширенной Конфигурации Iptables',
initMsg: 'Собираюсь инициализировать {0}, продолжить?',
initHelper:
'Обнаружено, что {0} не инициализирован. Нажмите кнопку инициализации в верхней строке состояния для настройки!',
bindHelper: 'Привязать - Правила брандмауэра вступят в силу только когда статус привязан. Подтвердить?',
unbindHelper:
'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?',
defaultStrategy: 'Политика по умолчанию для текущей цепочки {0} - {1}',
defaultStrategy2:
'Политика по умолчанию для текущей цепочки {0} - {1}, текущий статус - не привязан. Добавленные правила брандмауэра вступят в силу после привязки!',
filterRule: 'Правило Фильтра',
filterHelper:
'Правила фильтра позволяют управлять сетевым трафиком на уровне INPUT/OUTPUT. Настраивайте осторожно, чтобы избежать блокировки системы.',
chain: 'Цепочка',
targetChain: 'Целевая Цепочка',
sourceIP: 'Исходный IP',
destIP: 'Целевой IP',
inboundDirection: 'Входящее Направление',
outboundDirection: 'Исходящее Направление',
destPort: 'Целевой Порт',
action: 'Действие',
reject: 'Отклонить',
sourceIPHelper: 'Формат CIDR, напр. 192.168.1.0/24. Оставьте пустым для всех адресов',
destIPHelper: 'Формат CIDR, напр. 10.0.0.0/8. Оставьте пустым для всех адресов',
portHelper: '0 означает любой порт',
allPorts: 'Все Порта',
deleteRuleConfirm: 'Удалит {0} правил. Продолжить?',
},
runtime: {
runtime: 'Среда выполнения',

View File

@@ -2927,7 +2927,6 @@ const message = {
portHelper2: 'Aralık portu, ör. 8080-8089',
changeStrategyHelper:
'[{1}] {0} stratejisini [{2}] olarak değiştirin. Ayar yapıldıktan sonra {0}, dışarıdan {2} erişimi sağlayacak. Devam etmek istiyor musunuz?',
portHelper: 'Birden fazla port girilebilir, ör. 80,81 veya aralık portları, ör. 80-88',
strategy: 'Strateji',
accept: 'Kabul Et',
drop: 'Reddet',
@@ -2957,6 +2956,38 @@ const message = {
exportHelper: '{0} güvenlik duvarı kuralını dışa aktarmak üzere. Devam etmek istiyor musunuz?',
importSuccess: '{0} kural başarıyla içe aktarıldı',
importPartialSuccess: 'İçe aktarma tamamlandı: {0} başarılı, {1} başarısız',
basicStatus:
'Mevcut zincir {0} durumu bağlı değil. Eklenen güvenlik duvarı kuralları bağlandıktan sonra etkili olacak!',
baseIptables: 'Iptables Servisi',
forwardIptables: 'Iptables Port Yönlendirme Servisi',
advanceIptables: 'Iptables Gelişmiş Yapılandırma Servisi',
initMsg: '{0} başlatılmak üzere, devam etmek istiyor musunuz?',
initHelper:
'{0} başlatılmadığı tespit edildi. Lütfen üst durum çubuğundaki başlatma düğmesine tıklayarak yapılandırın!',
bindHelper: 'Bağla - Güvenlik duvarı kuralları yalnızca durum bağlı olduğunda etkili olur. Onaylıyor musunuz?',
unbindHelper:
'Bağlantıyı Kaldır - Bağlantı kaldırıldığında, eklenen tüm güvenlik duvarı kuralları geçersiz olacaktır. Dikkatli ilerleyin. Onaylıyor musunuz?',
defaultStrategy: 'Mevcut zincir {0} için varsayılan politika {1}',
defaultStrategy2:
'Mevcut zincir {0} için varsayılan politika {1}, mevcut durum bağlı değil. Eklenen güvenlik duvarı kuralları bağlandıktan sonra etkili olacak!',
filterRule: 'Filtre Kuralı',
filterHelper:
'Filtre kuralları, INPUT/OUTPUT seviyesinde trafiğini kontrol etmenize izin verir. Sistemi kilitlememek için dikkatli yapılandırın.',
chain: 'Zincir',
targetChain: 'Hedef Zincir',
sourceIP: 'Kaynak IP',
destIP: 'Hedef IP',
inboundDirection: 'Gelen Yön',
outboundDirection: 'Giden Yön',
destPort: 'Hedef Port',
action: 'Eylem',
reject: 'Reddet',
sourceIPHelper: 'CIDR formatı, örn. 192.168.1.0/24. Tüm adresler için boş bırakın',
destIPHelper: 'CIDR formatı, örn. 10.0.0.0/8. Tüm adresler için boş bırakın',
portHelper: '0 herhangi bir port anlamına gelir',
allPorts: 'Tüm Portlar',
deleteRuleConfirm: '{0} kural silinecek. Devam etmek istiyor musunuz?',
},
runtime: {
runtime: 'Çalışma Zamanı',

View File

@@ -2708,6 +2708,33 @@ const message = {
exportHelper: '即將導出 {0} 條防火牆規則是否繼續',
importSuccess: '成功匯入 {0} 條規則',
importPartialSuccess: '匯入完成成功 {0} 失敗 {1} ',
basicStatus: '目前鏈 {0} 狀態為未綁定已新增的防火牆規則需要綁定後生效',
baseIptables: 'Iptables 服務',
forwardIptables: 'Iptables 端口轉發服務',
advanceIptables: 'Iptables 進階設定服務',
initMsg: '即將初始化 {0}, 是否繼續',
initHelper: '偵測到 {0} 未初始化請點選頂部狀態列的初始化按鈕進行設定',
bindHelper: '綁定 僅當狀態為綁定時防火牆規則才能生效是否確認',
unbindHelper: '解除綁定 解除綁定時已新增的所有防火牆規則將失效請謹慎操作是否確認',
defaultStrategy: '目前鏈 {0} 的預設策略為 {1}',
defaultStrategy2: '目前鏈 {0} 的預設策略為 {1}目前狀態為未綁定已新增的防火牆規則需要綁定後生效',
filterRule: 'Filter 規則',
filterHelper: 'Filter 規則允許您在 INPUT/OUTPUT 層級控制網路流量請謹慎設定避免鎖定系統',
chain: '鏈',
targetChain: '目標鏈',
sourceIP: '來源 IP',
destIP: '目標 IP',
inboundDirection: '入站方向',
outboundDirection: '出站方向',
destPort: '目標端口',
action: '動作',
reject: '拒絕',
sourceIPHelper: 'CIDR 格式 192.168.1.0/24留空表示所有地址',
destIPHelper: 'CIDR 格式 10.0.0.0/8留空表示所有地址',
portHelper: '0 表示任意端口',
allPorts: '所有端口',
deleteRuleConfirm: '將刪除 {0} 條規則是否繼續',
},
runtime: {
runtime: '執行環境',

View File

@@ -362,6 +362,7 @@ const message = {
config: '配置',
ssh: 'SSH 管理',
firewall: '防火墙',
filter: '过滤器',
ssl: '证书',
database: '数据库',
aiTools: 'AI',
@@ -2641,6 +2642,8 @@ const message = {
firewall: {
create: '创建规则',
edit: '编辑规则',
advancedControl: '高级控制',
advancedControlNotAvailable: '当前使用 {0} 防火墙高级规则仅支持 iptables',
ccDeny: 'CC 防护',
ipWhiteList: 'IP 白名单',
ipBlockList: 'IP 黑名单',
@@ -2704,6 +2707,33 @@ const message = {
exportHelper: '即将导出 {0} 条防火墙规则是否继续',
importSuccess: '成功导入 {0} 条规则',
importPartialSuccess: '导入完成成功 {0} 失败 {1} ',
basicStatus: '当前链 {0} 状态为未绑定已添加的防火墙规则需要绑定后生效',
baseIptables: 'Iptables 服务',
forwardIptables: 'Iptables 端口转发服务',
advanceIptables: 'Iptables 高级配置服务',
initMsg: '即将初始化 {0}, 是否继续',
initHelper: '检测到 {0} 未初始化请点击顶部状态栏的初始化按钮进行配置',
bindHelper: '绑定 仅当状态为绑定时防火墙规则才能生效是否确认',
unbindHelper: '解绑 解除绑定时已添加的所有防火墙规则将失效请谨慎操作是否确认',
defaultStrategy: '当前链 {0} 的默认策略为 {1}',
defaultStrategy2: '当前链 {0} 的默认策略为 {1}当前状态为未绑定已添加的防火墙规则需要绑定后生效',
filterRule: 'Filter 规则',
filterHelper: 'Filter 规则允许您在 INPUT/OUTPUT 级别控制网络流量请谨慎配置避免锁定系统',
chain: '链',
targetChain: '目标链',
sourceIP: ' IP',
destIP: '目标 IP',
inboundDirection: '入站方向',
outboundDirection: '出站方向',
destPort: '目标端口',
action: '动作',
reject: '拒绝',
sourceIPHelper: 'CIDR 格式 192.168.1.0/24留空表所有地址',
destIPHelper: 'CIDR 格式 10.0.0.0/留空表所有地址',
portHelper: '0 表示任意端口',
allPorts: '所有端口',
deleteRuleConfirm: '将删除 {0} 条规则是否继续',
},
runtime: {
runtime: '运行环境',

View File

@@ -80,6 +80,18 @@ const hostRouter = {
requiresAuth: false,
},
},
{
path: '/hosts/firewall/advance',
name: 'FirewallAdvance',
component: () => import('@/views/host/firewall/advance/index.vue'),
hidden: true,
meta: {
activeMenu: '/hosts/firewall/port',
parent: 'menu.firewall',
title: 'firewall.advancedControl',
requiresAuth: false,
},
},
{
path: '/hosts/disk',
name: 'Disk',

View File

@@ -0,0 +1,337 @@
<template>
<div>
<FireRouter />
<div v-loading="loading">
<FireStatus
ref="fireStatusRef"
@search="search"
v-model:loading="loading"
v-model:mask-show="maskShow"
v-model:is-active="isActive"
v-model:name="fireName"
current-tab="advance"
/>
<div v-if="fireName !== '-' && fireName !== 'iptables'">
<LayoutContent :divider="true">
<template #main>
<div class="app-warn">
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
<span>{{ $t('firewall.advancedControlNotAvailable', [firewallName]) }}</span>
</div>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</template>
</LayoutContent>
</div>
<div v-if="fireName === 'iptables'">
<el-card v-if="!isActive && maskShow" class="mask-prompt">
<span>{{ $t('firewall.firewallNotStart') }}</span>
</el-card>
<LayoutContent :title="$t('firewall.filterRule')" :class="{ mask: !isActive }">
<template #prompt>
<el-alert type="info" :closable="false" :title="loadPrompt()" />
</template>
<template #leftToolBar>
<el-button type="primary" @click="onOpenDialog('create')">
{{ $t('firewall.create') }}
</el-button>
<el-button v-if="isBind" plain @click="onUnBind">
{{ $t('commons.button.unbind') }}
</el-button>
<el-button v-if="!isBind" plain @click="onBind">
{{ $t('commons.button.bind') }}
</el-button>
<el-button @click="onDelete(null)" plain :disabled="selects.length === 0">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<template #rightToolBar>
<el-select v-model="selectedChain" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('firewall.chain') }}</template>
<el-option :label="$t('firewall.inboundDirection')" value="1PANEL_INPUT"></el-option>
<el-option :label="$t('firewall.outboundDirection')" value="1PANEL_OUTPUT"></el-option>
</el-select>
<TableRefresh @search="search()" />
<TableSetting title="firewall-filter-refresh" @search="search()" />
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
@search="search"
:data="data"
:heightDiff="220"
>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.protocol')" :min-width="80" prop="protocol">
<template #default="{ row }">
{{ row.protocol === '' ? 'ALL' : row.protocol }}
</template>
</el-table-column>
<el-table-column :label="$t('firewall.sourceIP')" :min-width="120" prop="srcIP">
<template #default="{ row }">
{{ formatIP(row.srcIP) }}
</template>
</el-table-column>
<el-table-column :label="$t('firewall.sourcePort')" :min-width="100" prop="sourcePort">
<template #default="{ row }">
{{ formatPort(row.srcPort) }}
</template>
</el-table-column>
<el-table-column :label="$t('firewall.destIP')" :min-width="120" prop="dstIP">
<template #default="{ row }">
{{ formatIP(row.dstIP) }}
</template>
</el-table-column>
<el-table-column :label="$t('firewall.destPort')" :min-width="100" prop="dstPort">
<template #default="{ row }">
{{ formatPort(row.dstPort) }}
</template>
</el-table-column>
<el-table-column :min-width="100" :label="$t('firewall.action')" prop="strategy">
<template #default="{ row }">
<el-tag v-if="row.strategy === 'ACCEPT'" type="success">{{ row.strategy }}</el-tag>
<el-tag v-else-if="row.strategy === 'DROP'" type="danger">
{{ row.strategy }}
</el-tag>
<el-tag v-else-if="row.strategy === 'REJECT'" type="warning">
{{ row.strategy }}
</el-tag>
<el-tag v-else type="info">{{ row.strategy }}</el-tag>
</template>
</el-table-column>
<el-table-column
:min-width="150"
:label="$t('commons.table.description')"
prop="description"
show-overflow-tooltip
/>
<fu-table-operations
width="120px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</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 FireStatus from '@/views/host/firewall/status/index.vue';
import OperateDialog from '@/views/host/firewall/advance/operate/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { searchFilterRules, batchOperateFilterRule, loadChainStatus, operateFilterChain } from '@/api/modules/host';
import { Host } from '@/api/interface/host';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const loading = ref();
const selects = ref<any>([]);
const selectedChain = ref('1PANEL_INPUT');
const defaultStrategy = ref('ACCEPT');
const firewallName = ref('');
const maskShow = ref(true);
const isActive = ref(false);
const isBind = ref(false);
const fireName = ref();
const fireStatusRef = ref();
const opRef = ref();
const data = ref();
const formatPort = (port?: number | null | string) => {
if (port === 0 || port === '0') {
return i18n.global.t('firewall.allPorts');
}
if (port === undefined || port === null || port === '') {
return '-';
}
return port;
};
const formatIP = (ip?: null | string) => {
if (ip) {
return ip === 'anywhere' ? i18n.global.t('firewall.anyWhere') : ip;
}
return i18n.global.t('firewall.anyWhere');
};
const paginationConfig = reactive({
cacheSizeKey: 'firewall-filter-page-size',
currentPage: 1,
pageSize: Number(localStorage.getItem('firewall-filter-page-size')) || 20,
total: 0,
});
const search = async () => {
if (!isActive.value) {
loading.value = false;
paginationConfig.total = 0;
return;
}
let params = {
type: selectedChain.value,
info: '',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
loading.value = true;
loadStatus();
await searchFilterRules(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const loadPrompt = () => {
if (isBind.value) {
return i18n.global.t('firewall.defaultStrategy', [selectedChain.value, defaultStrategy.value]);
}
return i18n.global.t('firewall.defaultStrategy2', [selectedChain.value, defaultStrategy.value]);
};
const loadStatus = async () => {
await loadChainStatus(selectedChain.value).then((res) => {
isBind.value = res.data.isBind;
defaultStrategy.value = res.data.defaultStrategy || 'ACCEPT';
});
};
const onBind = async () => {
ElMessageBox.confirm(i18n.global.t('firewall.bindHelper'), i18n.global.t('commons.button.bind'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await operateFilterChain(selectedChain.value, 'bind').then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadStatus();
});
});
};
const onUnBind = async () => {
ElMessageBox.confirm(i18n.global.t('firewall.unbindHelper'), i18n.global.t('commons.button.unbind'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await operateFilterChain(selectedChain.value, 'unbind').then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadStatus();
});
});
};
const dialogRef = ref();
const onOpenDialog = async (title: string, rowData?: Host.IptablesFilterRuleOp) => {
const params = {
title,
rowData: rowData || {
chain: selectedChain.value,
protocol: 'tcp',
strategy: 'ACCEPT',
srcPort: 0,
dstPort: 0,
},
};
dialogRef.value!.acceptParams(params);
};
const onDelete = async (row: Host.IptablesFilterRuleOp | null) => {
let names = [];
let rules = [];
if (row) {
rules.push({
operation: 'remove',
id: row.id,
chain: selectedChain.value,
srcPort: row.srcPort,
dstPort: row.dstPort,
srcIP: row.srcIP === 'anywhere' ? '' : row.srcIP,
dstIP: row.dstIP === 'anywhere' ? '' : row.dstIP,
protocol: row.protocol,
strategy: row.strategy,
});
names = [
`${row.protocol} ${row.srcIP || '*'}:${row.srcPort || '*'} -> ${row.dstIP || '*'}:${row.dstPort || '*'}`,
];
} else {
for (const item of selects.value) {
names.push(
`${item.protocol} ${item.srcIP || '*'}:${item.srcPort || '*'} -> ${item.dstIP || '*'}:${
item.dstPort || '*'
}`,
);
rules.push({
operation: 'remove',
id: item.id,
chain: selectedChain.value,
srcPort: item.srcPort,
dstPort: item.dstPort,
srcIP: item.srcIP === 'anywhere' ? '' : item.srcIP,
dstIP: item.dstIP === 'anywhere' ? '' : item.dstIP,
protocol: item.protocol,
strategy: item.strategy,
});
}
}
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('firewall.deleteRuleConfirm', [rules.length]),
api: batchOperateFilterRule,
params: { rules: rules },
});
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: (row: Host.IptablesFilterRuleOp) => {
onDelete(row);
},
},
];
onMounted(() => {
if (fireName.value !== '-') {
loading.value = true;
fireStatusRef.value.acceptParams();
}
});
</script>
<style lang="scss" scoped>
.chain-card {
.chain-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.chain-policy {
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<DrawerPro v-model="drawerVisible" :header="title" @close="handleClose" size="large">
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules" v-loading="loading">
<el-form-item :label="$t('commons.table.protocol')" prop="protocol">
<el-select class="w-full" v-model="dialogData.rowData!.protocol" @change="changeProtocol">
<el-option value="all" label="all" />
<el-option value="tcp" label="tcp" />
<el-option value="udp" label="udp" />
<el-option value="icmp" label="icmp" />
</el-select>
</el-form-item>
<el-form-item
v-if="dialogData.rowData?.chain === '1PANEL_INPUT'"
:label="$t('firewall.sourceIP')"
prop="srcIP"
>
<el-input clearable v-model.trim="dialogData.rowData!.srcIP" placeholder="0.0.0.0/0" />
<span class="input-help">{{ $t('firewall.sourceIPHelper') }}</span>
</el-form-item>
<el-form-item
v-if="dialogData.rowData?.chain === '1PANEL_OUTPUT'"
:label="$t('firewall.destIP')"
prop="dstIP"
>
<el-input clearable v-model.trim="dialogData.rowData!.dstIP" placeholder="0.0.0.0/0" />
<span class="input-help">{{ $t('firewall.destIPHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('firewall.destPort')" prop="dstPort">
<el-input-number
class="w-full"
v-model="dialogData.rowData!.dstPort"
:min="0"
:max="65535"
:disabled="dialogData.rowData?.protocol === 'all'"
/>
<span class="input-help">{{ $t('firewall.portHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('firewall.action')" prop="strategy">
<el-radio-group v-model="dialogData.rowData!.strategy">
<el-radio value="ACCEPT">{{ $t('firewall.accept') }}</el-radio>
<el-radio value="DROP">{{ $t('firewall.drop') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable v-model.trim="dialogData.rowData!.description" />
</el-form-item>
</el-form>
<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>
</DrawerPro>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import { Host } from '@/api/interface/host';
import { operateFilterRule } from '@/api/modules/host';
import { checkCidr, checkCidrV6, checkIpV4V6 } from '@/utils/util';
const loading = ref();
interface DialogProps {
title: string;
rowData?: Host.IptablesFilterRuleOp;
}
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('firewall.' + dialogData.value.title);
if (dialogData.value.rowData.chain === '1PANEL_INPUT') {
dialogData.value.rowData.dstIP = '';
} else if (dialogData.value.rowData.chain === '1PANEL_OUTPUT') {
dialogData.value.rowData.srcIP = '';
}
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
const rules = reactive({
chain: [Rules.requiredSelect],
protocol: [Rules.requiredSelect],
strategy: [Rules.requiredSelect],
srcIP: [{ validator: checkIPAddress, trigger: 'blur' }],
dstIP: [{ validator: checkIPAddress, trigger: 'blur' }],
});
function checkIPAddress(_rule: any, value: any, callback: any) {
if (!value) {
return callback();
}
if (value.indexOf('/') !== -1) {
if (value.indexOf(':') !== -1) {
if (checkCidrV6(value)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else {
if (checkCidr(value)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
} else {
if (checkIpV4V6(value)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
callback();
}
const changeProtocol = () => {
if (dialogData.value.rowData.protocol === 'all') {
dialogData.value.rowData.srcPort = 0;
dialogData.value.rowData.dstPort = 0;
}
};
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;
dialogData.value.rowData.operation = 'add';
if (!dialogData.value.rowData) return;
loading.value = true;
await operateFilterRule(dialogData.value.rowData)
.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

@@ -10,6 +10,7 @@
v-model:mask-show="maskShow"
v-model:is-active="isActive"
v-model:name="fireName"
current-tab="forward"
/>
<div v-if="fireName !== '-'">
<el-card v-if="!isActive && maskShow" class="mask-prompt">

View File

@@ -23,5 +23,9 @@ const buttons = [
label: i18n.global.t('firewall.ipRule', 2),
path: '/hosts/firewall/ip',
},
{
label: 'iptables ' + i18n.global.t('firewall.advancedControl'),
path: '/hosts/firewall/advance',
},
];
</script>

View File

@@ -10,6 +10,7 @@
v-model:name="fireName"
v-model:mask-show="maskShow"
v-model:is-active="isActive"
current-tab="base"
/>
<div v-if="fireName !== '-'">

View File

@@ -10,6 +10,7 @@
v-model:mask-show="maskShow"
v-model:is-active="isActive"
v-model:name="fireName"
current-tab="base"
/>
<div v-if="fireName !== '-'">
<el-card v-if="!isActive && maskShow" class="mask-prompt">
@@ -33,6 +34,14 @@
</span>
</template>
</el-alert>
<div v-if="!isBind" class="mt-2">
<el-alert
type="info"
:closable="false"
:title="$t('firewall.basicStatus', ['1PANEL_BASIC'])"
/>
</div>
</template>
<template #leftToolBar>
<el-button type="primary" @click="onOpenDialog('create')">
@@ -160,7 +169,13 @@ import OperateDialog from '@/views/host/firewall/port/operate/index.vue';
import ImportDialog from '@/views/host/firewall/port/import/index.vue';
import FireStatus from '@/views/host/firewall/status/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { batchOperateRule, searchFireRule, updateFirewallDescription, updatePortRule } from '@/api/modules/host';
import {
batchOperateRule,
loadChainStatus,
searchFireRule,
updateFirewallDescription,
updatePortRule,
} from '@/api/modules/host';
import { Host } from '@/api/interface/host';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
@@ -177,6 +192,7 @@ const searchStrategy = ref('');
const maskShow = ref(true);
const isActive = ref(false);
const isBind = ref(false);
const fireName = ref();
const fireStatusRef = ref();
@@ -207,6 +223,7 @@ const search = async () => {
pageSize: paginationConfig.pageSize,
};
loading.value = true;
loadStatus();
await searchFireRule(params)
.then((res) => {
loading.value = false;
@@ -289,6 +306,12 @@ const onChange = async (info: any) => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const loadStatus = async () => {
await loadChainStatus('1PANEL_BASIC').then((res) => {
isBind.value = res.data.isBind;
});
};
const onDelete = async (row: Host.RuleInfo | null) => {
let names = [];
let rules = [];

View File

@@ -9,16 +9,33 @@
<el-tag>{{ $t('app.version') }}: {{ baseInfo.version }}</el-tag>
</div>
<div class="mt-0.5">
<el-button type="primary" v-if="baseInfo.isActive" @click="onOperate('stop')" link>
{{ $t('commons.button.stop') }}
</el-button>
<el-button type="primary" v-if="!baseInfo.isActive" @click="onOperate('start')" link>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('commons.button.restart') }}
</el-button>
<template v-if="baseInfo.name !== 'iptables'">
<el-button type="primary" v-if="baseInfo.isActive" @click="onOperate('stop')" link>
{{ $t('commons.button.stop') }}
</el-button>
<el-button type="primary" v-if="!baseInfo.isActive" @click="onOperate('start')" link>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('commons.button.restart') }}
</el-button>
</template>
<template v-if="!baseInfo.isInit">
<el-divider direction="vertical" />
<el-button type="primary" link @click="onInit">
{{ $t('commons.button.init') }}
</el-button>
</template>
<template v-if="baseInfo.isInit && props.currentTab == 'base'">
<el-divider direction="vertical" />
<el-button v-if="baseInfo.isBind" type="primary" link @click="onUnBind">
{{ $t('commons.button.unbind') }}
</el-button>
<el-button v-if="!baseInfo.isBind" type="primary" link @click="onBind">
{{ $t('commons.button.bind') }}
</el-button>
</template>
<span v-if="onPing !== 'None'">
<el-divider direction="vertical" />
<el-button type="primary" link>{{ $t('firewall.noPing') }}</el-button>
@@ -35,7 +52,21 @@
</div>
</el-card>
</div>
<NoSuchService v-if="!baseInfo.isExist" name="Firewalld / Ufw" />
<NoSuchService v-else name="Firewalld / Ufw" />
<LayoutContent :divider="true" v-if="!baseInfo.isInit">
<template #main>
<div class="app-warn">
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
<span>{{ loadInitMsg() }}</span>
</div>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</template>
</LayoutContent>
<DockerRestart
ref="dockerRef"
v-model:withDockerRestart="withDockerRestart"
@@ -51,7 +82,7 @@
<script lang="ts" setup>
import { Host } from '@/api/interface/host';
import { loadFireBaseInfo, operateFire } from '@/api/modules/host';
import { loadFireBaseInfo, operateFilterChain, operateFire } from '@/api/modules/host';
import i18n from '@/lang';
import NoSuchService from '@/components/layout-content/no-such-service.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
@@ -59,7 +90,19 @@ import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { ref } from 'vue';
const baseInfo = ref<Host.FirewallBase>({ isActive: false, isExist: true, name: '', version: '', pingStatus: '' });
const props = defineProps({
currentTab: String,
});
const baseInfo = ref<Host.FirewallBase>({
isActive: false,
isExist: true,
isInit: false,
isBind: false,
name: '',
version: '',
pingStatus: '',
});
const onPing = ref('Disable');
const oldStatus = ref();
const dockerRef = ref();
@@ -72,13 +115,18 @@ const acceptParams = (): void => {
const emit = defineEmits(['search', 'update:is-active', 'update:loading', 'update:maskShow', 'update:name']);
const loadBaseInfo = async (search: boolean) => {
await loadFireBaseInfo()
.then((res) => {
await loadFireBaseInfo(props.currentTab)
.then(async (res) => {
baseInfo.value = res.data;
onPing.value = baseInfo.value.pingStatus;
oldStatus.value = onPing.value;
emit('update:name', baseInfo.value.name);
if (baseInfo.value.isInit) {
emit('update:name', baseInfo.value.name);
} else {
emit('update:name', '-');
}
emit('update:is-active', baseInfo.value.isActive);
if (search) {
emit('search');
} else {
@@ -92,9 +140,81 @@ const loadBaseInfo = async (search: boolean) => {
});
};
const loadInitMsg = () => {
switch (props.currentTab) {
case 'base':
return i18n.global.t('firewall.initHelper', [i18n.global.t('firewall.baseIptables')]);
case 'forward':
return i18n.global.t('firewall.initHelper', [i18n.global.t('firewall.forwardIptables')]);
case 'advance':
return i18n.global.t('firewall.initHelper', [i18n.global.t('firewall.advanceIptables')]);
}
};
const onInit = async () => {
let chainName = '';
let msg = '';
switch (props.currentTab) {
case 'base':
chainName = '1PANEL_BASIC';
msg = i18n.global.t('firewall.initMsg', [i18n.global.t('firewall.baseIptables')]);
case 'forward':
chainName = '1PANEL_FORWARD';
msg = i18n.global.t('firewall.initMsg', [i18n.global.t('firewall.forwardIptables')]);
case 'advance':
chainName = '1PANEL_INPUT';
msg = i18n.global.t('firewall.initMsg', [i18n.global.t('firewall.advanceIptables')]);
}
ElMessageBox.confirm(msg, i18n.global.t('commons.button.init'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await operateFilterChain(chainName, 'init-' + props.currentTab).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
});
});
};
const onBind = async () => {
ElMessageBox.confirm(i18n.global.t('firewall.bindHelper'), i18n.global.t('commons.button.bind'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await operateFilterChain('1PANEL_BASIC', 'bind-base').then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
});
});
};
const onUnBind = async () => {
ElMessageBox.confirm(i18n.global.t('firewall.unbindHelper'), i18n.global.t('commons.button.unbind'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await operateFilterChain('1PANEL_BASIC', 'unbind-base').then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
});
});
};
const onOperate = async (op: string) => {
operation.value = op;
dockerRef.value.acceptParams({ title: i18n.global.t('firewall.dockerRestart') });
if (baseInfo.value.name === 'iptables') {
emit('update:loading', true);
emit('update:maskShow', true);
await operateFire(operation.value, false)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
loadBaseInfo(true);
})
.catch(() => {
loadBaseInfo(true);
});
} else {
dockerRef.value.acceptParams({ title: i18n.global.t('firewall.dockerRestart') });
}
};
const onSubmit = async () => {