From 4a93d93e437b67eea57020ccd6f0e7228a849969 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:45:19 +0800 Subject: [PATCH 01/16] feat: support iptables control --- agent/app/api/v2/entry.go | 11 +- agent/app/api/v2/firewall.go | 111 +++++ agent/app/dto/firewall.go | 49 ++ agent/app/model/firewall.go | 26 + agent/app/repo/iptables_filter_rule.go | 67 +++ agent/app/service/iptables_filter.go | 434 +++++++++++++++++ agent/init/migration/migrations/init.go | 1 + agent/router/ro_host.go | 5 + agent/utils/docker/compose.go | 7 +- agent/utils/firewall/client.go | 6 + agent/utils/firewall/client/iptables.go | 322 ++++++++++++ agent/utils/firewall/client/iptables_fw.go | 461 ++++++++++++++++++ frontend/src/api/interface/host.ts | 43 ++ frontend/src/api/modules/host.ts | 14 + frontend/src/lang/modules/en.ts | 1 + frontend/src/lang/modules/zh.ts | 26 + frontend/src/routers/modules/host.ts | 10 + frontend/src/views/host/filter/index.vue | 358 ++++++++++++++ .../src/views/host/filter/operate/index.vue | 165 +++++++ 19 files changed, 2109 insertions(+), 8 deletions(-) create mode 100644 agent/app/repo/iptables_filter_rule.go create mode 100644 agent/app/service/iptables_filter.go create mode 100644 agent/utils/firewall/client/iptables_fw.go create mode 100644 frontend/src/views/host/filter/index.vue create mode 100644 frontend/src/views/host/filter/operate/index.vue diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index aa403c95cbe8..47fedc28fd66 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -35,11 +35,12 @@ var ( cronjobService = service.NewICronjobService() - fileService = service.NewIFileService() - sshService = service.NewISSHService() - firewallService = service.NewIFirewallService() - monitorService = service.NewIMonitorService() - systemService = service.NewISystemService() + fileService = service.NewIFileService() + sshService = service.NewISSHService() + firewallService = service.NewIFirewallService() + iptablesFilterService = service.NewIIptablesFilterService() + monitorService = service.NewIMonitorService() + systemService = service.NewISystemService() deviceService = service.NewIDeviceService() fail2banService = service.NewIFail2BanService() diff --git a/agent/app/api/v2/firewall.go b/agent/app/api/v2/firewall.go index 62e2a44a740d..9bfb315ef332 100644 --- a/agent/app/api/v2/firewall.go +++ b/agent/app/api/v2/firewall.go @@ -221,3 +221,114 @@ func (b *BaseApi) UpdateAddrRule(c *gin.Context) { } helper.Success(c) } + +// @Tags Firewall +// @Summary Get iptables filter rules +// @Accept json +// @Param request body dto.IptablesFilterRuleSearch true "request" +// @Success 200 {object} []dto.IptablesChainInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/filter/rules [post] +func (b *BaseApi) GetFilterRules(c *gin.Context) { + var req dto.IptablesFilterRuleSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := iptablesFilterService.GetFilterRules(req.Chains) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.SuccessWithData(c, data) +} + +// @Tags Firewall +// @Summary Operate iptables filter rule +// @Accept json +// @Param request body dto.IptablesFilterRuleOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/filter/rule [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.IptablesFilterRuleOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if req.Operation == "add" { + if err := iptablesFilterService.AddRule(req); err != nil { + helper.InternalServer(c, err) + return + } + } else if req.Operation == "remove" { + if err := iptablesFilterService.RemoveRule(req.ID); err != nil { + helper.InternalServer(c, err) + return + } + } + + helper.Success(c) +} + +// @Tags Firewall +// @Summary Batch operate iptables filter rules +// @Accept json +// @Param request body dto.IptablesFilterBatchOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/filter/batch [post] +func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) { + var req dto.IptablesFilterBatchOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := iptablesFilterService.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.IptablesFilterApply true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/firewall/filter/apply [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] iptables filter防火墙","formatEN":"[operation] iptables filter firewall"} +func (b *BaseApi) ApplyFilterFirewall(c *gin.Context) { + var req dto.IptablesFilterApply + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + switch req.Operation { + case "init": + if err := iptablesFilterService.InitChains(); err != nil { + helper.InternalServer(c, err) + return + } + case "apply": + if err := iptablesFilterService.ApplyFirewall(); err != nil { + helper.InternalServer(c, err) + return + } + case "unload": + if err := iptablesFilterService.UnloadFirewall(); err != nil { + helper.InternalServer(c, err) + return + } + } + + helper.Success(c) +} diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 99920d929232..40e33db44888 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -76,3 +76,52 @@ type BatchRuleOperate struct { Type string `json:"type" validate:"required"` Rules []PortRuleOperate `json:"rules"` } + +// Iptables Filter DTO + +type IptablesFilterRuleSearch struct { + Chains []string `json:"chains"` +} + +type IptablesChainInfo struct { + Version string `json:"version"` + Name string `json:"name"` + DefaultPolicy string `json:"defaultPolicy"` + Rules []IptablesFilterRuleInfo `json:"rules"` + IsApplied bool `json:"isApplied"` +} + +type IptablesFilterRuleInfo struct { + ID uint `json:"id"` + Protocol string `json:"protocol"` + SourceIP string `json:"sourceIP"` + SourcePort uint16 `json:"sourcePort"` + DestIP string `json:"destIP"` + DestPort uint16 `json:"destPort"` + Action string `json:"action"` + Comment string `json:"comment"` + Description string `json:"description"` + RuleOrder int `json:"ruleOrder"` + IsActive bool `json:"isActive"` +} + +type IptablesFilterRuleOperate 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"` + SourceIP string `json:"sourceIP"` + SourcePort uint16 `json:"sourcePort"` + DestIP string `json:"destIP"` + DestPort uint16 `json:"destPort"` + Action string `json:"action" validate:"required,oneof=ACCEPT DROP REJECT"` + Description string `json:"description"` +} + +type IptablesFilterApply struct { + Operation string `json:"operation" validate:"required,oneof=apply unload init"` +} + +type IptablesFilterBatchOperate struct { + Rules []IptablesFilterRuleOperate `json:"rules"` +} diff --git a/agent/app/model/firewall.go b/agent/app/model/firewall.go index 622203d058c9..358dd2ad16c4 100644 --- a/agent/app/model/firewall.go +++ b/agent/app/model/firewall.go @@ -20,3 +20,29 @@ type Forward struct { TargetPort string `gorm:"not null" json:"targetPort"` Interface string `json:"interface"` } + +type IptablesFilterRule struct { + BaseModel + + Chain string `gorm:"not null;index:idx_chain" json:"chain"` + Protocol string `json:"protocol"` + SourceIP string `json:"sourceIP"` + SourcePort uint16 `json:"sourcePort"` + DestIP string `json:"destIP"` + DestPort uint16 `json:"destPort"` + Action string `gorm:"not null" json:"action"` + Comment string `json:"comment"` + Description string `json:"description"` + RuleOrder int `gorm:"default:0;index:idx_chain_order" json:"ruleOrder"` +} + +type IptablesRule struct { + BaseModel + + RuleType string `gorm:"not null" json:"ruleType"` // port, address + Protocol string `json:"protocol"` + Port string `json:"port"` + Strategy string `gorm:"not null" json:"strategy"` // accept, drop, reject + Address string `json:"address"` + Family string `json:"family"` // ipv4, ipv6 +} diff --git a/agent/app/repo/iptables_filter_rule.go b/agent/app/repo/iptables_filter_rule.go new file mode 100644 index 000000000000..207ef5d252eb --- /dev/null +++ b/agent/app/repo/iptables_filter_rule.go @@ -0,0 +1,67 @@ +package repo + +import ( + "context" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type IIptablesFilterRuleRepo interface { + Create(ctx context.Context, rule *model.IptablesFilterRule) error + Delete(ctx context.Context, id uint) error + DeleteByChain(ctx context.Context, chain string) error + GetByID(ctx context.Context, id uint) (model.IptablesFilterRule, error) + List(ctx context.Context, chains []string) ([]model.IptablesFilterRule, error) + ListByChain(ctx context.Context, chain string) ([]model.IptablesFilterRule, error) + GetMaxRuleOrder(ctx context.Context, chain string) (int, error) +} + +type IptablesFilterRuleRepo struct{} + +func NewIIptablesFilterRuleRepo() IIptablesFilterRuleRepo { + return &IptablesFilterRuleRepo{} +} + +func (r *IptablesFilterRuleRepo) Create(ctx context.Context, rule *model.IptablesFilterRule) error { + return global.DB.WithContext(ctx).Create(rule).Error +} + +func (r *IptablesFilterRuleRepo) Delete(ctx context.Context, id uint) error { + return global.DB.WithContext(ctx).Where("id = ?", id).Delete(&model.IptablesFilterRule{}).Error +} + +func (r *IptablesFilterRuleRepo) DeleteByChain(ctx context.Context, chain string) error { + return global.DB.WithContext(ctx).Where("chain = ?", chain).Delete(&model.IptablesFilterRule{}).Error +} + +func (r *IptablesFilterRuleRepo) GetByID(ctx context.Context, id uint) (model.IptablesFilterRule, error) { + var rule model.IptablesFilterRule + err := global.DB.WithContext(ctx).Where("id = ?", id).First(&rule).Error + return rule, err +} + +func (r *IptablesFilterRuleRepo) List(ctx context.Context, chains []string) ([]model.IptablesFilterRule, error) { + var rules []model.IptablesFilterRule + query := global.DB.WithContext(ctx) + if len(chains) > 0 { + query = query.Where("chain IN ?", chains) + } + err := query.Order("chain ASC, rule_order ASC, id ASC").Find(&rules).Error + return rules, err +} + +func (r *IptablesFilterRuleRepo) ListByChain(ctx context.Context, chain string) ([]model.IptablesFilterRule, error) { + var rules []model.IptablesFilterRule + err := global.DB.WithContext(ctx).Where("chain = ?", chain).Order("rule_order ASC, id ASC").Find(&rules).Error + return rules, err +} + +func (r *IptablesFilterRuleRepo) GetMaxRuleOrder(ctx context.Context, chain string) (int, error) { + var maxOrder int + err := global.DB.WithContext(ctx).Model(&model.IptablesFilterRule{}). + Where("chain = ?", chain). + Select("COALESCE(MAX(rule_order), 0)"). + Scan(&maxOrder).Error + return maxOrder, err +} diff --git a/agent/app/service/iptables_filter.go b/agent/app/service/iptables_filter.go new file mode 100644 index 000000000000..9e4d9df2785a --- /dev/null +++ b/agent/app/service/iptables_filter.go @@ -0,0 +1,434 @@ +package service + +import ( + "context" + "fmt" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/firewall/client" +) + +type IIptablesFilterService interface { + GetFilterRules(chains []string) ([]dto.IptablesChainInfo, error) + AddRule(req dto.IptablesFilterRuleOperate) error + RemoveRule(id uint) error + BatchOperate(req dto.IptablesFilterBatchOperate) error + InitChains() error + ApplyFirewall() error + UnloadFirewall() error + ReloadRules() error +} + +type IptablesFilterService struct { + repo repo.IIptablesFilterRuleRepo + iptablesClient *client.Iptables +} + +func NewIIptablesFilterService() IIptablesFilterService { + iptablesClient, _ := client.NewIptables() + return &IptablesFilterService{ + repo: repo.NewIIptablesFilterRuleRepo(), + iptablesClient: iptablesClient, + } +} + +const ( + ChainInput = "INPUT" + ChainOutput = "OUTPUT" + Chain1PanelInput = "1PANEL_INPUT" + Chain1PanelOutput = "1PANEL_OUTPUT" +) + +func (s *IptablesFilterService) GetFilterRules(chains []string) ([]dto.IptablesChainInfo, error) { + if len(chains) == 0 { + chains = []string{ChainInput, ChainOutput, Chain1PanelInput, Chain1PanelOutput} + } + + // 获取 iptables 版本 + version, err := s.iptablesClient.GetVersion() + if err != nil { + global.LOG.Warnf("failed to get iptables version: %v", err) + version = "unknown" + } + + // 读取 iptables 规则 + iptablesChains, err := s.iptablesClient.ReadFilter(chains) + if err != nil { + return nil, fmt.Errorf("failed to read iptables rules: %w", err) + } + + // 从数据库读取规则描述 + ctx := context.Background() + dbRules, _ := s.repo.List(ctx, []string{Chain1PanelInput, Chain1PanelOutput}) + descMap := make(map[uint]string) + for _, rule := range dbRules { + descMap[rule.ID] = rule.Description + } + + var result []dto.IptablesChainInfo + for _, chainName := range chains { + chain, ok := iptablesChains[chainName] + if !ok { + continue + } + + chainInfo := dto.IptablesChainInfo{ + Version: version, + Name: chainName, + DefaultPolicy: chain.DefaultPolicy, + Rules: []dto.IptablesFilterRuleInfo{}, + IsApplied: false, + } + + // 检查是否已应用(INPUT/OUTPUT 链包含跳转规则) + if chainName == ChainInput || chainName == ChainOutput { + item := chain.FirstRule + targetChain := Chain1PanelInput + if chainName == ChainOutput { + targetChain = Chain1PanelOutput + } + for item != nil { + if item.P.Action == targetChain { + chainInfo.IsApplied = true + break + } + item = item.Next() + } + } + + // 转换规则 + item := chain.FirstRule + order := 0 + for item != nil { + p := item.P + ruleInfo := dto.IptablesFilterRuleInfo{ + Protocol: p.Protocol, + SourceIP: p.SourceIP, + SourcePort: p.SrcPort, + DestIP: p.DestIP, + DestPort: p.DstPort, + Action: p.Action, + Comment: p.Comment, + RuleOrder: order, + } + + // 从 comment 中提取 ID(格式:1Panel_FilterRule_) + if p.Comment != "" { + var id uint + fmt.Sscanf(p.Comment, "1Panel_FilterRule_%d", &id) + ruleInfo.ID = id + if desc, ok := descMap[id]; ok { + ruleInfo.Description = desc + } + } + + chainInfo.Rules = append(chainInfo.Rules, ruleInfo) + order++ + item = item.Next() + } + + result = append(result, chainInfo) + } + + return result, nil +} + +func (s *IptablesFilterService) AddRule(req dto.IptablesFilterRuleOperate) error { + if req.Chain != Chain1PanelInput && req.Chain != Chain1PanelOutput { + return fmt.Errorf("只允许操作 %s 或 %s 链", Chain1PanelInput, Chain1PanelOutput) + } + + // 安全检查:防止无条件 DROP/REJECT + if (req.Action == "DROP" || req.Action == "REJECT") && + req.Protocol == "" && req.SourceIP == "" && req.DestIP == "" && + req.SourcePort == 0 && req.DestPort == 0 { + return fmt.Errorf("不允许添加无条件 %s 规则,这会锁定系统", req.Action) + } + + ctx := context.Background() + + // 获取最大规则顺序 + maxOrder, err := s.repo.GetMaxRuleOrder(ctx, req.Chain) + if err != nil { + return fmt.Errorf("failed to get max rule order: %w", err) + } + + // 创建数据库记录 + rule := &model.IptablesFilterRule{ + Chain: req.Chain, + Protocol: req.Protocol, + SourceIP: req.SourceIP, + SourcePort: req.SourcePort, + DestIP: req.DestIP, + DestPort: req.DestPort, + Action: req.Action, + Description: req.Description, + RuleOrder: maxOrder + 1, + } + + if err := s.repo.Create(ctx, rule); err != nil { + return fmt.Errorf("failed to save rule to database: %w", err) + } + + // 生成 comment + rule.Comment = fmt.Sprintf("1Panel_FilterRule_%d", rule.ID) + + // 添加 iptables 规则 + policy := client.IptablesPolicy{ + Protocol: req.Protocol, + SourceIP: req.SourceIP, + SrcPort: req.SourcePort, + DestIP: req.DestIP, + DstPort: req.DestPort, + Action: req.Action, + Comment: rule.Comment, + } + + if err := s.iptablesClient.AddPolicy(req.Chain, policy); err != nil { + // 回滚数据库 + _ = s.repo.Delete(ctx, rule.ID) + return fmt.Errorf("failed to add iptables rule: %w", err) + } + + // 更新数据库 comment + rule.Comment = fmt.Sprintf("1Panel_FilterRule_%d", rule.ID) + return global.DB.Model(rule).Update("comment", rule.Comment).Error +} + +func (s *IptablesFilterService) RemoveRule(id uint) error { + ctx := context.Background() + + // 从数据库查询规则 + rule, err := s.repo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("rule not found: %w", err) + } + + // 按 comment 删除 iptables 规则 + if rule.Comment != "" { + if err := s.iptablesClient.RemovePolicyByComment(rule.Chain, rule.Comment); err != nil { + global.LOG.Warnf("failed to remove iptables rule by comment: %v, trying to delete by policy", err) + // 如果按 comment 删除失败,尝试按规则内容删除 + policy := client.IptablesPolicy{ + Protocol: rule.Protocol, + SourceIP: rule.SourceIP, + SrcPort: rule.SourcePort, + DestIP: rule.DestIP, + DstPort: rule.DestPort, + Action: rule.Action, + Comment: rule.Comment, + } + if err := s.iptablesClient.DeletePolicy(rule.Chain, policy); err != nil { + return fmt.Errorf("failed to remove iptables rule: %w", err) + } + } + } + + // 删除数据库记录 + return s.repo.Delete(ctx, id) +} + +func (s *IptablesFilterService) BatchOperate(req dto.IptablesFilterBatchOperate) error { + for _, ruleReq := range req.Rules { + if ruleReq.Operation == "add" { + if err := s.AddRule(ruleReq); err != nil { + return err + } + } else if ruleReq.Operation == "remove" { + if err := s.RemoveRule(ruleReq.ID); err != nil { + return err + } + } + } + return nil +} + +func (s *IptablesFilterService) InitChains() error { + // 检查并创建 1PANEL_INPUT + exists, err := s.iptablesClient.ChainExists(client.FilterTab, Chain1PanelInput) + if err != nil { + return fmt.Errorf("failed to check chain %s: %w", Chain1PanelInput, err) + } + + if !exists { + if err := s.iptablesClient.NewChain(client.FilterTab, Chain1PanelInput); err != nil { + return fmt.Errorf("failed to create chain %s: %w", Chain1PanelInput, err) + } + } else { + // 清空已存在的链 + if err := s.iptablesClient.FlushChain(client.FilterTab, Chain1PanelInput); err != nil { + return err + } + } + + // 检查并创建 1PANEL_OUTPUT + exists, err = s.iptablesClient.ChainExists(client.FilterTab, Chain1PanelOutput) + if err != nil { + return fmt.Errorf("failed to check chain %s: %w", Chain1PanelOutput, err) + } + + if !exists { + if err := s.iptablesClient.NewChain(client.FilterTab, Chain1PanelOutput); err != nil { + return fmt.Errorf("failed to create chain %s: %w", Chain1PanelOutput, err) + } + } else { + if err := s.iptablesClient.FlushChain(client.FilterTab, Chain1PanelOutput); err != nil { + return err + } + } + + return nil +} + +func (s *IptablesFilterService) ApplyFirewall() error { + // 安全检查 + chains, err := s.iptablesClient.ReadFilter([]string{Chain1PanelInput, Chain1PanelOutput}) + if err != nil { + return fmt.Errorf("failed to read filter chains: %w", err) + } + + // 检查 1PANEL_INPUT 安全性 + if inputChain, ok := chains[Chain1PanelInput]; ok { + item := inputChain.FirstRule + for item != nil { + p := item.P + if (p.Action == "DROP" || p.Action == "REJECT") && + p.Protocol == "" && p.SourceIP == "" && p.DestIP == "" && + p.SrcPort == 0 && p.DstPort == 0 { + return fmt.Errorf("链 %s 包含无条件 %s 规则,不允许应用", Chain1PanelInput, p.Action) + } + item = item.Next() + } + } + + // 检查 1PANEL_OUTPUT 安全性 + if outputChain, ok := chains[Chain1PanelOutput]; ok { + item := outputChain.FirstRule + for item != nil { + p := item.P + if (p.Action == "DROP" || p.Action == "REJECT") && + p.Protocol == "" && p.SourceIP == "" && p.DestIP == "" && + p.SrcPort == 0 && p.DstPort == 0 { + return fmt.Errorf("链 %s 包含无条件 %s 规则,不允许应用", Chain1PanelOutput, p.Action) + } + item = item.Next() + } + } + + // 检查 INPUT 链是否已有跳转规则 + inputChains, _ := s.iptablesClient.ReadFilter([]string{ChainInput}) + hasInputRule := false + if inputChain, ok := inputChains[ChainInput]; ok { + item := inputChain.FirstRule + for item != nil { + if item.P.Action == Chain1PanelInput { + hasInputRule = true + break + } + item = item.Next() + } + } + + // 应用到 INPUT 链 + if !hasInputRule { + if err := s.iptablesClient.Run(client.FilterTab, fmt.Sprintf("-I %s 1 -j %s", ChainInput, Chain1PanelInput)); err != nil { + return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelInput, ChainInput, err) + } + global.LOG.Infof("Applied %s to %s chain", Chain1PanelInput, ChainInput) + } else { + global.LOG.Infof("%s already applied to %s chain", Chain1PanelInput, ChainInput) + } + + // 检查 OUTPUT 链是否已有跳转规则 + outputChains, _ := s.iptablesClient.ReadFilter([]string{ChainOutput}) + hasOutputRule := false + if outputChain, ok := outputChains[ChainOutput]; ok { + item := outputChain.FirstRule + for item != nil { + if item.P.Action == Chain1PanelOutput { + hasOutputRule = true + break + } + item = item.Next() + } + } + + // 应用到 OUTPUT 链 + if !hasOutputRule { + if err := s.iptablesClient.Run(client.FilterTab, fmt.Sprintf("-I %s 1 -j %s", ChainOutput, Chain1PanelOutput)); err != nil { + return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelOutput, ChainOutput, err) + } + global.LOG.Infof("Applied %s to %s chain", Chain1PanelOutput, ChainOutput) + } else { + global.LOG.Infof("%s already applied to %s chain", Chain1PanelOutput, ChainOutput) + } + + return nil +} + +func (s *IptablesFilterService) UnloadFirewall() error { + // 从 INPUT 链删除跳转规则 + ruleNum, err := s.iptablesClient.FindRuleNum(ChainInput, Chain1PanelInput) + if err == nil { + if err := s.iptablesClient.RemovePolicyByNum(ChainInput, ruleNum); err != nil { + return fmt.Errorf("failed to unload %s from %s: %w", Chain1PanelInput, ChainInput, err) + } + global.LOG.Infof("Unloaded %s from %s chain", Chain1PanelInput, ChainInput) + } else { + global.LOG.Warnf("%s not found in %s chain: %v", Chain1PanelInput, ChainInput, err) + } + + // 从 OUTPUT 链删除跳转规则 + ruleNum, err = s.iptablesClient.FindRuleNum(ChainOutput, Chain1PanelOutput) + if err == nil { + if err := s.iptablesClient.RemovePolicyByNum(ChainOutput, ruleNum); err != nil { + return fmt.Errorf("failed to unload %s from %s: %w", Chain1PanelOutput, ChainOutput, err) + } + global.LOG.Infof("Unloaded %s from %s chain", Chain1PanelOutput, ChainOutput) + } else { + global.LOG.Warnf("%s not found in %s chain: %v", Chain1PanelOutput, ChainOutput, err) + } + + return nil +} + +func (s *IptablesFilterService) ReloadRules() error { + // 清空自定义链 + if err := s.iptablesClient.FlushChain(client.FilterTab, Chain1PanelInput); err != nil { + return fmt.Errorf("failed to flush chain %s: %w", Chain1PanelInput, err) + } + if err := s.iptablesClient.FlushChain(client.FilterTab, Chain1PanelOutput); err != nil { + return fmt.Errorf("failed to flush chain %s: %w", Chain1PanelOutput, err) + } + + // 从数据库读取规则 + ctx := context.Background() + rules, err := s.repo.List(ctx, []string{Chain1PanelInput, Chain1PanelOutput}) + if err != nil { + return fmt.Errorf("failed to load rules from database: %w", err) + } + + // 逐条恢复规则 + for _, rule := range rules { + policy := client.IptablesPolicy{ + Protocol: rule.Protocol, + SourceIP: rule.SourceIP, + SrcPort: rule.SourcePort, + DestIP: rule.DestIP, + DstPort: rule.DestPort, + Action: rule.Action, + Comment: rule.Comment, + } + + if err := s.iptablesClient.AddPolicy(rule.Chain, policy); err != nil { + global.LOG.Errorf("failed to reload rule %d: %v", rule.ID, err) + continue + } + } + + global.LOG.Infof("Reloaded %d firewall rules from database", len(rules)) + return nil +} diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 9763b498a384..60e7c8e5c0dc 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -50,6 +50,7 @@ var AddTable = &gormigrate.Migration{ &model.Favorite{}, &model.Forward{}, &model.Firewall{}, + &model.IptablesFilterRule{}, &model.Ftp{}, &model.ImageRepo{}, &model.ScriptLibrary{}, diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index 7a5a845c23a0..cdccc0d2741b 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -22,6 +22,11 @@ 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/rules", baseApi.GetFilterRules) + hostRouter.POST("/firewall/filter/rule", baseApi.OperateFilterRule) + hostRouter.POST("/firewall/filter/batch", baseApi.BatchOperateFilterRule) + hostRouter.POST("/firewall/filter/apply", baseApi.ApplyFilterFirewall) + hostRouter.POST("/monitor/search", baseApi.LoadMonitor) hostRouter.POST("/monitor/clean", baseApi.CleanMonitor) hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions) diff --git a/agent/utils/docker/compose.go b/agent/utils/docker/compose.go index 2f12d36eb13e..a2e43ac7ec19 100644 --- a/agent/utils/docker/compose.go +++ b/agent/utils/docker/compose.go @@ -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 { diff --git a/agent/utils/firewall/client.go b/agent/utils/firewall/client.go index 447cd8697572..f10ac7b2f7ae 100644 --- a/agent/utils/firewall/client.go +++ b/agent/utils/firewall/client.go @@ -41,5 +41,11 @@ func NewFirewallClient() (FirewallClient, error) { if ufw { return client.NewUfw() } + + iptables := cmd.Which("iptables") + if iptables { + return client.NewIptablesFw() + } + return nil, errors.New("No system firewalld or ufw service detected, please check and try again!") } diff --git a/agent/utils/firewall/client/iptables.go b/agent/utils/firewall/client/iptables.go index dc3610a0b301..58ba96d282fe 100644 --- a/agent/utils/firewall/client/iptables.go +++ b/agent/utils/firewall/client/iptables.go @@ -2,6 +2,7 @@ package client import ( "fmt" + "os/exec" "regexp" "strings" "time" @@ -17,6 +18,12 @@ const ( ForwardChain = "1PANEL_FORWARD" ) +const ( + ACCEPT = "ACCEPT" + DROP = "DROP" + REJECT = "REJECT" +) + const ( FilterTab = "filter" NatTab = "nat" @@ -55,6 +62,11 @@ func (iptables *Iptables) run(tab, rule string) error { return nil } +// Run 导出的 run 方法供外部调用 +func (iptables *Iptables) Run(tab, rule string) error { + return iptables.run(tab, rule) +} + func (iptables *Iptables) Check() error { stdout, err := cmd.RunDefaultWithStdoutBashC("cat /proc/sys/net/ipv4/ip_forward") if err != nil { @@ -75,6 +87,19 @@ func (iptables *Iptables) Check() error { return nil } +func (iptables *Iptables) GetVersion() (string, error) { + stdout, err := cmd.RunDefaultWithStdoutBashC("iptables --version") + if err != nil { + return "", fmt.Errorf("failed to get iptables version: %w", err) + } + // 提取版本号,例如 "iptables v1.8.7 (nf_tables)" + parts := strings.Fields(stdout) + if len(parts) >= 2 { + return strings.TrimPrefix(parts[1], "v"), nil + } + return strings.TrimSpace(stdout), nil +} + func (iptables *Iptables) NewChain(tab, chain string) error { return iptables.run(tab, "-N "+chain) } @@ -232,6 +257,55 @@ func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPor return nil } +// test struct +/* +🧩 规则解释 + +-A INPUT / -A OUTPUT:追加规则到对应链。 + +-p tcp / -p udp:指定协议。 + +--dport / --sport:目标端口 / 源端口。 + +-d / -s:目标 IP / 源 IP。 + +-j DROP:丢弃数据包(不回应)。 + +若改成 -j REJECT,则会主动返回一个拒绝报文(对调试有用)。* +*/ +type IptablesPolicy struct { + Protocol string + SrcPort uint16 + DstPort uint16 + SourceIP string + DestIP string + Action string // ACCEPT, DROP, REJECT + Comment string +} + +func (iptables *Iptables) AddPolicy(chain string, policy IptablesPolicy) error { + 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.SourceIP != "" { + iptablesArg += fmt.Sprintf(" -s %s", policy.SourceIP) + } + if policy.DestIP != "" { + iptablesArg += fmt.Sprintf(" -d %s", policy.DestIP) + } + iptablesArg += fmt.Sprintf(" -j %s", policy.Action) + iptablesArg += fmt.Sprintf(" -m comment --comment \"%s\"", policy.Comment) + + return iptables.run(FilterTab, iptablesArg) +} + func (iptables *Iptables) Reload() error { if err := iptables.run(NatTab, "-F "+PreRoutingChain); err != nil { return err @@ -252,3 +326,251 @@ func (iptables *Iptables) Reload() error { } return nil } + +// IptablesChain +type IptablesChain struct { + Name string + DefaultPolicy string + FirstRule *IptablesPolicyChainItem + LastRule *IptablesPolicyChainItem +} + +type IptablesPolicyChainItem struct { + next *IptablesPolicyChainItem + P IptablesPolicy +} + +func (item *IptablesPolicyChainItem) SetNext(next *IptablesPolicyChainItem) { + item.next = next +} + +func (item *IptablesPolicyChainItem) Next() *IptablesPolicyChainItem { + return item.next +} + +func (c *IptablesChain) ParseLine(line string) error { + cmd := strings.Split(line, " ") + + if cmd[0] == "-P" { + c.Name = cmd[1] + c.DefaultPolicy = cmd[2] + return nil + } + if cmd[0] == "-A" { + if cmd[1] != c.Name { + return fmt.Errorf("invalid chain name in rule line: %s", line) + } + policy := IptablesPolicy{} + for i := 2; i < len(cmd); i++ { + switch cmd[i] { + case "-p": + i++ + policy.Protocol = cmd[i] + case "--dport": + i++ + // parse port + var port uint16 + fmt.Sscanf(cmd[i], "%d", &port) + policy.DstPort = port + case "--sport": + i++ + var port uint16 + fmt.Sscanf(cmd[i], "%d", &port) + policy.SrcPort = port + case "-s": + i++ + policy.SourceIP = cmd[i] + case "-d": + i++ + policy.DestIP = cmd[i] + case "-j": + i++ + policy.Action = cmd[i] + case "-m": + // skip + i++ + case "--comment": + i++ + policy.Comment = strings.Trim(cmd[i], "\"") + } + } + newItem := &IptablesPolicyChainItem{ + P: policy, + } + if c.FirstRule == nil { + c.FirstRule = newItem + c.LastRule = newItem + } else { + current := c.LastRule + current.SetNext(newItem) + c.LastRule = newItem + } + return nil + } + return fmt.Errorf("invalid iptables rule line: %s", line) +} + +// iptables filter 解析 +func (iptables *Iptables) ReadFilter(chainName []string) (map[string]IptablesChain, error) { + // iptables -S + cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second)) + stdout, err := cmdMgr.RunWithStdoutBashCf("%s iptables -S", iptables.CmdStr) + if err != nil { + global.LOG.Errorf("iptables failed, %v", err) + } + // 解析内容 + chains := make(map[string]IptablesChain) + for _, line := range strings.Split(stdout, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "-P") || strings.HasPrefix(line, "-A") { + parts := strings.SplitN(line, " ", 3) + chain := parts[1] + if len(chainName) > 0 { + found := false + for _, name := range chainName { + if chain == name { + found = true + break + } + } + if !found { + continue + } + } + if _, exists := chains[chain]; !exists { + chains[chain] = IptablesChain{ + Name: chain, + } + } + chainStruct := chains[chain] + if err := chainStruct.ParseLine(line); err != nil { + return nil, err + } + chains[chain] = chainStruct + } + } + return chains, nil +} + +// 检查链中是否有无条件 DROP/REJECT 规则 +func checkChainSafety(chain IptablesChain) error { + item := chain.FirstRule + for item != nil { + policy := item.P + if policy.Action == "DROP" || policy.Action == "REJECT" { + // 检查是否为无条件规则(所有字段都为空) + if policy.Protocol == "" && policy.SrcPort == 0 && policy.DstPort == 0 && + policy.SourceIP == "" && policy.DestIP == "" { + return fmt.Errorf("发现无条件 %s 规则,不允许应用", policy.Action) + } + } + item = item.Next() + } + return nil +} + +// 执行 iptables 命令 +func runIptables(args ...string) error { + cmd := exec.Command("iptables", args...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("执行 iptables 失败: %v, 输出: %s", err, string(output)) + } + return nil +} + +// RemovePolicyByComment 按 comment 删除规则 +func (iptables *Iptables) RemovePolicyByComment(chain, comment string) error { + stdout, err := iptables.out(FilterTab, fmt.Sprintf("-S %s", chain)) + if err != nil { + return err + } + + // 解析规则找到匹配 comment 的行 + lines := strings.Split(stdout, "\n") + for _, line := range lines { + if strings.Contains(line, comment) && strings.HasPrefix(line, "-A") { + // 替换 -A 为 -D 执行删除 + deleteRule := strings.Replace(line, "-A", "-D", 1) + if err := iptables.run(FilterTab, deleteRule); err != nil { + return err + } + return nil + } + } + return fmt.Errorf("rule with comment '%s' not found in chain %s", comment, chain) +} + +// RemovePolicyByNum 按规则编号删除 +func (iptables *Iptables) RemovePolicyByNum(chain string, num int) error { + return iptables.run(FilterTab, fmt.Sprintf("-D %s %d", chain, num)) +} + +// FindRuleNum 查找规则编号(返回第一个匹配的行号) +func (iptables *Iptables) FindRuleNum(chain, matchStr string) (int, error) { + stdout, err := iptables.out(FilterTab, fmt.Sprintf("-L %s --line-numbers -n", chain)) + if err != nil { + return 0, err + } + + lines := strings.Split(stdout, "\n") + for _, line := range lines { + if strings.Contains(line, matchStr) { + // 提取行号(第一列) + fields := strings.Fields(line) + if len(fields) > 0 { + num, err := fmt.Sscanf(fields[0], "%d", new(int)) + if err == nil && num == 1 { + var ruleNum int + fmt.Sscanf(fields[0], "%d", &ruleNum) + return ruleNum, nil + } + } + } + } + return 0, fmt.Errorf("rule not found in chain %s", chain) +} + +// ChainExists 检查链是否存在 +func (iptables *Iptables) ChainExists(tab, chain string) (bool, error) { + // iptables -t filter -S | grep 'N 1PANEL' + stdout, err := iptables.out(tab, fmt.Sprintf("-S | grep 'N %s'", chain)) + if err != nil { + return false, err + } + if strings.TrimSpace(stdout) == "" { + return false, nil + } + return true, nil +} + +// FlushChain 清空链(保留链结构) +func (iptables *Iptables) FlushChain(tab, chain string) error { + return iptables.run(tab, fmt.Sprintf("-F %s", chain)) +} + +// DeletePolicy 删除指定策略规则 +func (iptables *Iptables) DeletePolicy(chain string, policy IptablesPolicy) 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.SourceIP != "" { + iptablesArg += fmt.Sprintf(" -s %s", policy.SourceIP) + } + if policy.DestIP != "" { + iptablesArg += fmt.Sprintf(" -d %s", policy.DestIP) + } + iptablesArg += fmt.Sprintf(" -j %s", policy.Action) + if policy.Comment != "" { + iptablesArg += fmt.Sprintf(" -m comment --comment \"%s\"", policy.Comment) + } + + return iptables.run(FilterTab, iptablesArg) +} diff --git a/agent/utils/firewall/client/iptables_fw.go b/agent/utils/firewall/client/iptables_fw.go new file mode 100644 index 000000000000..d63f84fbd7ff --- /dev/null +++ b/agent/utils/firewall/client/iptables_fw.go @@ -0,0 +1,461 @@ +package client + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "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" +) + +const ( + Chain1PanelInput = "1PFW-INPUT" + Chain1PanelOutput = "1PFW-OUTPUT" +) + +var ( + // 匹配 iptables 规则的正则表达式 + filterListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?))?(?:\s+(.+?))?`) +) + +type IptablesFw struct { + iptables *Iptables +} + +func NewIptablesFw() (*IptablesFw, error) { + iptables, err := NewIptables() + if err != nil { + return nil, err + } + return &IptablesFw{ + iptables: iptables, + }, nil +} + +func (f *IptablesFw) Name() string { + return "iptables" +} + +func (f *IptablesFw) Status() (bool, error) { + // 检查自定义链是否存在来判断防火墙是否启用 + exists, err := f.iptables.ChainExists(FilterTab, Chain1PanelInput) + if err != nil { + return false, err + } + return exists, nil +} + +func (f *IptablesFw) Version() (string, error) { + stdout, err := cmd.RunDefaultWithStdoutBashC("iptables --version") + if err != nil { + return "", fmt.Errorf("load the firewall version failed, %v", err) + } + // 提取版本号,例如 "iptables v1.8.7 (nf_tables)" + parts := strings.Fields(stdout) + if len(parts) >= 2 { + return strings.TrimPrefix(parts[1], "v"), nil + } + return strings.TrimSpace(stdout), nil +} + +func (f *IptablesFw) Start() error { + // 创建自定义链 + if err := f.iptables.NewChain(FilterTab, Chain1PanelInput); err != nil { + global.LOG.Debugf("chain %s may already exist: %v", Chain1PanelInput, err) + } + if err := f.iptables.NewChain(FilterTab, Chain1PanelOutput); err != nil { + global.LOG.Debugf("chain %s may already exist: %v", Chain1PanelOutput, err) + } + + // 将自定义链附加到 INPUT 和 OUTPUT 链 + if err := f.ensureChainAttached("INPUT", Chain1PanelInput); err != nil { + return fmt.Errorf("failed to attach %s to INPUT: %v", Chain1PanelInput, err) + } + if err := f.ensureChainAttached("OUTPUT", Chain1PanelOutput); err != nil { + return fmt.Errorf("failed to attach %s to OUTPUT: %v", Chain1PanelOutput, err) + } + + // 重载规则(从数据库恢复) + return f.Reload() +} + +func (f *IptablesFw) Stop() error { + // 从 INPUT 和 OUTPUT 链中移除跳转规则 + _ = f.iptables.Run(FilterTab, fmt.Sprintf("-D INPUT -j %s", Chain1PanelInput)) + _ = f.iptables.Run(FilterTab, fmt.Sprintf("-D OUTPUT -j %s", Chain1PanelOutput)) + + // 清空并删除自定义链 + _ = f.iptables.FlushChain(FilterTab, Chain1PanelInput) + _ = f.iptables.FlushChain(FilterTab, Chain1PanelOutput) + _ = f.iptables.Run(FilterTab, fmt.Sprintf("-X %s", Chain1PanelInput)) + _ = f.iptables.Run(FilterTab, fmt.Sprintf("-X %s", Chain1PanelOutput)) + + return nil +} + +func (f *IptablesFw) Restart() error { + if err := f.Stop(); err != nil { + return err + } + return f.Start() +} + +func (f *IptablesFw) Reload() error { + // 清空链但保留链结构 + if err := f.iptables.FlushChain(FilterTab, Chain1PanelInput); err != nil { + return err + } + if err := f.iptables.FlushChain(FilterTab, Chain1PanelOutput); err != nil { + return err + } + + // 从数据库加载规则并恢复 + var rules []model.IptablesRule + if err := global.DB.Find(&rules).Error; err != nil { + return fmt.Errorf("failed to load rules from database: %v", err) + } + + for _, rule := range rules { + fireInfo := FireInfo{ + Protocol: rule.Protocol, + Port: rule.Port, + Strategy: rule.Strategy, + Address: rule.Address, + Family: rule.Family, + } + if rule.RuleType == "port" { + if err := f.Port(fireInfo, "add"); err != nil { + global.LOG.Errorf("failed to restore port rule %+v: %v", rule, err) + } + } else if rule.RuleType == "address" { + if err := f.RichRules(fireInfo, "add"); err != nil { + global.LOG.Errorf("failed to restore address rule %+v: %v", rule, err) + } + } + } + + return nil +} + +func (f *IptablesFw) ListPort() ([]FireInfo, error) { + return f.listRules(Chain1PanelInput, "port") +} + +func (f *IptablesFw) ListAddress() ([]FireInfo, error) { + return f.listRules(Chain1PanelInput, "address") +} + +func (f *IptablesFw) ListForward() ([]FireInfo, error) { + // 复用 ufw 的转发实现 + if err := f.EnableForward(); err != nil { + global.LOG.Errorf("init port forward failed, err: %v", err) + } + + rules, err := f.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 *IptablesFw) Port(port FireInfo, operation string) error { + if cmd.CheckIllegal(operation, port.Protocol, port.Port) { + return buserr.New("ErrCmdIllegal") + } + + // 转换策略 + strategy := f.convertStrategy(port.Strategy) + if strategy == "" { + return fmt.Errorf("unsupported strategy %s", port.Strategy) + } + + // 构建规则的唯一标识 + comment := fmt.Sprintf("1Panel_Port_%s_%s_%s", port.Protocol, port.Port, strategy) + + if operation == "add" { + // 添加到 INPUT 链 + policy := IptablesPolicy{ + Protocol: port.Protocol, + DstPort: f.parsePort(port.Port), + Action: strategy, + Comment: comment, + } + if err := f.iptables.AddPolicy(Chain1PanelInput, policy); err != nil { + return fmt.Errorf("add port rule to INPUT failed: %v", err) + } + + // 保存到数据库 + rule := model.IptablesRule{ + RuleType: "port", + Protocol: port.Protocol, + Port: port.Port, + Strategy: port.Strategy, + Family: "ipv4", + } + if err := global.DB.Create(&rule).Error; err != nil { + return fmt.Errorf("save rule to database failed: %v", err) + } + } else if operation == "remove" { + // 从 INPUT 链删除 + if err := f.iptables.RemovePolicyByComment(Chain1PanelInput, comment); err != nil { + return fmt.Errorf("remove port rule from INPUT failed: %v", err) + } + + // 从数据库删除 + global.DB.Where("rule_type = ? AND protocol = ? AND port = ? AND strategy = ?", + "port", port.Protocol, port.Port, port.Strategy).Delete(&model.IptablesRule{}) + } + + return nil +} + +func (f *IptablesFw) RichRules(rule FireInfo, operation string) error { + if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port) { + return buserr.New("ErrCmdIllegal") + } + + strategy := f.convertStrategy(rule.Strategy) + if strategy == "" { + return fmt.Errorf("unsupported strategy %s", rule.Strategy) + } + + // 构建规则的唯一标识 + comment := fmt.Sprintf("1Panel_Addr_%s_%s_%s_%s", rule.Address, rule.Protocol, rule.Port, strategy) + + if operation == "add" { + policy := IptablesPolicy{ + Protocol: rule.Protocol, + SourceIP: rule.Address, + Action: strategy, + Comment: comment, + } + if len(rule.Port) > 0 { + policy.DstPort = f.parsePort(rule.Port) + } + + if err := f.iptables.AddPolicy(Chain1PanelInput, policy); err != nil { + return fmt.Errorf("add address rule failed: %v", err) + } + + // 保存到数据库 + dbRule := model.IptablesRule{ + RuleType: "address", + Protocol: rule.Protocol, + Port: rule.Port, + Strategy: rule.Strategy, + Address: rule.Address, + Family: rule.Family, + } + if dbRule.Family == "" { + dbRule.Family = "ipv4" + } + if err := global.DB.Create(&dbRule).Error; err != nil { + return fmt.Errorf("save rule to database failed: %v", err) + } + } else if operation == "remove" { + if err := f.iptables.RemovePolicyByComment(Chain1PanelInput, comment); err != nil { + return fmt.Errorf("remove address rule failed: %v", err) + } + + // 从数据库删除 + global.DB.Where("rule_type = ? AND address = ? AND protocol = ? AND port = ? AND strategy = ?", + "address", rule.Address, rule.Protocol, rule.Port, rule.Strategy).Delete(&model.IptablesRule{}) + } + + return nil +} + +func (f *IptablesFw) PortForward(info Forward, operation string) error { + // 直接复用 iptables 的 NAT 功能 + if operation == "add" { + err := f.iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true) + if err != nil { + return fmt.Errorf("add port forward failed: %v", err) + } + } else if operation == "remove" { + err := f.iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface) + if err != nil { + return fmt.Errorf("remove port forward failed: %v", err) + } + } + return nil +} + +func (f *IptablesFw) EnableForward() error { + // 复用 ufw 的转发初始化逻辑 + if err := f.iptables.Check(); err != nil { + return err + } + + _ = f.iptables.NewChain(NatTab, PreRoutingChain) + _ = f.iptables.NewChain(NatTab, PostRoutingChain) + _ = f.iptables.NewChain(FilterTab, ForwardChain) + + if err := f.enableForwardChain(); err != nil { + return err + } + return f.iptables.Reload() +} + +// 辅助方法:确保链已附加到目标链 +func (f *IptablesFw) ensureChainAttached(targetChain, customChain string) error { + // 检查是否已经附加 + rules, err := f.iptables.NatList(targetChain) + if err == nil { + for _, rule := range rules { + if rule.Target == customChain { + return nil + } + } + } + + // 附加链 + return f.iptables.AppendChain(FilterTab, targetChain, customChain) +} + +// 辅助方法:启用转发链 +func (f *IptablesFw) enableForwardChain() error { + rules, err := f.iptables.NatList("PREROUTING") + if err != nil { + return err + } + for _, rule := range rules { + if rule.Target == PreRoutingChain { + return nil + } + } + + _ = f.iptables.AppendChain(NatTab, "PREROUTING", PreRoutingChain) + _ = f.iptables.AppendChain(NatTab, "POSTROUTING", PostRoutingChain) + _ = f.iptables.AppendChain(FilterTab, "FORWARD", ForwardChain) + return nil +} + +// 辅助方法:列出规则 +func (f *IptablesFw) listRules(chain, ruleType string) ([]FireInfo, error) { + stdout, err := cmd.RunDefaultWithStdoutBashCf("%s iptables -t filter -L %s -n -v --line-numbers", f.iptables.CmdStr, chain) + if err != nil { + return nil, fmt.Errorf("list rules failed: %v", err) + } + + var datas []FireInfo + lines := strings.Split(stdout, "\n") + for i, line := range lines { + // 跳过表头 + if i < 2 || strings.TrimSpace(line) == "" { + continue + } + + fields := strings.Fields(line) + if len(fields) < 7 { + continue + } + + // 解析规则字段 + // num pkts bytes target prot opt in out source destination [extra] + num := fields[0] + target := fields[3] + protocol := fields[4] + source := fields[8] + + // 提取端口信息(从 extra 字段) + var port string + if len(fields) > 10 { + for j := 10; j < len(fields); j++ { + if strings.HasPrefix(fields[j], "dpt:") { + port = strings.TrimPrefix(fields[j], "dpt:") + } else if strings.HasPrefix(fields[j], "spt:") { + port = strings.TrimPrefix(fields[j], "spt:") + } + } + } + + // 根据类型过滤 + if ruleType == "port" { + // 端口规则:有端口信息,源地址为 0.0.0.0/0 + if len(port) > 0 && (source == "0.0.0.0/0" || source == "anywhere") { + datas = append(datas, FireInfo{ + Num: num, + Protocol: protocol, + Port: port, + Strategy: f.convertStrategyReverse(target), + Family: "ipv4", + }) + } + } else if ruleType == "address" { + // 地址规则:有源地址限制 + if source != "0.0.0.0/0" && source != "anywhere" { + datas = append(datas, FireInfo{ + Num: num, + Protocol: protocol, + Port: port, + Address: source, + Strategy: f.convertStrategyReverse(target), + Family: "ipv4", + }) + } + } + } + + return datas, nil +} + +// 辅助方法:转换策略名称 (accept/drop -> ACCEPT/DROP) +func (f *IptablesFw) convertStrategy(strategy string) string { + switch strategy { + case "accept": + return "ACCEPT" + case "drop": + return "DROP" + case "reject": + return "REJECT" + default: + return "" + } +} + +// 辅助方法:反向转换策略名称 (ACCEPT/DROP -> accept/drop) +func (f *IptablesFw) convertStrategyReverse(strategy string) string { + switch strategy { + case "ACCEPT": + return "accept" + case "DROP": + return "drop" + case "REJECT": + return "reject" + default: + return strategy + } +} + +// 辅助方法:解析端口字符串为 uint16 +func (f *IptablesFw) parsePort(portStr string) uint16 { + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return 0 + } + return uint16(port) +} diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index a39fcd95dea7..e3c6a1447bed 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -263,4 +263,47 @@ export namespace Host { path: string; error: string; } + + // Iptables Filter + export interface IptablesFilterRuleSearch { + chains?: string[]; + } + + export interface IptablesChainInfo { + version: string; + name: string; + defaultPolicy: string; + rules: IptablesFilterRuleInfo[]; + isApplied: boolean; + } + + export interface IptablesFilterRuleInfo { + id: number; + protocol: string; + sourceIP: string; + sourcePort: number; + destIP: string; + destPort: number; + action: string; + comment: string; + description: string; + ruleOrder: number; + } + + export interface IptablesFilterRuleOperate { + operation: string; + id?: number; + chain: string; + protocol: string; + sourceIP?: string; + sourcePort?: number; + destIP?: string; + destPort?: number; + action: string; + description?: string; + } + + export interface IptablesFilterApply { + operation: string; + } } diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index f394e38859d5..bac8c8c4da9a 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -44,6 +44,20 @@ export const batchOperateRule = (params: Host.BatchRule) => { return http.post(`/hosts/firewall/batch`, params, TimeoutEnum.T_60S); }; +// Iptables Filter +export const getFilterRules = (params: Host.IptablesFilterRuleSearch) => { + return http.post(`/hosts/firewall/filter/rules`, params); +}; +export const operateFilterRule = (params: Host.IptablesFilterRuleOperate) => { + return http.post(`/hosts/firewall/filter/rule`, params, TimeoutEnum.T_40S); +}; +export const batchOperateFilterRule = (params: { rules: Host.IptablesFilterRuleOperate[] }) => { + return http.post(`/hosts/firewall/filter/batch`, params, TimeoutEnum.T_40S); +}; +export const applyFilterFirewall = (params: Host.IptablesFilterApply) => { + return http.post(`/hosts/firewall/filter/apply`, params, TimeoutEnum.T_60S); +}; + // monitors export const loadMonitor = (param: Host.MonitorSearch) => { return http.post>(`/hosts/monitor/search`, param); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 278cd3ec9e3a..0089c2f735af 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -374,6 +374,7 @@ const message = { config: 'Configuration | Configurations', ssh: 'SSH Settings', firewall: 'Firewall', + filter: 'Filter', ssl: 'Certificate | Certificates', database: 'Database | Databases', aiTools: 'AI', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 250652d8afab..fb9e9d47ccf7 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -362,6 +362,7 @@ const message = { config: '配置', ssh: 'SSH 管理', firewall: '防火墙', + filter: '过滤器', ssl: '证书', database: '数据库', aiTools: 'AI', @@ -2704,6 +2705,31 @@ const message = { exportHelper: '即将导出 {0} 条防火墙规则,是否继续?', importSuccess: '成功导入 {0} 条规则', importPartialSuccess: '导入完成:成功 {0} 条,失败 {1} 条', + filterRule: 'Filter 规则', + filterHelper: 'Filter 规则允许您在 INPUT/OUTPUT 级别控制网络流量。请谨慎配置,避免锁定系统。', + chain: '链', + targetChain: '目标链', + chainHelper: '仅允许修改 1PANEL_INPUT 和 1PANEL_OUTPUT 自定义链', + ruleOrder: '顺序', + sourceIP: '源 IP', + sourcePort: '源端口', + destIP: '目标 IP', + destPort: '目标端口', + action: '动作', + reject: '拒绝', + defaultPolicy: '默认策略', + applied: '已应用', + notApplied: '未应用', + initChains: '初始化链', + applyFirewall: '应用防火墙', + unloadFirewall: '卸载防火墙', + sourceIPHelper: 'CIDR 格式,如 192.168.1.0/24,留空表示任意', + destIPHelper: 'CIDR 格式,如 10.0.0.0/8,留空表示任意', + portHelper: '0 表示任意端口', + deleteRuleConfirm: '将删除 {0} 条规则,是否继续?', + initChainsConfirm: '将重置 1PANEL_INPUT 和 1PANEL_OUTPUT 链为默认 ACCEPT 规则,是否继续?', + applyConfirm: '将应用自定义链到 INPUT/OUTPUT,请确保 SSH 端口(22)已放行,是否继续?', + unloadConfirm: '将从 INPUT/OUTPUT 移除自定义链,是否继续?', }, runtime: { runtime: '运行环境', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index eab0284102df..a86d8ef389b8 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -80,6 +80,16 @@ const hostRouter = { requiresAuth: false, }, }, + { + path: '/hosts/filter', + name: 'Filter', + component: () => import('@/views/host/filter/index.vue'), + meta: { + icon: 'p-filter-menu', + title: 'menu.filter', + requiresAuth: false, + }, + }, { path: '/hosts/disk', name: 'Disk', diff --git a/frontend/src/views/host/filter/index.vue b/frontend/src/views/host/filter/index.vue new file mode 100644 index 000000000000..8c613dbd9d6e --- /dev/null +++ b/frontend/src/views/host/filter/index.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/frontend/src/views/host/filter/operate/index.vue b/frontend/src/views/host/filter/operate/index.vue new file mode 100644 index 000000000000..2af02e8d314f --- /dev/null +++ b/frontend/src/views/host/filter/operate/index.vue @@ -0,0 +1,165 @@ + + + From 955123bf49085c9b5660e912f862e78ad88fb250 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:59:50 +0800 Subject: [PATCH 02/16] chore: clean up --- agent/utils/firewall/client.go | 6 - agent/utils/firewall/client/iptables.go | 142 ------ agent/utils/firewall/client/iptables_fw.go | 461 ------------------ .../utils/firewall/client/iptables_policy.go | 99 ++++ 4 files changed, 99 insertions(+), 609 deletions(-) delete mode 100644 agent/utils/firewall/client/iptables_fw.go create mode 100644 agent/utils/firewall/client/iptables_policy.go diff --git a/agent/utils/firewall/client.go b/agent/utils/firewall/client.go index f10ac7b2f7ae..447cd8697572 100644 --- a/agent/utils/firewall/client.go +++ b/agent/utils/firewall/client.go @@ -41,11 +41,5 @@ func NewFirewallClient() (FirewallClient, error) { if ufw { return client.NewUfw() } - - iptables := cmd.Which("iptables") - if iptables { - return client.NewIptablesFw() - } - return nil, errors.New("No system firewalld or ufw service detected, please check and try again!") } diff --git a/agent/utils/firewall/client/iptables.go b/agent/utils/firewall/client/iptables.go index 58ba96d282fe..b9aa6636a5e7 100644 --- a/agent/utils/firewall/client/iptables.go +++ b/agent/utils/firewall/client/iptables.go @@ -2,7 +2,6 @@ package client import ( "fmt" - "os/exec" "regexp" "strings" "time" @@ -62,11 +61,6 @@ func (iptables *Iptables) run(tab, rule string) error { return nil } -// Run 导出的 run 方法供外部调用 -func (iptables *Iptables) Run(tab, rule string) error { - return iptables.run(tab, rule) -} - func (iptables *Iptables) Check() error { stdout, err := cmd.RunDefaultWithStdoutBashC("cat /proc/sys/net/ipv4/ip_forward") if err != nil { @@ -257,32 +251,6 @@ func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPor return nil } -// test struct -/* -🧩 规则解释 - --A INPUT / -A OUTPUT:追加规则到对应链。 - --p tcp / -p udp:指定协议。 - ---dport / --sport:目标端口 / 源端口。 - --d / -s:目标 IP / 源 IP。 - --j DROP:丢弃数据包(不回应)。 - -若改成 -j REJECT,则会主动返回一个拒绝报文(对调试有用)。* -*/ -type IptablesPolicy struct { - Protocol string - SrcPort uint16 - DstPort uint16 - SourceIP string - DestIP string - Action string // ACCEPT, DROP, REJECT - Comment string -} - func (iptables *Iptables) AddPolicy(chain string, policy IptablesPolicy) error { iptablesArg := fmt.Sprintf("-A %s", chain) if policy.Protocol != "" { @@ -327,89 +295,6 @@ func (iptables *Iptables) Reload() error { return nil } -// IptablesChain -type IptablesChain struct { - Name string - DefaultPolicy string - FirstRule *IptablesPolicyChainItem - LastRule *IptablesPolicyChainItem -} - -type IptablesPolicyChainItem struct { - next *IptablesPolicyChainItem - P IptablesPolicy -} - -func (item *IptablesPolicyChainItem) SetNext(next *IptablesPolicyChainItem) { - item.next = next -} - -func (item *IptablesPolicyChainItem) Next() *IptablesPolicyChainItem { - return item.next -} - -func (c *IptablesChain) ParseLine(line string) error { - cmd := strings.Split(line, " ") - - if cmd[0] == "-P" { - c.Name = cmd[1] - c.DefaultPolicy = cmd[2] - return nil - } - if cmd[0] == "-A" { - if cmd[1] != c.Name { - return fmt.Errorf("invalid chain name in rule line: %s", line) - } - policy := IptablesPolicy{} - for i := 2; i < len(cmd); i++ { - switch cmd[i] { - case "-p": - i++ - policy.Protocol = cmd[i] - case "--dport": - i++ - // parse port - var port uint16 - fmt.Sscanf(cmd[i], "%d", &port) - policy.DstPort = port - case "--sport": - i++ - var port uint16 - fmt.Sscanf(cmd[i], "%d", &port) - policy.SrcPort = port - case "-s": - i++ - policy.SourceIP = cmd[i] - case "-d": - i++ - policy.DestIP = cmd[i] - case "-j": - i++ - policy.Action = cmd[i] - case "-m": - // skip - i++ - case "--comment": - i++ - policy.Comment = strings.Trim(cmd[i], "\"") - } - } - newItem := &IptablesPolicyChainItem{ - P: policy, - } - if c.FirstRule == nil { - c.FirstRule = newItem - c.LastRule = newItem - } else { - current := c.LastRule - current.SetNext(newItem) - c.LastRule = newItem - } - return nil - } - return fmt.Errorf("invalid iptables rule line: %s", line) -} - // iptables filter 解析 func (iptables *Iptables) ReadFilter(chainName []string) (map[string]IptablesChain, error) { // iptables -S @@ -452,33 +337,6 @@ func (iptables *Iptables) ReadFilter(chainName []string) (map[string]IptablesCha return chains, nil } -// 检查链中是否有无条件 DROP/REJECT 规则 -func checkChainSafety(chain IptablesChain) error { - item := chain.FirstRule - for item != nil { - policy := item.P - if policy.Action == "DROP" || policy.Action == "REJECT" { - // 检查是否为无条件规则(所有字段都为空) - if policy.Protocol == "" && policy.SrcPort == 0 && policy.DstPort == 0 && - policy.SourceIP == "" && policy.DestIP == "" { - return fmt.Errorf("发现无条件 %s 规则,不允许应用", policy.Action) - } - } - item = item.Next() - } - return nil -} - -// 执行 iptables 命令 -func runIptables(args ...string) error { - cmd := exec.Command("iptables", args...) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("执行 iptables 失败: %v, 输出: %s", err, string(output)) - } - return nil -} - // RemovePolicyByComment 按 comment 删除规则 func (iptables *Iptables) RemovePolicyByComment(chain, comment string) error { stdout, err := iptables.out(FilterTab, fmt.Sprintf("-S %s", chain)) diff --git a/agent/utils/firewall/client/iptables_fw.go b/agent/utils/firewall/client/iptables_fw.go deleted file mode 100644 index d63f84fbd7ff..000000000000 --- a/agent/utils/firewall/client/iptables_fw.go +++ /dev/null @@ -1,461 +0,0 @@ -package client - -import ( - "fmt" - "regexp" - "strconv" - "strings" - - "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" -) - -const ( - Chain1PanelInput = "1PFW-INPUT" - Chain1PanelOutput = "1PFW-OUTPUT" -) - -var ( - // 匹配 iptables 规则的正则表达式 - filterListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?))?(?:\s+(.+?))?`) -) - -type IptablesFw struct { - iptables *Iptables -} - -func NewIptablesFw() (*IptablesFw, error) { - iptables, err := NewIptables() - if err != nil { - return nil, err - } - return &IptablesFw{ - iptables: iptables, - }, nil -} - -func (f *IptablesFw) Name() string { - return "iptables" -} - -func (f *IptablesFw) Status() (bool, error) { - // 检查自定义链是否存在来判断防火墙是否启用 - exists, err := f.iptables.ChainExists(FilterTab, Chain1PanelInput) - if err != nil { - return false, err - } - return exists, nil -} - -func (f *IptablesFw) Version() (string, error) { - stdout, err := cmd.RunDefaultWithStdoutBashC("iptables --version") - if err != nil { - return "", fmt.Errorf("load the firewall version failed, %v", err) - } - // 提取版本号,例如 "iptables v1.8.7 (nf_tables)" - parts := strings.Fields(stdout) - if len(parts) >= 2 { - return strings.TrimPrefix(parts[1], "v"), nil - } - return strings.TrimSpace(stdout), nil -} - -func (f *IptablesFw) Start() error { - // 创建自定义链 - if err := f.iptables.NewChain(FilterTab, Chain1PanelInput); err != nil { - global.LOG.Debugf("chain %s may already exist: %v", Chain1PanelInput, err) - } - if err := f.iptables.NewChain(FilterTab, Chain1PanelOutput); err != nil { - global.LOG.Debugf("chain %s may already exist: %v", Chain1PanelOutput, err) - } - - // 将自定义链附加到 INPUT 和 OUTPUT 链 - if err := f.ensureChainAttached("INPUT", Chain1PanelInput); err != nil { - return fmt.Errorf("failed to attach %s to INPUT: %v", Chain1PanelInput, err) - } - if err := f.ensureChainAttached("OUTPUT", Chain1PanelOutput); err != nil { - return fmt.Errorf("failed to attach %s to OUTPUT: %v", Chain1PanelOutput, err) - } - - // 重载规则(从数据库恢复) - return f.Reload() -} - -func (f *IptablesFw) Stop() error { - // 从 INPUT 和 OUTPUT 链中移除跳转规则 - _ = f.iptables.Run(FilterTab, fmt.Sprintf("-D INPUT -j %s", Chain1PanelInput)) - _ = f.iptables.Run(FilterTab, fmt.Sprintf("-D OUTPUT -j %s", Chain1PanelOutput)) - - // 清空并删除自定义链 - _ = f.iptables.FlushChain(FilterTab, Chain1PanelInput) - _ = f.iptables.FlushChain(FilterTab, Chain1PanelOutput) - _ = f.iptables.Run(FilterTab, fmt.Sprintf("-X %s", Chain1PanelInput)) - _ = f.iptables.Run(FilterTab, fmt.Sprintf("-X %s", Chain1PanelOutput)) - - return nil -} - -func (f *IptablesFw) Restart() error { - if err := f.Stop(); err != nil { - return err - } - return f.Start() -} - -func (f *IptablesFw) Reload() error { - // 清空链但保留链结构 - if err := f.iptables.FlushChain(FilterTab, Chain1PanelInput); err != nil { - return err - } - if err := f.iptables.FlushChain(FilterTab, Chain1PanelOutput); err != nil { - return err - } - - // 从数据库加载规则并恢复 - var rules []model.IptablesRule - if err := global.DB.Find(&rules).Error; err != nil { - return fmt.Errorf("failed to load rules from database: %v", err) - } - - for _, rule := range rules { - fireInfo := FireInfo{ - Protocol: rule.Protocol, - Port: rule.Port, - Strategy: rule.Strategy, - Address: rule.Address, - Family: rule.Family, - } - if rule.RuleType == "port" { - if err := f.Port(fireInfo, "add"); err != nil { - global.LOG.Errorf("failed to restore port rule %+v: %v", rule, err) - } - } else if rule.RuleType == "address" { - if err := f.RichRules(fireInfo, "add"); err != nil { - global.LOG.Errorf("failed to restore address rule %+v: %v", rule, err) - } - } - } - - return nil -} - -func (f *IptablesFw) ListPort() ([]FireInfo, error) { - return f.listRules(Chain1PanelInput, "port") -} - -func (f *IptablesFw) ListAddress() ([]FireInfo, error) { - return f.listRules(Chain1PanelInput, "address") -} - -func (f *IptablesFw) ListForward() ([]FireInfo, error) { - // 复用 ufw 的转发实现 - if err := f.EnableForward(); err != nil { - global.LOG.Errorf("init port forward failed, err: %v", err) - } - - rules, err := f.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 *IptablesFw) Port(port FireInfo, operation string) error { - if cmd.CheckIllegal(operation, port.Protocol, port.Port) { - return buserr.New("ErrCmdIllegal") - } - - // 转换策略 - strategy := f.convertStrategy(port.Strategy) - if strategy == "" { - return fmt.Errorf("unsupported strategy %s", port.Strategy) - } - - // 构建规则的唯一标识 - comment := fmt.Sprintf("1Panel_Port_%s_%s_%s", port.Protocol, port.Port, strategy) - - if operation == "add" { - // 添加到 INPUT 链 - policy := IptablesPolicy{ - Protocol: port.Protocol, - DstPort: f.parsePort(port.Port), - Action: strategy, - Comment: comment, - } - if err := f.iptables.AddPolicy(Chain1PanelInput, policy); err != nil { - return fmt.Errorf("add port rule to INPUT failed: %v", err) - } - - // 保存到数据库 - rule := model.IptablesRule{ - RuleType: "port", - Protocol: port.Protocol, - Port: port.Port, - Strategy: port.Strategy, - Family: "ipv4", - } - if err := global.DB.Create(&rule).Error; err != nil { - return fmt.Errorf("save rule to database failed: %v", err) - } - } else if operation == "remove" { - // 从 INPUT 链删除 - if err := f.iptables.RemovePolicyByComment(Chain1PanelInput, comment); err != nil { - return fmt.Errorf("remove port rule from INPUT failed: %v", err) - } - - // 从数据库删除 - global.DB.Where("rule_type = ? AND protocol = ? AND port = ? AND strategy = ?", - "port", port.Protocol, port.Port, port.Strategy).Delete(&model.IptablesRule{}) - } - - return nil -} - -func (f *IptablesFw) RichRules(rule FireInfo, operation string) error { - if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port) { - return buserr.New("ErrCmdIllegal") - } - - strategy := f.convertStrategy(rule.Strategy) - if strategy == "" { - return fmt.Errorf("unsupported strategy %s", rule.Strategy) - } - - // 构建规则的唯一标识 - comment := fmt.Sprintf("1Panel_Addr_%s_%s_%s_%s", rule.Address, rule.Protocol, rule.Port, strategy) - - if operation == "add" { - policy := IptablesPolicy{ - Protocol: rule.Protocol, - SourceIP: rule.Address, - Action: strategy, - Comment: comment, - } - if len(rule.Port) > 0 { - policy.DstPort = f.parsePort(rule.Port) - } - - if err := f.iptables.AddPolicy(Chain1PanelInput, policy); err != nil { - return fmt.Errorf("add address rule failed: %v", err) - } - - // 保存到数据库 - dbRule := model.IptablesRule{ - RuleType: "address", - Protocol: rule.Protocol, - Port: rule.Port, - Strategy: rule.Strategy, - Address: rule.Address, - Family: rule.Family, - } - if dbRule.Family == "" { - dbRule.Family = "ipv4" - } - if err := global.DB.Create(&dbRule).Error; err != nil { - return fmt.Errorf("save rule to database failed: %v", err) - } - } else if operation == "remove" { - if err := f.iptables.RemovePolicyByComment(Chain1PanelInput, comment); err != nil { - return fmt.Errorf("remove address rule failed: %v", err) - } - - // 从数据库删除 - global.DB.Where("rule_type = ? AND address = ? AND protocol = ? AND port = ? AND strategy = ?", - "address", rule.Address, rule.Protocol, rule.Port, rule.Strategy).Delete(&model.IptablesRule{}) - } - - return nil -} - -func (f *IptablesFw) PortForward(info Forward, operation string) error { - // 直接复用 iptables 的 NAT 功能 - if operation == "add" { - err := f.iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true) - if err != nil { - return fmt.Errorf("add port forward failed: %v", err) - } - } else if operation == "remove" { - err := f.iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface) - if err != nil { - return fmt.Errorf("remove port forward failed: %v", err) - } - } - return nil -} - -func (f *IptablesFw) EnableForward() error { - // 复用 ufw 的转发初始化逻辑 - if err := f.iptables.Check(); err != nil { - return err - } - - _ = f.iptables.NewChain(NatTab, PreRoutingChain) - _ = f.iptables.NewChain(NatTab, PostRoutingChain) - _ = f.iptables.NewChain(FilterTab, ForwardChain) - - if err := f.enableForwardChain(); err != nil { - return err - } - return f.iptables.Reload() -} - -// 辅助方法:确保链已附加到目标链 -func (f *IptablesFw) ensureChainAttached(targetChain, customChain string) error { - // 检查是否已经附加 - rules, err := f.iptables.NatList(targetChain) - if err == nil { - for _, rule := range rules { - if rule.Target == customChain { - return nil - } - } - } - - // 附加链 - return f.iptables.AppendChain(FilterTab, targetChain, customChain) -} - -// 辅助方法:启用转发链 -func (f *IptablesFw) enableForwardChain() error { - rules, err := f.iptables.NatList("PREROUTING") - if err != nil { - return err - } - for _, rule := range rules { - if rule.Target == PreRoutingChain { - return nil - } - } - - _ = f.iptables.AppendChain(NatTab, "PREROUTING", PreRoutingChain) - _ = f.iptables.AppendChain(NatTab, "POSTROUTING", PostRoutingChain) - _ = f.iptables.AppendChain(FilterTab, "FORWARD", ForwardChain) - return nil -} - -// 辅助方法:列出规则 -func (f *IptablesFw) listRules(chain, ruleType string) ([]FireInfo, error) { - stdout, err := cmd.RunDefaultWithStdoutBashCf("%s iptables -t filter -L %s -n -v --line-numbers", f.iptables.CmdStr, chain) - if err != nil { - return nil, fmt.Errorf("list rules failed: %v", err) - } - - var datas []FireInfo - lines := strings.Split(stdout, "\n") - for i, line := range lines { - // 跳过表头 - if i < 2 || strings.TrimSpace(line) == "" { - continue - } - - fields := strings.Fields(line) - if len(fields) < 7 { - continue - } - - // 解析规则字段 - // num pkts bytes target prot opt in out source destination [extra] - num := fields[0] - target := fields[3] - protocol := fields[4] - source := fields[8] - - // 提取端口信息(从 extra 字段) - var port string - if len(fields) > 10 { - for j := 10; j < len(fields); j++ { - if strings.HasPrefix(fields[j], "dpt:") { - port = strings.TrimPrefix(fields[j], "dpt:") - } else if strings.HasPrefix(fields[j], "spt:") { - port = strings.TrimPrefix(fields[j], "spt:") - } - } - } - - // 根据类型过滤 - if ruleType == "port" { - // 端口规则:有端口信息,源地址为 0.0.0.0/0 - if len(port) > 0 && (source == "0.0.0.0/0" || source == "anywhere") { - datas = append(datas, FireInfo{ - Num: num, - Protocol: protocol, - Port: port, - Strategy: f.convertStrategyReverse(target), - Family: "ipv4", - }) - } - } else if ruleType == "address" { - // 地址规则:有源地址限制 - if source != "0.0.0.0/0" && source != "anywhere" { - datas = append(datas, FireInfo{ - Num: num, - Protocol: protocol, - Port: port, - Address: source, - Strategy: f.convertStrategyReverse(target), - Family: "ipv4", - }) - } - } - } - - return datas, nil -} - -// 辅助方法:转换策略名称 (accept/drop -> ACCEPT/DROP) -func (f *IptablesFw) convertStrategy(strategy string) string { - switch strategy { - case "accept": - return "ACCEPT" - case "drop": - return "DROP" - case "reject": - return "REJECT" - default: - return "" - } -} - -// 辅助方法:反向转换策略名称 (ACCEPT/DROP -> accept/drop) -func (f *IptablesFw) convertStrategyReverse(strategy string) string { - switch strategy { - case "ACCEPT": - return "accept" - case "DROP": - return "drop" - case "REJECT": - return "reject" - default: - return strategy - } -} - -// 辅助方法:解析端口字符串为 uint16 -func (f *IptablesFw) parsePort(portStr string) uint16 { - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return 0 - } - return uint16(port) -} diff --git a/agent/utils/firewall/client/iptables_policy.go b/agent/utils/firewall/client/iptables_policy.go new file mode 100644 index 000000000000..edce0c3b4955 --- /dev/null +++ b/agent/utils/firewall/client/iptables_policy.go @@ -0,0 +1,99 @@ +package client + +import ( + "fmt" + "strings" +) + +// IptablesChain +type IptablesChain struct { + Name string + DefaultPolicy string + FirstRule *IptablesPolicyChainItem + LastRule *IptablesPolicyChainItem +} + +func (c *IptablesChain) ParseLine(line string) error { + cmd := strings.Split(line, " ") + + if cmd[0] == "-P" { + c.Name = cmd[1] + c.DefaultPolicy = cmd[2] + return nil + } + if cmd[0] == "-A" { + if cmd[1] != c.Name { + return fmt.Errorf("invalid chain name in rule line: %s", line) + } + policy := IptablesPolicy{} + for i := 2; i < len(cmd); i++ { + switch cmd[i] { + case "-p": + i++ + policy.Protocol = cmd[i] + case "--dport": + i++ + // parse port + var port uint16 + fmt.Sscanf(cmd[i], "%d", &port) + policy.DstPort = port + case "--sport": + i++ + var port uint16 + fmt.Sscanf(cmd[i], "%d", &port) + policy.SrcPort = port + case "-s": + i++ + policy.SourceIP = cmd[i] + case "-d": + i++ + policy.DestIP = cmd[i] + case "-j": + i++ + policy.Action = cmd[i] + case "-m": + // skip + i++ + case "--comment": + i++ + policy.Comment = strings.Trim(cmd[i], "\"") + } + } + newItem := &IptablesPolicyChainItem{ + P: policy, + } + if c.FirstRule == nil { + c.FirstRule = newItem + c.LastRule = newItem + } else { + current := c.LastRule + current.SetNext(newItem) + c.LastRule = newItem + } + return nil + } + return fmt.Errorf("invalid iptables rule line: %s", line) +} + +type IptablesPolicyChainItem struct { + next *IptablesPolicyChainItem + P IptablesPolicy +} + +func (item *IptablesPolicyChainItem) SetNext(next *IptablesPolicyChainItem) { + item.next = next +} + +func (item *IptablesPolicyChainItem) Next() *IptablesPolicyChainItem { + return item.next +} + +type IptablesPolicy struct { + Protocol string + SrcPort uint16 + DstPort uint16 + SourceIP string + DestIP string + Action string // ACCEPT, DROP, REJECT + Comment string +} From 6177d40d3b2bac9946ee87bd696c578b29bf18d7 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:14:37 +0800 Subject: [PATCH 03/16] feat: improve iptables firewall --- agent/app/service/iptables_filter.go | 58 ++------ agent/utils/firewall/client/iptables.go | 64 ++++++++- frontend/src/lang/modules/en.ts | 5 + frontend/src/lang/modules/es-es.ts | 5 + frontend/src/lang/modules/ja.ts | 5 + frontend/src/lang/modules/ko.ts | 5 + frontend/src/lang/modules/ms.ts | 5 + frontend/src/lang/modules/pt-br.ts | 5 + frontend/src/lang/modules/ru.ts | 5 + frontend/src/lang/modules/tr.ts | 4 + frontend/src/lang/modules/zh-Hant.ts | 4 + frontend/src/lang/modules/zh.ts | 9 +- frontend/src/views/host/filter/index.vue | 64 ++++----- .../src/views/host/filter/operate/index.vue | 135 ++++++++++++++---- 14 files changed, 257 insertions(+), 116 deletions(-) diff --git a/agent/app/service/iptables_filter.go b/agent/app/service/iptables_filter.go index 9e4d9df2785a..6cfbe941a179 100644 --- a/agent/app/service/iptables_filter.go +++ b/agent/app/service/iptables_filter.go @@ -138,14 +138,14 @@ func (s *IptablesFilterService) GetFilterRules(chains []string) ([]dto.IptablesC func (s *IptablesFilterService) AddRule(req dto.IptablesFilterRuleOperate) error { if req.Chain != Chain1PanelInput && req.Chain != Chain1PanelOutput { - return fmt.Errorf("只允许操作 %s 或 %s 链", Chain1PanelInput, Chain1PanelOutput) + return fmt.Errorf("Only %s or %s chains are allowed", Chain1PanelInput, Chain1PanelOutput) } // 安全检查:防止无条件 DROP/REJECT if (req.Action == "DROP" || req.Action == "REJECT") && req.Protocol == "" && req.SourceIP == "" && req.DestIP == "" && req.SourcePort == 0 && req.DestPort == 0 { - return fmt.Errorf("不允许添加无条件 %s 规则,这会锁定系统", req.Action) + return fmt.Errorf("Iptables Rule Security Check: unconditional %s rules are not allowed, this may lock you out of the system", req.Action) } ctx := context.Background() @@ -298,7 +298,7 @@ func (s *IptablesFilterService) ApplyFirewall() error { if (p.Action == "DROP" || p.Action == "REJECT") && p.Protocol == "" && p.SourceIP == "" && p.DestIP == "" && p.SrcPort == 0 && p.DstPort == 0 { - return fmt.Errorf("链 %s 包含无条件 %s 规则,不允许应用", Chain1PanelInput, p.Action) + return fmt.Errorf("Chain %s includes unconditional %s rule, not allowed to apply", Chain1PanelInput, p.Action) } item = item.Next() } @@ -312,59 +312,21 @@ func (s *IptablesFilterService) ApplyFirewall() error { if (p.Action == "DROP" || p.Action == "REJECT") && p.Protocol == "" && p.SourceIP == "" && p.DestIP == "" && p.SrcPort == 0 && p.DstPort == 0 { - return fmt.Errorf("链 %s 包含无条件 %s 规则,不允许应用", Chain1PanelOutput, p.Action) + return fmt.Errorf("Chain %s includes unconditional %s rule, not allowed to apply", Chain1PanelOutput, p.Action) } item = item.Next() } } - // 检查 INPUT 链是否已有跳转规则 - inputChains, _ := s.iptablesClient.ReadFilter([]string{ChainInput}) - hasInputRule := false - if inputChain, ok := inputChains[ChainInput]; ok { - item := inputChain.FirstRule - for item != nil { - if item.P.Action == Chain1PanelInput { - hasInputRule = true - break - } - item = item.Next() - } - } - - // 应用到 INPUT 链 - if !hasInputRule { - if err := s.iptablesClient.Run(client.FilterTab, fmt.Sprintf("-I %s 1 -j %s", ChainInput, Chain1PanelInput)); err != nil { - return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelInput, ChainInput, err) - } - global.LOG.Infof("Applied %s to %s chain", Chain1PanelInput, ChainInput) - } else { - global.LOG.Infof("%s already applied to %s chain", Chain1PanelInput, ChainInput) + if err := s.iptablesClient.Setup1PanelFirewallChains("input"); err != nil { + return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelInput, ChainInput, err) } + global.LOG.Infof("Applied %s to %s chain", Chain1PanelInput, ChainInput) - // 检查 OUTPUT 链是否已有跳转规则 - outputChains, _ := s.iptablesClient.ReadFilter([]string{ChainOutput}) - hasOutputRule := false - if outputChain, ok := outputChains[ChainOutput]; ok { - item := outputChain.FirstRule - for item != nil { - if item.P.Action == Chain1PanelOutput { - hasOutputRule = true - break - } - item = item.Next() - } - } - - // 应用到 OUTPUT 链 - if !hasOutputRule { - if err := s.iptablesClient.Run(client.FilterTab, fmt.Sprintf("-I %s 1 -j %s", ChainOutput, Chain1PanelOutput)); err != nil { - return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelOutput, ChainOutput, err) - } - global.LOG.Infof("Applied %s to %s chain", Chain1PanelOutput, ChainOutput) - } else { - global.LOG.Infof("%s already applied to %s chain", Chain1PanelOutput, ChainOutput) + if err := s.iptablesClient.Setup1PanelFirewallChains("output"); err != nil { + return fmt.Errorf("failed to apply %s to %s: %w", Chain1PanelInput, ChainInput, err) } + global.LOG.Infof("Applied %s to %s chain", Chain1PanelInput, ChainInput) return nil } diff --git a/agent/utils/firewall/client/iptables.go b/agent/utils/firewall/client/iptables.go index b9aa6636a5e7..9202770b7630 100644 --- a/agent/utils/firewall/client/iptables.go +++ b/agent/utils/firewall/client/iptables.go @@ -12,9 +12,13 @@ import ( ) const ( - PreRoutingChain = "1PANEL_PREROUTING" - PostRoutingChain = "1PANEL_POSTROUTING" - ForwardChain = "1PANEL_FORWARD" + PreRoutingChain = "1PANEL_PREROUTING" + PostRoutingChain = "1PANEL_POSTROUTING" + ForwardChain = "1PANEL_FORWARD" + ChainInput = "INPUT" + ChainOutput = "OUTPUT" + Chain1PanelInput = "1PANEL_INPUT" + Chain1PanelOutput = "1PANEL_OUTPUT" ) const ( @@ -432,3 +436,57 @@ func (iptables *Iptables) DeletePolicy(chain string, policy IptablesPolicy) erro return iptables.run(FilterTab, iptablesArg) } + +// CheckPolicyExists 检查策略是否存在 +func (iptables *Iptables) CheckPolicyExists(chain string, policyStr string) bool { + err := iptables.run(FilterTab, fmt.Sprintf("-C %s %s", chain, policyStr)) + if err != nil { + return false + } + return true +} + +const ( + establishedRule = "-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" + ioRuleIn = "-i lo -j ACCEPT" + ioRuleOut = "-o lo -j ACCEPT" +) + +func (iptables *Iptables) setupEstablishedRules(direction string) { + if direction == "input" { + if !iptables.CheckPolicyExists("INPUT", ioRuleIn) { + iptables.run(FilterTab, fmt.Sprintf("-I INPUT 1 %s", ioRuleIn)) + } + + if !iptables.CheckPolicyExists("INPUT", establishedRule) { + iptables.run(FilterTab, fmt.Sprintf("-I INPUT 2 %s", establishedRule)) + } + } + + if direction == "output" { + if !iptables.CheckPolicyExists("OUTPUT", ioRuleOut) { + iptables.run(FilterTab, fmt.Sprintf("-I OUTPUT 1 %s", ioRuleOut)) + } + if !iptables.CheckPolicyExists("OUTPUT", establishedRule) { + iptables.run(FilterTab, fmt.Sprintf("-I OUTPUT 2 %s", establishedRule)) + } + } +} + +func (iptables *Iptables) Setup1PanelFirewallChains(direction string) error { + iptables.setupEstablishedRules(direction) + if direction == "input" { + if !iptables.CheckPolicyExists(ChainInput, fmt.Sprintf("-j %s", Chain1PanelInput)) { + if err := iptables.run(FilterTab, fmt.Sprintf("-I %s 3 -j %s", ChainInput, Chain1PanelInput)); err != nil { + return err + } + } + } else if direction == "output" { + if !iptables.CheckPolicyExists(ChainOutput, fmt.Sprintf("-j %s", Chain1PanelOutput)) { + if err := iptables.run(FilterTab, fmt.Sprintf("-I %s 3 -j %s", ChainOutput, Chain1PanelOutput)); err != nil { + return err + } + } + } + return nil +} diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 0089c2f735af..8920d7802f67 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2887,6 +2887,9 @@ const message = { 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', + allPorts: 'All Ports', + ruleTemplate: 'Rule template', + strategy: 'Strategy', accept: 'Accept', drop: 'Drop', @@ -2907,6 +2910,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'User-Agent filter', sourcePort: 'Source port', + inboundDirection: 'Inbound', + outboundDirection: 'Outbound', targetIP: 'Destination IP', targetPort: 'Destination port', forwardHelper1: 'If you want to forward to the local port, the destination IP should be set to "127.0.0.1".', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 26436be7397d..8dc84a1b4cb6 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2862,6 +2862,9 @@ const message = { 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', + allPorts: 'Todos los puertos', + ruleTemplate: 'Plantilla de regla', + strategy: 'Estrategia', accept: 'Aceptar', drop: 'Rechazar', @@ -2882,6 +2885,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'Filtro User-Agent', sourcePort: 'Puerto de origen', + inboundDirection: 'Entrada', + outboundDirection: 'Salida', targetIP: 'IP de destino', targetPort: 'Puerto de destino', forwardHelper1: 'Si quieres reenviar al puerto local, la IP de destino debe ser "127.0.0.1".', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 8ccfcc80c982..28e2bd229d73 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2805,6 +2805,9 @@ const message = { changeStrategyHelper: '[{1}] {0}戦略を[{2}]に変更します。設定後、{0}は外部から{2}にアクセスします。続けたいですか?', portHelper: '複数のポートを入力できます。80,81、または範囲ポート、例えば80-88', + allPorts: 'すべてのポート', + ruleTemplate: 'ルールテンプレート', + strategy: '戦略', accept: '受け入れる', drop: '落とす', @@ -2825,6 +2828,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.iprule', userAgent: 'ユーザーエージェントフィルター', sourcePort: 'ソースポート', + inboundDirection: '受信方向', + outboundDirection: '送信方向', targetIP: '宛先IP', targetPort: '宛先ポート', forwardHelper1: 'ローカルポートに転送する場合は、宛先IPを「127.0.0.1」に設定する必要があります。', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index dcbe63902a40..efe2b8aea333 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2756,6 +2756,9 @@ const message = { changeStrategyHelper: '[{1}] {0} 전략을 [{2}]로 변경합니다. 설정 후 {0}은(는) {2}로 외부 접근을 허용합니다. 계속하시겠습니까?', portHelper: '여러 포트를 입력할 수 있습니다. 예: 80, 81 또는 포트 범위, 예: 80-88', + allPorts: '모든 포트', + ruleTemplate: '규칙 템플릿', + strategy: '전략', accept: '허용', drop: '차단', @@ -2776,6 +2779,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'User-Agent 필터', sourcePort: '소스 포트', + inboundDirection: '인바운드', + outboundDirection: '아웃바운드', targetIP: '대상 IP', targetPort: '대상 포트', forwardHelper1: "로컬 포트로 전달하려면, 대상 IP 를 '127.0.0.1'로 설정해야 합니다.", diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 8d820c3ac819..bce3029bcb1a 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2870,6 +2870,9 @@ const message = { 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', + allPorts: 'Semua port', + ruleTemplate: 'Templat peraturan', + strategy: 'Strategi', accept: 'Terima', drop: 'Lumpuhkan', @@ -2890,6 +2893,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'Penapis User-Agent', sourcePort: 'Port sumber', + inboundDirection: 'Masuk', + outboundDirection: 'Keluar', targetIP: 'IP sasaran', targetPort: 'Port sasaran', forwardHelper1: 'Jika anda ingin memajukan ke port tempatan, IP sasaran harus ditetapkan kepada "127.0.0.1".', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 5ef88e7124ac..889cd3dd8872 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2875,6 +2875,9 @@ const message = { 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', + allPorts: 'Todas as portas', + ruleTemplate: 'Modelo de regra', + strategy: 'Estratégia', accept: 'Aceitar', drop: 'Bloquear', @@ -2895,6 +2898,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'Filtro User-Agent', sourcePort: 'Porta de origem', + inboundDirection: 'Entrada', + outboundDirection: 'Saída', targetIP: 'IP de destino', targetPort: 'Porta de destino', forwardHelper1: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f7f9290c7d93..e52c3ca82d8e 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2869,6 +2869,9 @@ const message = { changeStrategyHelper: 'Изменить стратегию {0} [{1}] на [{2}]. После установки {0} будет иметь внешний доступ {2}. Хотите продолжить?', portHelper: 'Можно ввести несколько портов, например 80,81, или диапазон портов, например 80-88', + allPorts: 'Все порты', + ruleTemplate: 'Шаблон правила', + strategy: 'Стратегия', accept: 'Принять', drop: 'Отбросить', @@ -2889,6 +2892,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'Фильтр User-Agent', sourcePort: 'Исходный порт', + inboundDirection: 'Входящий', + outboundDirection: 'Исходящий', targetIP: 'Целевой IP', targetPort: 'Целевой порт', forwardHelper1: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index b08f4f3501af..643271e6963a 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2928,6 +2928,8 @@ const message = { 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', + allPorts: 'Tüm portlar', + ruleTemplate: 'Kural şablonu', strategy: 'Strateji', accept: 'Kabul Et', drop: 'Reddet', @@ -2948,6 +2950,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'Kullanıcı-Aracısı filtresi', sourcePort: 'Kaynak port', + inboundDirection: 'Gelen', + outboundDirection: 'Giden', targetIP: 'Hedef IP', targetPort: 'Hedef port', forwardHelper1: 'Yerel porta yönlendirmek istiyorsanız, hedef IP "127.0.0.1" olarak ayarlanmalıdır.', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 7846a019b566..69eccdae0911 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -2679,6 +2679,8 @@ const message = { portFormatError: '請輸入正確的埠資訊!', portHelper1: '多個埠,如:8080,8081', portHelper2: '範圍埠,如:8080-8089', + allPorts: '所有埠', + ruleTemplate: '規則模板', strategy: '策略', accept: '允許', drop: '拒絕', @@ -2699,6 +2701,8 @@ const message = { createIpRule: '@:commons.button.create @:firewall.ipRule', userAgent: 'User-Agent 過濾', sourcePort: '來源埠', + inboundDirection: '入站方向', + outboundDirection: '出站方向', targetIP: '目標 IP', targetPort: '目標埠', forwardHelper1: '如果是本機埠轉發,目標 IP 為:127.0.0.1', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fb9e9d47ccf7..b2613704d29b 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2712,8 +2712,9 @@ const message = { chainHelper: '仅允许修改 1PANEL_INPUT 和 1PANEL_OUTPUT 自定义链', ruleOrder: '顺序', sourceIP: '源 IP', - sourcePort: '源端口', destIP: '目标 IP', + inboundDirection: '入站方向', + outboundDirection: '出站方向', destPort: '目标端口', action: '动作', reject: '拒绝', @@ -2723,9 +2724,11 @@ const message = { initChains: '初始化链', applyFirewall: '应用防火墙', unloadFirewall: '卸载防火墙', - sourceIPHelper: 'CIDR 格式,如 192.168.1.0/24,留空表示任意', - destIPHelper: 'CIDR 格式,如 10.0.0.0/8,留空表示任意', + sourceIPHelper: 'CIDR 格式,如 192.168.1.0/24,留空表所有地址', + destIPHelper: 'CIDR 格式,如 10.0.0.0/留空表所有地址', portHelper: '0 表示任意端口', + allPorts: '所有端口', + ruleTemplate: '规则模板', deleteRuleConfirm: '将删除 {0} 条规则,是否继续?', initChainsConfirm: '将重置 1PANEL_INPUT 和 1PANEL_OUTPUT 链为默认 ACCEPT 规则,是否继续?', applyConfirm: '将应用自定义链到 INPUT/OUTPUT,请确保 SSH 端口(22)已放行,是否继续?', diff --git a/frontend/src/views/host/filter/index.vue b/frontend/src/views/host/filter/index.vue index 8c613dbd9d6e..df46693b818c 100644 --- a/frontend/src/views/host/filter/index.vue +++ b/frontend/src/views/host/filter/index.vue @@ -5,17 +5,14 @@
{{ 'iptables v' + iptablesVersion }} + + {{ inputChainInfo?.isApplied ? $t('firewall.applied') : $t('firewall.notApplied') }} +
- -