mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 08:00:55 +00:00
374 lines
11 KiB
Go
374 lines
11 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const firewallTransferMigrationID = "firewall-transfer"
|
|
|
|
type legacyFirewalldForward struct {
|
|
rule forwarding.Rule
|
|
spec string
|
|
}
|
|
|
|
type legacyFirewalldForwardFailure struct {
|
|
spec string
|
|
err error
|
|
}
|
|
|
|
type firewallTransferSource struct {
|
|
rules []forwarding.Rule
|
|
firewalld []legacyFirewalldForward
|
|
provider string
|
|
cleanupOld func([]legacyFirewalldForward) error
|
|
}
|
|
|
|
type firewallTransfer struct {
|
|
db *gorm.DB
|
|
load func() (firewallTransferSource, error)
|
|
}
|
|
|
|
// TransferFirewallForwarding migrates the legacy system-backed forwarding
|
|
// inventory into the forwarding_rules table once. The migrations table is
|
|
// also used as the completion ledger so a failed transfer can be retried on
|
|
// the next agent start without introducing another persisted setting.
|
|
func TransferFirewallForwarding(ctx context.Context) error {
|
|
transfer := &firewallTransfer{
|
|
db: global.DB,
|
|
load: loadLegacyFirewallForwarding,
|
|
}
|
|
return transfer.run(ctx)
|
|
}
|
|
|
|
func (t *firewallTransfer) run(ctx context.Context) error {
|
|
if t.db == nil {
|
|
return errors.New("firewall transfer database is required")
|
|
}
|
|
completed, err := firewallTransferCompleted(t.db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if completed {
|
|
return nil
|
|
}
|
|
if t.load == nil {
|
|
return errors.New("legacy firewall forwarding loader is required")
|
|
}
|
|
source, err := t.load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := importLegacyForwardingRules(ctx, t.db, source.rules, source.provider); err != nil {
|
|
return err
|
|
}
|
|
if len(source.firewalld) > 0 {
|
|
if source.cleanupOld == nil {
|
|
return errors.New("legacy firewalld cleanup is required")
|
|
}
|
|
if err := source.cleanupOld(source.firewalld); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return markFirewallTransferCompleted(t.db)
|
|
}
|
|
|
|
func firewallTransferCompleted(db *gorm.DB) (bool, error) {
|
|
return migrationRecordExists(db, firewallTransferMigrationID)
|
|
}
|
|
|
|
func markFirewallTransferCompleted(db *gorm.DB) error {
|
|
return markMigrationRecord(db, firewallTransferMigrationID)
|
|
}
|
|
|
|
func migrationRecordExists(db *gorm.DB, migrationID string) (bool, error) {
|
|
var count int64
|
|
if err := db.Table("migrations").Where("id = ?", migrationID).Count(&count).Error; err != nil {
|
|
return false, fmt.Errorf("check migration record %q: %w", migrationID, err)
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func markMigrationRecord(db *gorm.DB, migrationID string) error {
|
|
if err := db.Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
|
|
Create(map[string]interface{}{"id": migrationID}).Error; err != nil {
|
|
return fmt.Errorf("record migration %q: %w", migrationID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func importLegacyForwardingRules(ctx context.Context, db *gorm.DB, rules []forwarding.Rule, provider string) error {
|
|
models := make([]model.ForwardingRule, 0, len(rules))
|
|
seen := make(map[string]struct{}, len(rules))
|
|
for _, rule := range rules {
|
|
normalized, err := forwarding.NormalizeRule(rule)
|
|
if err != nil {
|
|
return fmt.Errorf("normalize legacy forwarding rule: %w", err)
|
|
}
|
|
key := normalized.Identity()
|
|
if _, exists := seen[key]; exists {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
models = append(models, model.ForwardingRule{
|
|
Family: normalized.Family, Protocol: normalized.Protocol, Port: normalized.Port,
|
|
TargetIP: normalized.TargetIP, TargetPort: normalized.TargetPort, Interface: normalized.Interface,
|
|
})
|
|
}
|
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if len(models) > 0 {
|
|
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&models).Error; err != nil {
|
|
return fmt.Errorf("import legacy forwarding rules: %w", err)
|
|
}
|
|
if err := updateOrCreateSetting(tx, "IptablesForwardStatus", constant.StatusEnable); err != nil {
|
|
return err
|
|
}
|
|
if provider != "" {
|
|
if err := updateOrCreateSetting(tx, "ForwardingBackend", provider); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func updateOrCreateSetting(tx *gorm.DB, key, value string) error {
|
|
result := tx.Model(&model.Setting{}).Where("key = ?", key).Update("value", value)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected > 0 {
|
|
return nil
|
|
}
|
|
return tx.Create(&model.Setting{Key: key, Value: value}).Error
|
|
}
|
|
|
|
func loadLegacyFirewallForwarding() (firewallTransferSource, error) {
|
|
source := firewallTransferSource{cleanupOld: cleanupLegacyFirewalldForwarding}
|
|
if cmd.Which("firewall-cmd") {
|
|
firewalldRules, err := listLegacyFirewalldForwarding()
|
|
if err != nil {
|
|
return firewallTransferSource{}, err
|
|
}
|
|
supportedRules, cleanupRules, failures := selectSupportedLegacyFirewalldForwarding(firewalldRules)
|
|
for _, failure := range failures {
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
|
|
}
|
|
}
|
|
source.rules = append(source.rules, supportedRules...)
|
|
source.firewalld = append(source.firewalld, cleanupRules...)
|
|
if len(source.rules) > 0 {
|
|
source.provider = "iptables"
|
|
}
|
|
return source, nil
|
|
}
|
|
if _, err := lifecycle.ResolveIptablesCommands(); err != nil {
|
|
return source, nil
|
|
}
|
|
rules, err := listLegacyIptablesForwarding()
|
|
if err != nil {
|
|
return firewallTransferSource{}, err
|
|
}
|
|
source.rules = append(source.rules, rules...)
|
|
return source, nil
|
|
}
|
|
|
|
func selectSupportedLegacyFirewalldForwarding(items []legacyFirewalldForward) (
|
|
[]forwarding.Rule, []legacyFirewalldForward, []legacyFirewalldForwardFailure,
|
|
) {
|
|
rules := make([]forwarding.Rule, 0, len(items))
|
|
cleanup := make([]legacyFirewalldForward, 0, len(items))
|
|
failures := make([]legacyFirewalldForwardFailure, 0)
|
|
for _, item := range items {
|
|
if _, err := forwarding.NormalizeRule(item.rule); err != nil {
|
|
failures = append(failures, legacyFirewalldForwardFailure{spec: item.spec, err: err})
|
|
continue
|
|
}
|
|
rules = append(rules, item.rule)
|
|
cleanup = append(cleanup, item)
|
|
}
|
|
return rules, cleanup, failures
|
|
}
|
|
|
|
func listLegacyIptablesForwarding() ([]forwarding.Rule, error) {
|
|
exists, err := iptables_helper.CheckChainExist(iptables_helper.NatTab, forwarding.ChainPreRouting)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
stdout, err := iptables_helper.RunWithStd(
|
|
iptables_helper.NatTab, "-nvL", forwarding.ChainPreRouting, "--line-numbers",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list legacy iptables forwarding rules: %w", err)
|
|
}
|
|
return parseLegacyIptablesForwarding(stdout), nil
|
|
}
|
|
|
|
func parseLegacyIptablesForwarding(stdout string) []forwarding.Rule {
|
|
rules := make([]forwarding.Rule, 0)
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 13 {
|
|
continue
|
|
}
|
|
rule := forwarding.Rule{
|
|
Family: forwarding.FamilyIPv4, Protocol: loadLegacyIptablesProtocol(fields[4]),
|
|
Interface: fields[6], Port: loadLegacyIptablesSourcePort(fields[11]),
|
|
}
|
|
if len(fields) == 15 && fields[13] == "ports" {
|
|
rule.TargetPort = fields[14]
|
|
}
|
|
if len(fields) == 13 && strings.HasPrefix(fields[12], "to:") {
|
|
target := strings.TrimPrefix(fields[12], "to:")
|
|
separator := strings.LastIndex(target, ":")
|
|
if separator > 0 {
|
|
rule.TargetIP, rule.TargetPort = target[:separator], target[separator+1:]
|
|
}
|
|
}
|
|
if rule.TargetIP == "" {
|
|
rule.TargetIP = "127.0.0.1"
|
|
}
|
|
rule.TargetPort = strings.TrimPrefix(rule.TargetPort, ":")
|
|
rules = append(rules, rule)
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func loadLegacyIptablesProtocol(protocol string) string {
|
|
switch protocol {
|
|
case "6":
|
|
return "tcp"
|
|
case "17":
|
|
return "udp"
|
|
default:
|
|
return protocol
|
|
}
|
|
}
|
|
|
|
func loadLegacyIptablesSourcePort(value string) string {
|
|
port := ""
|
|
if strings.Contains(value, "dpt:") {
|
|
port = strings.ReplaceAll(value, "dpt:", "")
|
|
}
|
|
if strings.Contains(value, "dpts:") {
|
|
port = strings.ReplaceAll(value, "dpts:", "")
|
|
}
|
|
return strings.ReplaceAll(port, ":", "-")
|
|
}
|
|
|
|
func listLegacyFirewalldForwarding() ([]legacyFirewalldForward, error) {
|
|
stdout, err := cmd.NewCommandMgr().RunWithStdout(
|
|
"firewall-cmd", "--permanent", "--zone=public", "--list-forward-ports",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list legacy firewalld forwarding rules: %w", err)
|
|
}
|
|
rules, failures := parseLegacyFirewalldForwarding(stdout)
|
|
for _, failure := range failures {
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf("skip unsupported legacy firewalld forwarding rule %q: %v", failure.spec, failure.err)
|
|
}
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
func parseLegacyFirewalldForwarding(stdout string) ([]legacyFirewalldForward, []legacyFirewalldForwardFailure) {
|
|
result := make([]legacyFirewalldForward, 0)
|
|
failures := make([]legacyFirewalldForwardFailure, 0)
|
|
for _, spec := range strings.Fields(stdout) {
|
|
item, err := parseLegacyFirewalldForward(spec)
|
|
if err != nil {
|
|
failures = append(failures, legacyFirewalldForwardFailure{spec: spec, err: err})
|
|
continue
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
return result, failures
|
|
}
|
|
|
|
func parseLegacyFirewalldForward(spec string) (legacyFirewalldForward, error) {
|
|
if !strings.HasPrefix(spec, "port=") {
|
|
return legacyFirewalldForward{}, errors.New("missing port field")
|
|
}
|
|
port, rest, ok := strings.Cut(strings.TrimPrefix(spec, "port="), ":proto=")
|
|
if !ok {
|
|
return legacyFirewalldForward{}, errors.New("missing protocol field")
|
|
}
|
|
protocol, rest, ok := strings.Cut(rest, ":toport=")
|
|
if !ok {
|
|
return legacyFirewalldForward{}, errors.New("missing target port field")
|
|
}
|
|
targetPort, targetIP, ok := strings.Cut(rest, ":toaddr=")
|
|
if !ok {
|
|
return legacyFirewalldForward{}, errors.New("missing target address field")
|
|
}
|
|
if targetIP == "" {
|
|
targetIP = "127.0.0.1"
|
|
}
|
|
return legacyFirewalldForward{
|
|
rule: forwarding.Rule{
|
|
Family: forwarding.FamilyIPv4, Protocol: protocol, Port: port,
|
|
TargetIP: targetIP, TargetPort: targetPort,
|
|
},
|
|
spec: spec,
|
|
}, nil
|
|
}
|
|
|
|
func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
|
|
manager := cmd.NewCommandMgr()
|
|
for _, item := range rules {
|
|
if err := manager.Run(
|
|
"firewall-cmd", "--permanent", "--zone=public", "--remove-forward-port="+item.spec,
|
|
); err != nil {
|
|
return fmt.Errorf("remove legacy firewalld forwarding rule %q: %w", item.spec, err)
|
|
}
|
|
}
|
|
if len(rules) == 0 {
|
|
return nil
|
|
}
|
|
if err := manager.Run("firewall-cmd", "--reload"); err != nil {
|
|
return fmt.Errorf("reload firewalld after forwarding transfer: %w", err)
|
|
}
|
|
return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart)
|
|
}
|
|
|
|
func restartDockerAfterFirewalldReload(
|
|
which func(string) bool,
|
|
checkActive func(string) (bool, error),
|
|
restart func(string) error,
|
|
) error {
|
|
const service = "docker"
|
|
if !which(service) {
|
|
return nil
|
|
}
|
|
active, err := checkActive(service)
|
|
if err != nil {
|
|
return fmt.Errorf("check Docker status after reloading firewalld: %w", err)
|
|
}
|
|
if !active {
|
|
return nil
|
|
}
|
|
if err := restart(service); err != nil {
|
|
return fmt.Errorf("restart Docker after reloading firewalld: %w", err)
|
|
}
|
|
return nil
|
|
}
|