Files
1Panel/agent/init/migration/migrations/utils/host_firewall_transfer.go
T
ssongliu e119239d3d refactor: manage firewall rules through runtime state and file backups (#13998)
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations.

Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
2026-10-08 18:19:30 +08:00

435 lines
14 KiB
Go

package utils
import (
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
"gorm.io/gorm"
)
var errUnsupportedLegacyHostFirewallRule = errors.New("unsupported legacy host firewall rule")
type legacyHostFirewallRecord struct {
ID uint
Type string
Port string
Address string
Chain string
Protocol string
SrcIP string
SrcPort string
DstIP string
DstPort string
Strategy string
Description string
}
type legacyFirewallRuleDescription struct {
UUID string
Provider string
ScopeKey string
Location string
Family string
NativeKind string
Protocol string
SourceAddress string
SourcePort string
DestinationAddress string
DestinationPort string
Interface string
ConnectionStates string
CompatibilityError string
Action string
Priority *int
Description string
Owner string
}
func MigrateHostFirewallDescriptions(db *gorm.DB) error {
providers := []filter.Provider{filter.ProviderIptables, filter.ProviderNftables, filter.ProviderFirewalld, filter.ProviderUFW}
descriptions := make(map[string][]string)
add := func(id, description string) {
if description != "" && !slices.Contains(descriptions[id], description) {
descriptions[id] = append(descriptions[id], description)
}
}
addRules := func(rules []filter.FirewallRule, description string) bool {
matched := false
for _, rule := range rules {
var err error
switch rule.Scope.Provider {
case filter.ProviderFirewalld:
rule, err = (&filterfirewalld.Adapter{}).PrepareRule(rule)
case filter.ProviderUFW:
rule, err = (&filterufw.Adapter{}).PrepareRule(rule)
}
if err != nil {
continue
}
id, err := filter.DescriptionID(filter.ObservedRule{Rule: rule, ParseStatus: filter.ParseStatusSupported})
if err == nil {
add(id, description)
matched = true
}
}
return matched
}
if db.Migrator().HasTable("firewalls") {
var records []legacyHostFirewallRecord
if err := db.Table("firewalls").Order("id ASC").Find(&records).Error; err != nil {
return err
}
for _, record := range records {
if record.Description == "" {
continue
}
matched := false
for _, provider := range providers {
rules, err := legacyHostFirewallRules(record, provider)
if err == nil && addRules(rules, record.Description) {
matched = true
}
}
if !matched {
add(fmt.Sprintf("firewall:legacy:firewalls:%d", record.ID), record.Description)
}
}
}
if db.Migrator().HasTable("firewall_rules") {
var records []legacyFirewallRuleDescription
if err := db.Table("firewall_rules").Order("uuid ASC").Find(&records).Error; err != nil {
return err
}
for _, record := range records {
if record.Description == "" {
continue
}
matched := false
for _, provider := range providers {
rules, err := legacyFirewallDescriptionRules(record, provider)
if err == nil && addRules(rules, record.Description) {
matched = true
}
}
if !matched {
add("firewall:legacy:firewall_rules:"+record.UUID, record.Description)
}
}
}
if len(descriptions) == 0 {
return nil
}
var existing []model.CommonDescription
if err := db.Where("type = ?", "firewall").Find(&existing).Error; err != nil {
return err
}
byID := make(map[string]model.CommonDescription, len(existing))
for _, description := range existing {
byID[description.ID] = description
}
ids := make([]string, 0, len(descriptions))
for id := range descriptions {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
value := strings.Join(descriptions[id], "\n")
if current, ok := byID[id]; ok {
if current.Description == value {
continue
}
if current.Description == "" {
if err := db.Model(&model.CommonDescription{}).Where("id = ?", id).Update("description", value).Error; err != nil {
return err
}
continue
}
id = "firewall:legacy:description:" + strings.TrimPrefix(id, "firewall:")
if _, ok := byID[id]; ok {
continue
}
}
record := model.CommonDescription{ID: id, Type: "firewall", Description: value}
if strings.HasPrefix(id, "firewall:legacy:") {
record.DetailType = "legacy"
}
if err := db.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func legacyFirewallDescriptionRules(record legacyFirewallRuleDescription, provider filter.Provider) ([]filter.FirewallRule, error) {
if record.CompatibilityError != "" {
return nil, errUnsupportedLegacyHostFirewallRule
}
source := filter.Provider(strings.ToLower(strings.TrimSpace(record.Provider)))
scopeParts := strings.Split(record.ScopeKey, ":")
if source == "" && len(scopeParts) > 1 {
source = filter.Provider(scopeParts[0])
}
if source != "" && source != provider {
return nil, nil
}
base := filter.FirewallRule{
Protocol: record.Protocol, SourceAddress: record.SourceAddress, SourcePort: record.SourcePort,
DestinationAddress: record.DestinationAddress, DestinationPort: record.DestinationPort,
Interface: record.Interface, Action: filter.Action(record.Action),
}
if record.ConnectionStates != "" {
base.ConnectionStates = strings.Split(record.ConnectionStates, ",")
}
if source != "" {
base.NativeKind = filter.NativeKind(record.NativeKind)
}
switch base.NativeKind {
case filter.NativeKindOpaque, filter.NativeKindZoneService, filter.NativeKindUFWApplication:
return nil, errUnsupportedLegacyHostFirewallRule
}
if provider != filter.ProviderUFW && strings.EqualFold(base.Protocol, "all") && base.SourcePort == "" && base.DestinationPort != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = record.Priority
}
families := []filter.Family{filter.Family(record.Family)}
if families[0] == "" {
families[0] = legacyRuleFamily(base.SourceAddress, base.DestinationAddress)
}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
if base.SourceAddress != "" || base.DestinationAddress != "" || base.Protocol == "icmpv6" {
families = []filter.Family{legacyRuleFamily(base.SourceAddress, base.DestinationAddress)}
if base.Protocol == "icmpv6" {
families[0] = filter.FamilyIPv6
}
} else {
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
var result []filter.FirewallRule
for _, family := range families {
rule := base
rule.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
rule.Scope.Table, rule.Scope.Chain = "filter", filter.IptablesInputChain
if source != "" {
if record.Location != "" {
rule.Scope.Chain = record.Location
}
if len(scopeParts) == 5 {
rule.Scope.Table, rule.Scope.Chain = scopeParts[2], scopeParts[3]
}
}
case filter.ProviderFirewalld:
rule.Scope.Zone = filter.FirewalldInputZone
if source != "" && record.Location != "" {
rule.Scope.Zone = record.Location
}
case filter.ProviderUFW:
rule.Scope.Chain = filter.UFWInputChain
}
expanded, err := filter.ExpandAtomicRules(rule)
if err != nil {
return nil, err
}
result = append(result, expanded...)
if source == "" && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) && strings.HasPrefix(record.Owner, constant.FirewallRuleSourceSecurity+":"+constant.FirewallSystemAcceptedPortSourcePrefix) {
for _, item := range expanded {
item.Scope.Chain = filter.BasicBeforeChain
result = append(result, item)
}
}
}
return result, nil
}
func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Provider) ([]filter.FirewallRule, error) {
sourceAddress := record.SrcIP
if sourceAddress == "" {
sourceAddress = record.Address
}
destinationPort := record.DstPort
if destinationPort == "" {
destinationPort = record.Port
}
rule := filter.FirewallRule{
Protocol: record.Protocol,
SourceAddress: sourceAddress,
SourcePort: record.SrcPort,
DestinationAddress: record.DstIP,
DestinationPort: destinationPort,
Action: filter.Action(record.Strategy),
Description: record.Description,
}
switch strings.ToLower(strings.TrimSpace(record.Type)) {
case "port":
rule.SourcePort = ""
rule.DestinationAddress = ""
rule.DestinationPort = destinationPort
case "address", "ip":
rule.Protocol = "all"
rule.SourcePort = ""
rule.DestinationPort = ""
default:
if provider != filter.ProviderIptables || legacyIptablesAdvancedChain(record.Chain) {
return nil, fmt.Errorf("%w: advanced rule for provider %q", errUnsupportedLegacyHostFirewallRule, provider)
}
}
switch provider {
case filter.ProviderIptables:
rule.Scope = filter.Scope{
Provider: provider, Family: legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress), Table: "filter",
Chain: legacyIptablesChain(record), Direction: filter.DirectionInput,
}
rule.NativeKind = filter.NativeKindRule
case filter.ProviderFirewalld:
return legacyFirewalldHostRules(record, rule)
case filter.ProviderUFW:
return legacyUFWHostRules(record, rule)
default:
return nil, fmt.Errorf("%w: provider %q", errUnsupportedLegacyHostFirewallRule, provider)
}
return filter.ExpandAtomicRules(rule)
}
func legacyIptablesAdvancedChain(chain string) bool {
switch strings.ToUpper(strings.TrimSpace(chain)) {
case "1PANEL_INPUT", "1PANEL_OUTPUT":
return true
default:
return false
}
}
func legacyIptablesChain(record legacyHostFirewallRecord) string {
typeName := strings.ToLower(strings.TrimSpace(record.Type))
if typeName == "port" || typeName == "address" || typeName == "ip" {
return filter.IptablesInputChain
}
return strings.TrimSpace(record.Chain)
}
func legacyFirewalldHostRules(record legacyHostFirewallRecord, rule filter.FirewallRule) ([]filter.FirewallRule, error) {
rule.Scope = filter.Scope{
Provider: filter.ProviderFirewalld, Zone: filter.FirewalldInputZone, Direction: filter.DirectionInput,
}
typeName := strings.ToLower(strings.TrimSpace(record.Type))
if typeName == "port" && legacyActionIsAccept(record.Strategy) && legacyAddressIsEmpty(record.SrcIP) {
rule.Scope.Family = filter.FamilyInet
rule.NativeKind = filter.NativeKindZonePort
return filter.ExpandAtomicRules(rule)
}
rule.NativeKind = filter.NativeKindRichRule
if legacyAddressIsEmpty(rule.SourceAddress) && legacyAddressIsEmpty(rule.DestinationAddress) {
return expandLegacyFamilies(rule, filter.FamilyIPv4, filter.FamilyIPv6)
}
rule.Scope.Family = legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress)
return filter.ExpandAtomicRules(rule)
}
func legacyUFWHostRules(record legacyHostFirewallRecord, rule filter.FirewallRule) ([]filter.FirewallRule, error) {
rule.Scope = filter.Scope{
Provider: filter.ProviderUFW, Chain: filter.UFWInputChain, Direction: filter.DirectionInput,
}
rule.NativeKind = filter.NativeKindUFWRule
if strings.EqualFold(strings.TrimSpace(record.Type), "address") || strings.EqualFold(strings.TrimSpace(record.Type), "ip") {
rule.SourceAddress, rule.DestinationAddress = splitLegacyUFWAddress(rule.SourceAddress)
}
if legacyUFWSinglePortAllProtocols(record, rule.DestinationPort) {
rule.Protocol = "all"
}
if legacyAddressIsEmpty(rule.SourceAddress) && legacyAddressIsEmpty(rule.DestinationAddress) {
rule.Scope.Family = filter.FamilyInet
} else {
rule.Scope.Family = legacyRuleFamily(rule.SourceAddress, rule.DestinationAddress)
}
return filter.ExpandAtomicRules(rule)
}
func legacyUFWSinglePortAllProtocols(record legacyHostFirewallRecord, port string) bool {
if !strings.EqualFold(strings.TrimSpace(record.Type), "port") {
return false
}
protocol := strings.ToLower(strings.TrimSpace(record.Protocol))
if protocol != "tcp/udp" && protocol != "udp/tcp" {
return false
}
port = strings.TrimSpace(port)
return port != "" && !strings.Contains(port, ",") && !strings.Contains(port, "-")
}
func expandLegacyFamilies(rule filter.FirewallRule, families ...filter.Family) ([]filter.FirewallRule, error) {
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
item := rule
item.Scope.Family = family
expanded, err := filter.ExpandAtomicRules(item)
if err != nil {
return nil, err
}
result = append(result, expanded...)
}
return result, nil
}
func legacyActionIsAccept(action string) bool {
switch strings.ToLower(strings.TrimSpace(action)) {
case "accept", "allow":
return true
default:
return false
}
}
func legacyAddressIsEmpty(address string) bool {
address = strings.ToLower(strings.TrimSpace(address))
return address == "" || address == "any" || strings.HasPrefix(address, "anywhere")
}
func legacyRuleFamily(addresses ...string) filter.Family {
for _, value := range addresses {
value = strings.TrimSpace(value)
if prefix, err := netip.ParsePrefix(value); err == nil {
if prefix.Addr().Is6() && !prefix.Addr().Is4In6() {
return filter.FamilyIPv6
}
continue
}
if address, err := netip.ParseAddr(value); err == nil && address.Is6() && !address.Is4In6() {
return filter.FamilyIPv6
}
}
return filter.FamilyIPv4
}
func splitLegacyUFWAddress(value string) (string, string) {
value = strings.TrimSpace(value)
if source, destination, ok := strings.Cut(value, "-"); ok && legacyIPOrPrefix(source) && legacyIPOrPrefix(destination) {
return strings.TrimSpace(source), strings.TrimSpace(destination)
}
return value, ""
}
func legacyIPOrPrefix(value string) bool {
value = strings.TrimSpace(value)
if _, err := netip.ParseAddr(value); err == nil {
return true
}
_, err := netip.ParsePrefix(value)
return err == nil
}