From 15bd16f7098c3d8e5d9842e5032baf28d2d0e9d5 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Wed, 16 Sep 2026 16:47:16 +0800 Subject: [PATCH] refactor(firewall): consolidate utilities and flatten packages --- agent/app/service/firewall.go | 7 +- agent/app/service/firewall_setting.go | 3 +- agent/app/service/forward.go | 7 +- agent/init/firewall/firewall.go | 5 +- agent/init/migration/migrations/init.go | 4 +- .../firewall/docker_guard/forward_policy.go | 42 --- .../utils/firewall/docker_guard/ip_forward.go | 27 -- agent/utils/firewall/docker_guard/nftables.go | 35 +++ agent/utils/firewall/docker_guard/policy.go | 249 +++++++++++++++++ .../firewall/docker_guard/policy_inspect.go | 258 ------------------ agent/utils/firewall/docker_guard/runtime.go | 22 ++ agent/utils/firewall/filter/adopt.go | 25 -- agent/utils/firewall/filter/identity.go | 24 ++ agent/utils/firewall/filter/inventory.go | 37 +++ .../firewall/filter/inventory_position.go | 38 --- agent/utils/firewall/filter/observed.go | 25 -- .../filter/providers/firewalld/adapter.go | 32 +++ .../filter/providers/firewalld/create.go | 35 --- .../filter/providers/iptables/adapter.go | 52 ++++ .../filter/providers/iptables/observation.go | 61 ----- agent/utils/firewall/filter/runtime/create.go | 40 --- .../utils/firewall/filter/runtime/runtime.go | 33 +++ agent/utils/firewall/filter/safety.go | 22 ++ agent/utils/firewall/forwarding/forwarding.go | 11 + .../forwarding/{providers => }/iptables.go | 121 ++++---- .../forwarding/{providers => }/nftables.go | 87 +++--- .../firewall/forwarding/providers/factory.go | 18 -- .../forwarding/{providers => }/sysctl.go | 2 +- .../utils/firewall/iptables_helper/command.go | 19 ++ agent/utils/firewall/iptables_helper/read.go | 29 -- agent/utils/firewall/nftables_helper/read.go | 16 -- .../utils/firewall/nftables_helper/runtime.go | 14 + agent/utils/firewall/{ping => }/ping.go | 6 +- 33 files changed, 668 insertions(+), 738 deletions(-) delete mode 100644 agent/utils/firewall/docker_guard/forward_policy.go delete mode 100644 agent/utils/firewall/docker_guard/ip_forward.go delete mode 100644 agent/utils/firewall/docker_guard/policy_inspect.go delete mode 100644 agent/utils/firewall/filter/adopt.go delete mode 100644 agent/utils/firewall/filter/inventory_position.go delete mode 100644 agent/utils/firewall/filter/observed.go delete mode 100644 agent/utils/firewall/filter/providers/firewalld/create.go delete mode 100644 agent/utils/firewall/filter/providers/iptables/observation.go delete mode 100644 agent/utils/firewall/filter/runtime/create.go rename agent/utils/firewall/forwarding/{providers => }/iptables.go (79%) rename agent/utils/firewall/forwarding/{providers => }/nftables.go (74%) delete mode 100644 agent/utils/firewall/forwarding/providers/factory.go rename agent/utils/firewall/forwarding/{providers => }/sysctl.go (99%) delete mode 100644 agent/utils/firewall/iptables_helper/read.go delete mode 100644 agent/utils/firewall/nftables_helper/read.go rename agent/utils/firewall/{ping => }/ping.go (97%) diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 39ed1b2abc8a..9f413e393ffc 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -27,7 +27,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" "github.com/google/uuid" "gorm.io/gorm" @@ -144,7 +143,7 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem return status, err } status.Name, status.Backend = runtimeStatus.Name, runtimeStatus.Name - status.Version, status.PingStatus = runtimeStatus.Version, ping.LoadStatus() + status.Version, status.PingStatus = runtimeStatus.Version, firewall.LoadPingStatus() status.IsActive = runtimeStatus.IsActive if supportsManagedFilterChains(runtimeStatus.Name) { initialized, bound, err := loadFirewallInitStatus(runtimeStatus.Name, chainGroup) @@ -341,12 +340,12 @@ func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycl func (s *FirewallService) OperateFirewall(request dto.FirewallLifecycleOperation) error { switch request.Operation { case "disableBanPing": - if err := ping.UpdateStatus("0"); err != nil { + if err := firewall.UpdatePingStatus("0"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusDisable) case "enableBanPing": - if err := ping.UpdateStatus("1"); err != nil { + if err := firewall.UpdatePingStatus("1"); err != nil { return err } return settingRepo.Update(constant.FirewallPingStatusKey, constant.StatusEnable) diff --git a/agent/app/service/firewall_setting.go b/agent/app/service/firewall_setting.go index 251b3f45fa06..c5c1fe674c6b 100644 --- a/agent/app/service/firewall_setting.go +++ b/agent/app/service/firewall_setting.go @@ -26,7 +26,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" "github.com/google/uuid" "gorm.io/gorm" ) @@ -874,7 +873,7 @@ func (service *FirewallService) syncSystemAccessPortTransition(ctx context.Conte } func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) { - result := dto.FirewallSettings{PingStatus: ping.LoadStatus()} + result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()} installed := make(map[string]bool) for _, name := range lifecycle.InstalledProviders() { diff --git a/agent/app/service/forward.go b/agent/app/service/forward.go index 0d3e058a482f..10f3b9ef2512 100644 --- a/agent/app/service/forward.go +++ b/agent/app/service/forward.go @@ -16,10 +16,9 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" - forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" ) type IForwardingService interface { @@ -91,7 +90,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) baseInfo.IsExist = true baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name baseInfo.Version = status.Version - baseInfo.PingStatus = ping.LoadStatus() + baseInfo.PingStatus = firewall.LoadPingStatus() baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4) baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6) @@ -574,7 +573,7 @@ func newForwardingManagerFor(backend string) (*forwarding.Manager, error) { errForwardingBackendUnavailable, backend, err, ) } - adapter, err := forwardingproviders.New(client.Name()) + adapter, err := forwarding.New(client.Name()) if err != nil { return nil, err } diff --git a/agent/init/firewall/firewall.go b/agent/init/firewall/firewall.go index 010e9f1ce7a2..f27b356f4fb5 100644 --- a/agent/init/firewall/firewall.go +++ b/agent/init/firewall/firewall.go @@ -16,7 +16,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" ) func Init() { @@ -154,7 +153,7 @@ func needInit() bool { func InitPingStatus() { global.LOG.Info("initializing ban ping status from settings...") - status := ping.LoadStatus() + status := firewall.LoadPingStatus() statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing") if statusInDB == status { return @@ -164,7 +163,7 @@ func InitPingStatus() { if statusInDB == constant.StatusDisable { enable = "0" } - if err := ping.UpdateStatus(enable); err != nil { + if err := firewall.UpdatePingStatus(enable); err != nil { global.LOG.Errorf("initialize ping status failed: %v", err) } } diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 2a6d7d327c92..7cc0a91edf58 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -23,7 +23,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/encrypt" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/ping" + "github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/ssh" "github.com/1Panel-dev/1Panel/agent/utils/xpack" @@ -1131,7 +1131,7 @@ var AddisIPtoWebsiteSSL = &gormigrate.Migration{ var InitPingStatus = &gormigrate.Migration{ ID: "20251201-init-ping-status", Migrate: func(tx *gorm.DB) error { - status := ping.LoadStatus() + status := firewall.LoadPingStatus() if err := tx.Create(&model.Setting{Key: "BanPing", Value: status}).Error; err != nil { return err } diff --git a/agent/utils/firewall/docker_guard/forward_policy.go b/agent/utils/firewall/docker_guard/forward_policy.go deleted file mode 100644 index 3a923b847eb9..000000000000 --- a/agent/utils/firewall/docker_guard/forward_policy.go +++ /dev/null @@ -1,42 +0,0 @@ -package docker_guard - -import ( - "errors" - "fmt" - "strings" -) - -var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP") - -func (m *NftablesManager) checkForwardPolicy() error { - for _, family := range []struct{ command, name string }{ - {"iptables", FamilyIPv4}, - {"ip6tables", FamilyIPv6}, - } { - if !m.runner.Exists(family.command) { - continue - } - output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD") - if err != nil { - return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)} - } - found := false - for _, line := range strings.Split(output, "\n") { - fields := strings.Fields(line) - if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" { - continue - } - found = true - if fields[2] == "DROP" { - return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop} - } - if fields[2] != "ACCEPT" { - return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} - } - } - if !found { - return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")} - } - } - return nil -} diff --git a/agent/utils/firewall/docker_guard/ip_forward.go b/agent/utils/firewall/docker_guard/ip_forward.go deleted file mode 100644 index 562c8b3f1ae5..000000000000 --- a/agent/utils/firewall/docker_guard/ip_forward.go +++ /dev/null @@ -1,27 +0,0 @@ -package docker_guard - -import ( - "errors" - "fmt" - "os" - "strings" -) - -const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward" - -var ErrIPv4ForwardingDisabled = errors.New("IPv4 forwarding is disabled; set net.ipv4.ip_forward=1 before using Docker's firewall backend") - -func CheckIPv4Forwarding() error { - return checkIPv4Forwarding(os.ReadFile) -} - -func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error { - value, err := readFile(ipv4ForwardingPath) - if err != nil { - return fmt.Errorf("inspect IPv4 forwarding: %w", err) - } - if strings.TrimSpace(string(value)) != "1" { - return ErrIPv4ForwardingDisabled - } - return nil -} diff --git a/agent/utils/firewall/docker_guard/nftables.go b/agent/utils/firewall/docker_guard/nftables.go index b8435e662cbf..b459a4171eff 100644 --- a/agent/utils/firewall/docker_guard/nftables.go +++ b/agent/utils/firewall/docker_guard/nftables.go @@ -496,3 +496,38 @@ func nftHasFirstUniqueJump(output string) bool { } return false } + +var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP") + +func (m *NftablesManager) checkForwardPolicy() error { + for _, family := range []struct{ command, name string }{ + {"iptables", FamilyIPv4}, + {"ip6tables", FamilyIPv6}, + } { + if !m.runner.Exists(family.command) { + continue + } + output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD") + if err != nil { + return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)} + } + found := false + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(line) + if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" { + continue + } + found = true + if fields[2] == "DROP" { + return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop} + } + if fields[2] != "ACCEPT" { + return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])} + } + } + if !found { + return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")} + } + } + return nil +} diff --git a/agent/utils/firewall/docker_guard/policy.go b/agent/utils/firewall/docker_guard/policy.go index 131f88ea1be7..03aa77884547 100644 --- a/agent/utils/firewall/docker_guard/policy.go +++ b/agent/utils/firewall/docker_guard/policy.go @@ -8,6 +8,8 @@ import ( "sort" "strconv" "strings" + + "github.com/mattn/go-shellwords" ) var ErrInvalidPolicy = errors.New("invalid Docker port guard request") @@ -144,3 +146,250 @@ func DecodeSources(value string) []string { _ = json.Unmarshal([]byte(value), &result) return result } + +type observedPolicy struct { + policy Policy + sequence int64 + nativeRules []NativeRule + managedOrders []int64 + dropAll bool + droppedSource []string + allowedSource []string + acceptedSource []string + acceptAll bool +} + +func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { + groups := make(map[string]*observedPolicy) + order := make([]string, 0) + sequence := int64(0) + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + tokens, err := shellwords.Parse(line) + if err != nil { + return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err) + } + managed := strings.Contains(line, "1panel-docker:") + if !managed && !hasAcceptAction(tokens) { + continue + } + sequence++ + fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family) + if err != nil { + return PolicyInventory{}, err + } + identity := fragment.UUID + if action == "accept" { + identity = action + } + key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|") + group, exists := groups[key] + if !exists { + group = &observedPolicy{policy: fragment, sequence: sequence} + groups[key] = group + order = append(order, key) + } + switch { + case action == "accept" && source != "": + group.acceptedSource = append(group.acceptedSource, source) + group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) + case action == "accept": + group.acceptAll = true + group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) + case action == "return" && source != "": + group.allowedSource = append(group.allowedSource, source) + case action == "drop" && source != "": + group.droppedSource = append(group.droppedSource, source) + case action == "drop": + group.dropAll = true + default: + return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action) + } + if action != "accept" { + group.managedOrders = append(group.managedOrders, sequence) + } + } + inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)} + for _, key := range order { + group := groups[key] + if group.acceptAll || len(group.acceptedSource) > 0 { + group.policy.Sources = uniqueSortedStrings(group.acceptedSource) + inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{ + Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules, + }) + continue + } + switch { + case len(group.allowedSource) > 0: + group.policy.Mode = ModeAllow + group.policy.Sources = uniqueSortedStrings(group.allowedSource) + case len(group.droppedSource) > 0: + group.policy.Mode = ModeSources + group.policy.Sources = uniqueSortedStrings(group.droppedSource) + case group.dropAll: + group.policy.Mode = ModeAll + default: + return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID) + } + inventory.Policies = append(inventory.Policies, group.policy) + inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...) + } + return inventory, nil +} + +func nativeRuleTokens(tokens []string) []string { + result := make([]string, 0, len(tokens)) + for index, token := range tokens { + if token == "#" { + tokens = tokens[:index] + break + } + } + if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" { + tokens = tokens[:len(tokens)-2] + } + for index := 0; index < len(tokens); index++ { + result = append(result, tokens[index]) + if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" { + index += 4 + } + } + return result +} + +func managedOrderKey(family, policyUUID string) string { + return family + "\x00" + policyUUID +} + +func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) { + policy := Policy{Family: family, HostIP: wildcardHost(family)} + source, action := "", "" + for index := 0; index < len(tokens); index++ { + switch tokens[index] { + case "-p": + policy.Protocol = nextPolicyToken(tokens, index) + case "--ctorigdst": + policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index)) + case "-d": + policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index)) + case "--ctorigdstport": + policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index)) + case "--dport": + policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index)) + case "-s": + source = nextPolicyToken(tokens, index) + case "--comment", "comment": + marker := nextPolicyToken(tokens, index) + if strings.HasPrefix(marker, "1panel-docker:") { + policy.UUID = strings.TrimPrefix(marker, "1panel-docker:") + } + case "-j": + action = strings.ToLower(nextPolicyToken(tokens, index)) + case "meta": + if nextPolicyToken(tokens, index) == "l4proto" { + policy.Protocol = nextPolicyToken(tokens, index+1) + } + case "ct": + if nextPolicyToken(tokens, index) != "original" { + continue + } + switch nextPolicyToken(tokens, index+1) { + case "proto-dst": + policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2)) + case "ip", "ip6": + if nextPolicyToken(tokens, index+2) == "daddr" { + policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3)) + } + } + case "ip", "ip6": + switch nextPolicyToken(tokens, index) { + case "saddr": + source = nextPolicyToken(tokens, index+1) + case "daddr": + policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1)) + } + case "tcp", "udp": + if nextPolicyToken(tokens, index) == "dport" { + policy.Protocol = tokens[index] + policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1)) + } + case "accept", "drop", "return": + if isCommentValue(tokens, index) { + continue + } + action = tokens[index] + } + } + if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) { + return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule") + } + if action == "accept" && policy.Protocol == "" { + policy.Protocol = "all" + } + return policy, source, action, nil +} + +func hasAcceptAction(tokens []string) bool { + for index, token := range tokens { + if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") { + return true + } + if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) { + return true + } + } + return false +} + +func isCommentValue(tokens []string, index int) bool { + if index == 0 { + return false + } + return tokens[index-1] == "comment" || tokens[index-1] == "--comment" +} + +func normalizeObservedHost(value string) string { + if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() { + return prefix.Addr().String() + } + return value +} + +func nextPolicyToken(tokens []string, index int) string { + if index+1 >= len(tokens) { + return "" + } + return tokens[index+1] +} + +func parsePolicyPort(value string) uint16 { + port, err := strconv.ParseUint(value, 10, 16) + if err != nil || port == 0 { + return 0 + } + return uint16(port) +} + +func wildcardHost(family string) string { + if family == FamilyIPv6 { + return "::" + } + return "0.0.0.0" +} + +func uniqueSortedStrings(values []string) []string { + seen := make(map[string]struct{}, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + if _, exists := seen[value]; exists { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Strings(result) + return result +} diff --git a/agent/utils/firewall/docker_guard/policy_inspect.go b/agent/utils/firewall/docker_guard/policy_inspect.go deleted file mode 100644 index 54615f2f6199..000000000000 --- a/agent/utils/firewall/docker_guard/policy_inspect.go +++ /dev/null @@ -1,258 +0,0 @@ -package docker_guard - -import ( - "fmt" - "net/netip" - "sort" - "strconv" - "strings" - - "github.com/mattn/go-shellwords" -) - -type observedPolicy struct { - policy Policy - sequence int64 - nativeRules []NativeRule - managedOrders []int64 - dropAll bool - droppedSource []string - allowedSource []string - acceptedSource []string - acceptAll bool -} - -func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) { - groups := make(map[string]*observedPolicy) - order := make([]string, 0) - sequence := int64(0) - for _, line := range strings.Split(output, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - tokens, err := shellwords.Parse(line) - if err != nil { - return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err) - } - managed := strings.Contains(line, "1panel-docker:") - if !managed && !hasAcceptAction(tokens) { - continue - } - sequence++ - fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family) - if err != nil { - return PolicyInventory{}, err - } - identity := fragment.UUID - if action == "accept" { - identity = action - } - key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|") - group, exists := groups[key] - if !exists { - group = &observedPolicy{policy: fragment, sequence: sequence} - groups[key] = group - order = append(order, key) - } - switch { - case action == "accept" && source != "": - group.acceptedSource = append(group.acceptedSource, source) - group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) - case action == "accept": - group.acceptAll = true - group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)}) - case action == "return" && source != "": - group.allowedSource = append(group.allowedSource, source) - case action == "drop" && source != "": - group.droppedSource = append(group.droppedSource, source) - case action == "drop": - group.dropAll = true - default: - return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action) - } - if action != "accept" { - group.managedOrders = append(group.managedOrders, sequence) - } - } - inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)} - for _, key := range order { - group := groups[key] - if group.acceptAll || len(group.acceptedSource) > 0 { - group.policy.Sources = uniqueSortedStrings(group.acceptedSource) - inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{ - Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules, - }) - continue - } - switch { - case len(group.allowedSource) > 0: - group.policy.Mode = ModeAllow - group.policy.Sources = uniqueSortedStrings(group.allowedSource) - case len(group.droppedSource) > 0: - group.policy.Mode = ModeSources - group.policy.Sources = uniqueSortedStrings(group.droppedSource) - case group.dropAll: - group.policy.Mode = ModeAll - default: - return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID) - } - inventory.Policies = append(inventory.Policies, group.policy) - inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...) - } - return inventory, nil -} - -func nativeRuleTokens(tokens []string) []string { - result := make([]string, 0, len(tokens)) - for index, token := range tokens { - if token == "#" { - tokens = tokens[:index] - break - } - } - if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" { - tokens = tokens[:len(tokens)-2] - } - for index := 0; index < len(tokens); index++ { - result = append(result, tokens[index]) - if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" { - index += 4 - } - } - return result -} - -func managedOrderKey(family, policyUUID string) string { - return family + "\x00" + policyUUID -} - -func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) { - policy := Policy{Family: family, HostIP: wildcardHost(family)} - source, action := "", "" - for index := 0; index < len(tokens); index++ { - switch tokens[index] { - case "-p": - policy.Protocol = nextPolicyToken(tokens, index) - case "--ctorigdst": - policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index)) - case "-d": - policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index)) - case "--ctorigdstport": - policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index)) - case "--dport": - policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index)) - case "-s": - source = nextPolicyToken(tokens, index) - case "--comment", "comment": - marker := nextPolicyToken(tokens, index) - if strings.HasPrefix(marker, "1panel-docker:") { - policy.UUID = strings.TrimPrefix(marker, "1panel-docker:") - } - case "-j": - action = strings.ToLower(nextPolicyToken(tokens, index)) - case "meta": - if nextPolicyToken(tokens, index) == "l4proto" { - policy.Protocol = nextPolicyToken(tokens, index+1) - } - case "ct": - if nextPolicyToken(tokens, index) != "original" { - continue - } - switch nextPolicyToken(tokens, index+1) { - case "proto-dst": - policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2)) - case "ip", "ip6": - if nextPolicyToken(tokens, index+2) == "daddr" { - policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3)) - } - } - case "ip", "ip6": - switch nextPolicyToken(tokens, index) { - case "saddr": - source = nextPolicyToken(tokens, index+1) - case "daddr": - policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1)) - } - case "tcp", "udp": - if nextPolicyToken(tokens, index) == "dport" { - policy.Protocol = tokens[index] - policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1)) - } - case "accept", "drop", "return": - if isCommentValue(tokens, index) { - continue - } - action = tokens[index] - } - } - if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) { - return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule") - } - if action == "accept" && policy.Protocol == "" { - policy.Protocol = "all" - } - return policy, source, action, nil -} - -func hasAcceptAction(tokens []string) bool { - for index, token := range tokens { - if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") { - return true - } - if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) { - return true - } - } - return false -} - -func isCommentValue(tokens []string, index int) bool { - if index == 0 { - return false - } - return tokens[index-1] == "comment" || tokens[index-1] == "--comment" -} - -func normalizeObservedHost(value string) string { - if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() { - return prefix.Addr().String() - } - return value -} - -func nextPolicyToken(tokens []string, index int) string { - if index+1 >= len(tokens) { - return "" - } - return tokens[index+1] -} - -func parsePolicyPort(value string) uint16 { - port, err := strconv.ParseUint(value, 10, 16) - if err != nil || port == 0 { - return 0 - } - return uint16(port) -} - -func wildcardHost(family string) string { - if family == FamilyIPv6 { - return "::" - } - return "0.0.0.0" -} - -func uniqueSortedStrings(values []string) []string { - seen := make(map[string]struct{}, len(values)) - result := make([]string, 0, len(values)) - for _, value := range values { - if _, exists := seen[value]; exists { - continue - } - seen[value] = struct{}{} - result = append(result, value) - } - sort.Strings(result) - return result -} diff --git a/agent/utils/firewall/docker_guard/runtime.go b/agent/utils/firewall/docker_guard/runtime.go index 3516b4f132f2..6531b248663c 100644 --- a/agent/utils/firewall/docker_guard/runtime.go +++ b/agent/utils/firewall/docker_guard/runtime.go @@ -1,8 +1,11 @@ package docker_guard import ( + "errors" "fmt" + "os" "slices" + "strings" "github.com/1Panel-dev/1Panel/agent/constant" ) @@ -144,3 +147,22 @@ func ReconcileTarget(backend string, policies []Policy, runtime Runtime) error { } return nil } + +const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward" + +var ErrIPv4ForwardingDisabled = errors.New("IPv4 forwarding is disabled; set net.ipv4.ip_forward=1 before using Docker's firewall backend") + +func CheckIPv4Forwarding() error { + return checkIPv4Forwarding(os.ReadFile) +} + +func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error { + value, err := readFile(ipv4ForwardingPath) + if err != nil { + return fmt.Errorf("inspect IPv4 forwarding: %w", err) + } + if strings.TrimSpace(string(value)) != "1" { + return ErrIPv4ForwardingDisabled + } + return nil +} diff --git a/agent/utils/firewall/filter/adopt.go b/agent/utils/firewall/filter/adopt.go deleted file mode 100644 index 039b0eb73145..000000000000 --- a/agent/utils/firewall/filter/adopt.go +++ /dev/null @@ -1,25 +0,0 @@ -package filter - -import "fmt" - -var ErrDuplicateAdoption = fmt.Errorf("%w: duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", ErrRuleOperation) - -func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error { - count := 0 - for _, observed := range snapshot.Rules { - if observed.ParseStatus != ParseStatusSupported { - continue - } - same, err := SameRuleContent(observed.Rule, requested) - if err != nil { - return err - } - if same { - count++ - if count > 1 { - return ErrDuplicateAdoption - } - } - } - return nil -} diff --git a/agent/utils/firewall/filter/identity.go b/agent/utils/firewall/filter/identity.go index 3091d43c5ad5..690033a11654 100644 --- a/agent/utils/firewall/filter/identity.go +++ b/agent/utils/firewall/filter/identity.go @@ -319,3 +319,27 @@ func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, er } return matched[0], nil } + +const ObservedFieldProtocol = "protocol" + +// ObservedRuleMatchesExpected compares the semantic fields that the backend +// could actually observe. Partial rules name fields omitted by their native +// listing; those fields are supplied from the expected rule before comparing +// normalized identities. +func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool { + if observed.ParseStatus == ParseStatusOpaque { + return false + } + hydrated := observed.Rule + for _, field := range observed.UncertainFields { + switch field { + case ObservedFieldProtocol: + hydrated.Protocol = expected.Protocol + default: + return false + } + } + gotKey, gotErr := RuleKey(hydrated) + wantKey, wantErr := RuleKey(expected) + return gotErr == nil && wantErr == nil && gotKey == wantKey +} diff --git a/agent/utils/firewall/filter/inventory.go b/agent/utils/firewall/filter/inventory.go index 0ed27c3f06e5..3ad473ce0d1a 100644 --- a/agent/utils/firewall/filter/inventory.go +++ b/agent/utils/firewall/filter/inventory.go @@ -304,3 +304,40 @@ func inventoryStateForDesired(desired DesiredRule, match InventoryMatch) Invento return InventoryStateManaged } } + +func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) { + if provider == ProviderFirewalld { + return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767} + } + for _, item := range items { + if item.Observed == nil || item.Observed.Locator.Position == nil { + continue + } + scope := item.Observed.Rule.Scope + if scope.Provider != provider || scope.Direction != DirectionInput { + continue + } + if (provider == ProviderIptables || provider == ProviderNftables) && + (scope.Table != "filter" || scope.Chain != IptablesInputChain) { + continue + } + bounds := &ipv4 + if scope.Family == FamilyIPv6 { + bounds = &ipv6 + } else if scope.Family != FamilyIPv4 { + continue + } + position := *item.Observed.Locator.Position + if position < 1 { + continue + } + if bounds.Min == 0 || position < bounds.Min { + bounds.Min = position + } + bounds.Max = max(bounds.Max, position) + if provider != ProviderUFW { + bounds.Min = 1 + } + } + return +} diff --git a/agent/utils/firewall/filter/inventory_position.go b/agent/utils/firewall/filter/inventory_position.go deleted file mode 100644 index 688ff8f84105..000000000000 --- a/agent/utils/firewall/filter/inventory_position.go +++ /dev/null @@ -1,38 +0,0 @@ -package filter - -func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) { - if provider == ProviderFirewalld { - return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767} - } - for _, item := range items { - if item.Observed == nil || item.Observed.Locator.Position == nil { - continue - } - scope := item.Observed.Rule.Scope - if scope.Provider != provider || scope.Direction != DirectionInput { - continue - } - if (provider == ProviderIptables || provider == ProviderNftables) && - (scope.Table != "filter" || scope.Chain != IptablesInputChain) { - continue - } - bounds := &ipv4 - if scope.Family == FamilyIPv6 { - bounds = &ipv6 - } else if scope.Family != FamilyIPv4 { - continue - } - position := *item.Observed.Locator.Position - if position < 1 { - continue - } - if bounds.Min == 0 || position < bounds.Min { - bounds.Min = position - } - bounds.Max = max(bounds.Max, position) - if provider != ProviderUFW { - bounds.Min = 1 - } - } - return -} diff --git a/agent/utils/firewall/filter/observed.go b/agent/utils/firewall/filter/observed.go deleted file mode 100644 index a1f458e3c838..000000000000 --- a/agent/utils/firewall/filter/observed.go +++ /dev/null @@ -1,25 +0,0 @@ -package filter - -const ObservedFieldProtocol = "protocol" - -// ObservedRuleMatchesExpected compares the semantic fields that the backend -// could actually observe. Partial rules name fields omitted by their native -// listing; those fields are supplied from the expected rule before comparing -// normalized identities. -func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool { - if observed.ParseStatus == ParseStatusOpaque { - return false - } - hydrated := observed.Rule - for _, field := range observed.UncertainFields { - switch field { - case ObservedFieldProtocol: - hydrated.Protocol = expected.Protocol - default: - return false - } - } - gotKey, gotErr := RuleKey(hydrated) - wantKey, wantErr := RuleKey(expected) - return gotErr == nil && wantErr == nil && gotKey == wantKey -} diff --git a/agent/utils/firewall/filter/providers/firewalld/adapter.go b/agent/utils/firewall/filter/providers/firewalld/adapter.go index 1e28e0030624..2428b9ceb8fa 100644 --- a/agent/utils/firewall/filter/providers/firewalld/adapter.go +++ b/agent/utils/firewall/filter/providers/firewalld/adapter.go @@ -1093,3 +1093,35 @@ func validateSystemCommand(command filter.NativeCommand) error { } return fmt.Errorf("%w: firewalld command must target the managed input zone", filter.ErrUnsupportedScope) } + +type createPlanner struct { + adapter *Adapter + snapshot filter.Snapshot + byCanonical map[string][]filter.ObservedRule +} + +func (a *Adapter) NewCreatePlanner(snapshot filter.Snapshot) filter.CreatePlanner { + byCanonical := make(map[string][]filter.ObservedRule, len(snapshot.Rules)) + for _, observed := range snapshot.Rules { + byCanonical[observed.Locator.Canonical] = append(byCanonical[observed.Locator.Canonical], observed) + } + snapshot.Rules = nil + return &createPlanner{adapter: a, snapshot: snapshot, byCanonical: byCanonical} +} + +func (p *createPlanner) Compile(change filter.DesiredChange) (filter.BackendPlan, error) { + if change.Operation != filter.ChangeCreate || change.After == nil { + return filter.BackendPlan{}, filter.ErrInvalidRule + } + rule, err := p.adapter.PrepareRule(*change.After) + if err != nil { + return filter.BackendPlan{}, err + } + snapshot := p.snapshot + snapshot.Rules = p.byCanonical[nativeCanonical(rule)] + return p.adapter.Compile(snapshot, []filter.DesiredChange{change}) +} + +func (p *createPlanner) Applied(rule filter.ObservedRule) { + p.byCanonical[rule.Locator.Canonical] = []filter.ObservedRule{rule} +} diff --git a/agent/utils/firewall/filter/providers/firewalld/create.go b/agent/utils/firewall/filter/providers/firewalld/create.go deleted file mode 100644 index e4f5a26f27e8..000000000000 --- a/agent/utils/firewall/filter/providers/firewalld/create.go +++ /dev/null @@ -1,35 +0,0 @@ -package firewalld - -import "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" - -type createPlanner struct { - adapter *Adapter - snapshot filter.Snapshot - byCanonical map[string][]filter.ObservedRule -} - -func (a *Adapter) NewCreatePlanner(snapshot filter.Snapshot) filter.CreatePlanner { - byCanonical := make(map[string][]filter.ObservedRule, len(snapshot.Rules)) - for _, observed := range snapshot.Rules { - byCanonical[observed.Locator.Canonical] = append(byCanonical[observed.Locator.Canonical], observed) - } - snapshot.Rules = nil - return &createPlanner{adapter: a, snapshot: snapshot, byCanonical: byCanonical} -} - -func (p *createPlanner) Compile(change filter.DesiredChange) (filter.BackendPlan, error) { - if change.Operation != filter.ChangeCreate || change.After == nil { - return filter.BackendPlan{}, filter.ErrInvalidRule - } - rule, err := p.adapter.PrepareRule(*change.After) - if err != nil { - return filter.BackendPlan{}, err - } - snapshot := p.snapshot - snapshot.Rules = p.byCanonical[nativeCanonical(rule)] - return p.adapter.Compile(snapshot, []filter.DesiredChange{change}) -} - -func (p *createPlanner) Applied(rule filter.ObservedRule) { - p.byCanonical[rule.Locator.Canonical] = []filter.ObservedRule{rule} -} diff --git a/agent/utils/firewall/filter/providers/iptables/adapter.go b/agent/utils/firewall/filter/providers/iptables/adapter.go index e992c01844f2..860894e0110d 100644 --- a/agent/utils/firewall/filter/providers/iptables/adapter.go +++ b/agent/utils/firewall/filter/providers/iptables/adapter.go @@ -959,3 +959,55 @@ func takeValue(args []string, index *int, target *string) bool { *target = args[*index] return true } + +type tableReader interface { + ListTable(context.Context, filter.Scope) (string, error) +} + +type tableRead struct { + output string + err error +} + +type tableObservationReader struct { + reader tableReader + tables map[string]tableRead +} + +func (r *tableObservationReader) ListChain(ctx context.Context, scope filter.Scope) (string, error) { + if err := ctx.Err(); err != nil { + return "", err + } + key := string(scope.Family) + ":" + scope.Table + read, exists := r.tables[key] + if !exists { + read.output, read.err = r.reader.ListTable(ctx, scope) + r.tables[key] = read + } + return chainOutput(scope, read.output, read.err) +} + +func (a *Adapter) NewObservationSession() filter.Adapter { + reader, ok := a.reader.(tableReader) + if !ok { + return a + } + return &Adapter{ + reader: &tableObservationReader{reader: reader, tables: make(map[string]tableRead)}, + writer: a.writer, checker: a.checker, + } +} + +func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) { + return native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6) +} + +func chainOutput(scope filter.Scope, output string, err error) (string, error) { + if err != nil { + return "", err + } + if !containsChainDeclaration(output, scope.Chain) { + return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain) + } + return output, nil +} diff --git a/agent/utils/firewall/filter/providers/iptables/observation.go b/agent/utils/firewall/filter/providers/iptables/observation.go deleted file mode 100644 index 8745306a8ad5..000000000000 --- a/agent/utils/firewall/filter/providers/iptables/observation.go +++ /dev/null @@ -1,61 +0,0 @@ -package iptables - -import ( - "context" - "fmt" - - "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" - native "github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper" -) - -type tableReader interface { - ListTable(context.Context, filter.Scope) (string, error) -} - -type tableRead struct { - output string - err error -} - -type tableObservationReader struct { - reader tableReader - tables map[string]tableRead -} - -func (r *tableObservationReader) ListChain(ctx context.Context, scope filter.Scope) (string, error) { - if err := ctx.Err(); err != nil { - return "", err - } - key := string(scope.Family) + ":" + scope.Table - read, exists := r.tables[key] - if !exists { - read.output, read.err = r.reader.ListTable(ctx, scope) - r.tables[key] = read - } - return chainOutput(scope, read.output, read.err) -} - -func (a *Adapter) NewObservationSession() filter.Adapter { - reader, ok := a.reader.(tableReader) - if !ok { - return a - } - return &Adapter{ - reader: &tableObservationReader{reader: reader, tables: make(map[string]tableRead)}, - writer: a.writer, checker: a.checker, - } -} - -func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) { - return native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6) -} - -func chainOutput(scope filter.Scope, output string, err error) (string, error) { - if err != nil { - return "", err - } - if !containsChainDeclaration(output, scope.Chain) { - return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain) - } - return output, nil -} diff --git a/agent/utils/firewall/filter/runtime/create.go b/agent/utils/firewall/filter/runtime/create.go deleted file mode 100644 index 58cf7efe9fa2..000000000000 --- a/agent/utils/firewall/filter/runtime/create.go +++ /dev/null @@ -1,40 +0,0 @@ -package runtime - -import ( - "context" - - "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" -) - -func (e *Engine) NewCreatePlanner(snapshot filter.Snapshot) (filter.CreatePlanner, error) { - factory, ok := e.adapter.(filter.CreatePlannerFactory) - if !ok { - return nil, filter.ErrAdapterUnavailable - } - return factory.NewCreatePlanner(snapshot), nil -} - -func (e *Engine) ExecutePlannedCreate(ctx context.Context, planner filter.CreatePlanner, change filter.DesiredChange) (filter.ObservedRule, error) { - if err := ctx.Err(); err != nil { - return filter.ObservedRule{}, err - } - plan, err := planner.Compile(change) - if err != nil { - return filter.ObservedRule{}, err - } - if !plan.CreatesOnly() { - return filter.ObservedRule{}, filter.ErrInvalidRule - } - InvalidateInventory() - defer InvalidateInventory() - plan.CommandOnly = change.CommandOnly - result, err := e.adapter.Apply(ctx, plan) - if err != nil { - return filter.ObservedRule{}, err - } - if len(result.Applied) != 1 { - return filter.ObservedRule{}, filter.ErrVerificationFailed - } - planner.Applied(result.Applied[0]) - return result.Applied[0], nil -} diff --git a/agent/utils/firewall/filter/runtime/runtime.go b/agent/utils/firewall/filter/runtime/runtime.go index d97785eba48a..75dce91b6b3f 100644 --- a/agent/utils/firewall/filter/runtime/runtime.go +++ b/agent/utils/firewall/filter/runtime/runtime.go @@ -369,3 +369,36 @@ func compiledRuleUUID(policyUUID, ruleKey string, scopeOrdinal int) string { } return fmt.Sprintf("%s-%d-%s", policyUUID, scopeOrdinal+1, ruleKey) } + +func (e *Engine) NewCreatePlanner(snapshot filter.Snapshot) (filter.CreatePlanner, error) { + factory, ok := e.adapter.(filter.CreatePlannerFactory) + if !ok { + return nil, filter.ErrAdapterUnavailable + } + return factory.NewCreatePlanner(snapshot), nil +} + +func (e *Engine) ExecutePlannedCreate(ctx context.Context, planner filter.CreatePlanner, change filter.DesiredChange) (filter.ObservedRule, error) { + if err := ctx.Err(); err != nil { + return filter.ObservedRule{}, err + } + plan, err := planner.Compile(change) + if err != nil { + return filter.ObservedRule{}, err + } + if !plan.CreatesOnly() { + return filter.ObservedRule{}, filter.ErrInvalidRule + } + InvalidateInventory() + defer InvalidateInventory() + plan.CommandOnly = change.CommandOnly + result, err := e.adapter.Apply(ctx, plan) + if err != nil { + return filter.ObservedRule{}, err + } + if len(result.Applied) != 1 { + return filter.ObservedRule{}, filter.ErrVerificationFailed + } + planner.Applied(result.Applied[0]) + return result.Applied[0], nil +} diff --git a/agent/utils/firewall/filter/safety.go b/agent/utils/firewall/filter/safety.go index e9eed11895db..4357c752d07f 100644 --- a/agent/utils/firewall/filter/safety.go +++ b/agent/utils/firewall/filter/safety.go @@ -251,3 +251,25 @@ func portInterval(value string) (int, int, error) { end, err := strconv.Atoi(parts[1]) return start, end, err } + +var ErrDuplicateAdoption = fmt.Errorf("%w: duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", ErrRuleOperation) + +func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error { + count := 0 + for _, observed := range snapshot.Rules { + if observed.ParseStatus != ParseStatusSupported { + continue + } + same, err := SameRuleContent(observed.Rule, requested) + if err != nil { + return err + } + if same { + count++ + if count > 1 { + return ErrDuplicateAdoption + } + } + } + return nil +} diff --git a/agent/utils/firewall/forwarding/forwarding.go b/agent/utils/firewall/forwarding/forwarding.go index 1f2f13b85d05..6b340e62f890 100644 --- a/agent/utils/firewall/forwarding/forwarding.go +++ b/agent/utils/firewall/forwarding/forwarding.go @@ -75,6 +75,17 @@ type Manager struct { runtime RuntimeClient } +func New(provider string) (Adapter, error) { + switch provider { + case "iptables": + return newIptablesNATAdapter(provider), nil + case "nftables": + return newNftablesAdapter(), nil + default: + return nil, errors.New("unsupported forwarding provider: " + provider) + } +} + func NewManager(adapter Adapter, runtime RuntimeClient) *Manager { return &Manager{adapter: adapter, runtime: runtime} } diff --git a/agent/utils/firewall/forwarding/providers/iptables.go b/agent/utils/firewall/forwarding/iptables.go similarity index 79% rename from agent/utils/firewall/forwarding/providers/iptables.go rename to agent/utils/firewall/forwarding/iptables.go index 662cbb0dfec9..db6e15262676 100644 --- a/agent/utils/firewall/forwarding/providers/iptables.go +++ b/agent/utils/firewall/forwarding/iptables.go @@ -1,4 +1,4 @@ -package providers +package forwarding import ( "context" @@ -13,7 +13,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall" - "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" "github.com/mattn/go-shellwords" @@ -65,7 +64,7 @@ func (systemIptablesBackend) Restore(family, input string) error { return err } executable := commands.Restore4 - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { executable = commands.Restore6 if executable == "" { return fmt.Errorf("ip6tables-restore command family is unavailable") @@ -122,12 +121,12 @@ func (l *iptablesNATAdapter) Name() string { return l.provider } -func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) { +func (l *iptablesNATAdapter) List() ([]Rule, error) { stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S") if err != nil { return nil, fmt.Errorf("failed to list NAT rules: %w", err) } - rules := parseIptablesRules(stdout, forwarding.FamilyIPv4) + rules := parseIptablesRules(stdout, FamilyIPv4) if !l.backend.IPv6Available() { return rules, nil } @@ -135,26 +134,26 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) { if err != nil { return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err) } - return append(rules, parseIptablesRules(stdout, forwarding.FamilyIPv6)...), nil + return append(rules, parseIptablesRules(stdout, FamilyIPv6)...), nil } -func (l *iptablesNATAdapter) Reconcile(rules []forwarding.Rule) error { - byFamily := map[string][]forwarding.Rule{ - forwarding.FamilyIPv4: nil, - forwarding.FamilyIPv6: nil, +func (l *iptablesNATAdapter) Reconcile(rules []Rule) error { + byFamily := map[string][]Rule{ + FamilyIPv4: nil, + FamilyIPv6: nil, } for _, rule := range rules { - normalized, err := forwarding.NormalizeRule(rule) + normalized, err := NormalizeRule(rule) if err != nil { return err } - if normalized.Family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + if normalized.Family == FamilyIPv6 && !l.backend.IPv6Available() { return fmt.Errorf("ip6tables command family is unavailable") } byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized) } - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + if family == FamilyIPv6 && !l.backend.IPv6Available() { continue } if err := l.batchEnsureChains(family); err != nil { @@ -171,13 +170,13 @@ func (l *iptablesNATAdapter) Reconcile(rules []forwarding.Rule) error { return nil } -func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) { - natRules := [][]string{{"-F", forwarding.ChainPreRouting}, {"-F", forwarding.ChainPostRouting}} - filterRules := [][]string{{"-F", forwarding.ChainForward}} +func buildIptablesForwardRestoreScript(rules []Rule) (string, error) { + natRules := [][]string{{"-F", ChainPreRouting}, {"-F", ChainPostRouting}} + filterRules := [][]string{{"-F", ChainForward}} for _, rule := range rules { sourcePort := strings.ReplaceAll(rule.Port, "-", ":") targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":") - preRouting := []string{"-A", forwarding.ChainPreRouting} + preRouting := []string{"-A", ChainPreRouting} if rule.Interface != "" { preRouting = append(preRouting, "-i", rule.Interface) } @@ -188,11 +187,11 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) } natRules = append(natRules, append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)), - []string{"-A", forwarding.ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"}, + []string{"-A", ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"}, ) filterRules = append(filterRules, - []string{"-A", forwarding.ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"}, - []string{"-A", forwarding.ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"}, + []string{"-A", ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"}, + []string{"-A", ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"}, ) } var script strings.Builder @@ -220,15 +219,15 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) return script.String(), nil } -func forwardingTarget(rule forwarding.Rule) string { - if rule.Family == forwarding.FamilyIPv6 { +func forwardingTarget(rule Rule) string { + if rule.Family == FamilyIPv6 { return "[" + rule.TargetIP + "]:" + rule.TargetPort } return rule.TargetIP + ":" + rule.TargetPort } func isRemoteTarget(family, target string) bool { - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { return target != "" && target != "::1" && target != "localhost" } return target != "" && target != "127.0.0.1" && target != "localhost" @@ -239,8 +238,8 @@ func (l *iptablesNATAdapter) Enable() error { return err } - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + if family == FamilyIPv6 && !l.backend.IPv6Available() { continue } if err := l.batchEnsureChains(family); err != nil { @@ -252,7 +251,7 @@ func (l *iptablesNATAdapter) Enable() error { func (l *iptablesNATAdapter) batchEnsureChains(family string) error { list := l.backend.RunWithStd - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { list = l.backend.RunIPv6WithStd } outputs := make(map[string]string, 2) @@ -274,12 +273,12 @@ func (l *iptablesNATAdapter) batchEnsureChains(family string) error { } func (l *iptablesNATAdapter) Cleanup() error { - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + if family == FamilyIPv6 && !l.backend.IPv6Available() { continue } list := l.backend.RunWithStd - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { list = l.backend.RunIPv6WithStd } outputs := make(map[string]string, 2) @@ -297,8 +296,8 @@ func (l *iptablesNATAdapter) Cleanup() error { } } } - for _, file := range []string{forwarding.ForwardFile, forwarding.PreRoutingFile, forwarding.PostRoutingFile, - iptables_helper.IPv6FileName(forwarding.ForwardFile), iptables_helper.IPv6FileName(forwarding.PreRoutingFile), iptables_helper.IPv6FileName(forwarding.PostRoutingFile)} { + for _, file := range []string{ForwardFile, PreRoutingFile, PostRoutingFile, + iptables_helper.IPv6FileName(ForwardFile), iptables_helper.IPv6FileName(PreRoutingFile), iptables_helper.IPv6FileName(PostRoutingFile)} { if err := os.Remove(filepath.Join(global.Dir.FirewallDir, file)); err != nil && !errors.Is(err, os.ErrNotExist) { return err } @@ -308,8 +307,8 @@ func (l *iptablesNATAdapter) Cleanup() error { func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string { items := []struct{ table, parent, chain string }{ - {iptables_helper.NatTab, "PREROUTING", forwarding.ChainPreRouting}, - {iptables_helper.NatTab, "POSTROUTING", forwarding.ChainPostRouting}, + {iptables_helper.NatTab, "PREROUTING", ChainPreRouting}, + {iptables_helper.NatTab, "POSTROUTING", ChainPostRouting}, } byTable := make(map[string][]string, 2) for _, item := range items { @@ -335,25 +334,25 @@ func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) } filterOutput := outputs[iptables_helper.FilterTab] - filterChainExists := containsExactLine(filterOutput, "-N "+forwarding.ChainForward) - filterBinding := "-A FORWARD -j " + forwarding.ChainForward + filterChainExists := containsExactLine(filterOutput, "-N "+ChainForward) + filterBinding := "-A FORWARD -j " + ChainForward filterBindingCount := countExactLines(filterOutput, filterBinding) if create { if !filterChainExists { - byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+forwarding.ChainForward) + byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+ChainForward) } if !forwardBindingEffective(filterOutput) { for range filterBindingCount { - byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward) + byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward) } byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], canonicalForwardBindingRule(filterOutput)) } } else { for range filterBindingCount { - byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward) + byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward) } if filterChainExists { - byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+forwarding.ChainForward, "-X "+forwarding.ChainForward) + byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+ChainForward, "-X "+ChainForward) } } var script strings.Builder @@ -382,7 +381,7 @@ func countExactLines(output, want string) int { } func forwardBindingEffective(output string) bool { - binding := "-A FORWARD -j " + forwarding.ChainForward + binding := "-A FORWARD -j " + ChainForward bindingPosition := 0 terminalPosition := 0 position := 0 @@ -405,7 +404,7 @@ func forwardBindingEffective(output string) bool { } func canonicalForwardBindingRule(output string) string { - binding := "-A FORWARD -j " + forwarding.ChainForward + binding := "-A FORWARD -j " + ChainForward position := 1 for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) @@ -413,11 +412,11 @@ func canonicalForwardBindingRule(output string) string { continue } if isUnconditionalForwardTerminal(line) { - return fmt.Sprintf("-I FORWARD %d -j %s", position, forwarding.ChainForward) + return fmt.Sprintf("-I FORWARD %d -j %s", position, ChainForward) } position++ } - return "-A FORWARD -j " + forwarding.ChainForward + return "-A FORWARD -j " + ChainForward } func isUnconditionalForwardTerminal(line string) bool { @@ -443,14 +442,14 @@ func containsExactLine(output, want string) bool { } func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) { - ipv4Init, ipv4Bind, err := l.familyInitStatus(forwarding.FamilyIPv4) + ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4) if err != nil { return false, false, err } if !l.backend.IPv6Available() { return ipv4Init, ipv4Bind, nil } - ipv6Init, ipv6Bind, err := l.familyInitStatus(forwarding.FamilyIPv6) + ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6) if err != nil { return false, false, err } @@ -461,7 +460,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error) sysctlPath := "/proc/sys/net/ipv4/ip_forward" label := "IPv4" list := l.backend.RunWithStd - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" label = "IPv6" list = l.backend.RunIPv6WithStd @@ -476,8 +475,8 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error) return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err) } natInit, natBind := checkInitAndBind( - []string{"-N " + forwarding.ChainPreRouting, "-N " + forwarding.ChainPostRouting}, - []string{"-A PREROUTING -j " + forwarding.ChainPreRouting, "-A POSTROUTING -j " + forwarding.ChainPostRouting}, + []string{"-N " + ChainPreRouting, "-N " + ChainPostRouting}, + []string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting}, strings.Split(natRules, "\n"), ) if !natInit { @@ -488,7 +487,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error) return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err) } filterInit, _ := checkInitAndBind( - []string{"-N " + forwarding.ChainForward}, + []string{"-N " + ChainForward}, nil, strings.Split(filterRules, "\n"), ) @@ -497,7 +496,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error) } func (l *iptablesNATAdapter) FamilyStatus(family string) (bool, bool, error) { - if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + if family == FamilyIPv6 && !l.backend.IPv6Available() { return false, false, nil } return l.familyInitStatus(family) @@ -527,8 +526,8 @@ func containsExactRule(lines []string, rule string) bool { } func (l *iptablesNATAdapter) Replay() error { - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + if family == FamilyIPv6 && !l.backend.IPv6Available() { continue } if err := l.batchEnsureChains(family); err != nil { @@ -540,9 +539,9 @@ func (l *iptablesNATAdapter) Replay() error { chain string file string }{ - {iptables_helper.FilterTab, forwarding.ChainForward, forwarding.ForwardFile}, - {iptables_helper.NatTab, forwarding.ChainPreRouting, forwarding.PreRoutingFile}, - {iptables_helper.NatTab, forwarding.ChainPostRouting, forwarding.PostRoutingFile}, + {iptables_helper.FilterTab, ChainForward, ForwardFile}, + {iptables_helper.NatTab, ChainPreRouting, PreRoutingFile}, + {iptables_helper.NatTab, ChainPostRouting, PostRoutingFile}, } { if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil { return err @@ -556,17 +555,17 @@ func (l *iptablesNATAdapter) Replay() error { return nil } -func parseIptablesRules(stdout, family string) []forwarding.Rule { - var rules []forwarding.Rule +func parseIptablesRules(stdout, family string) []Rule { + var rules []Rule num := 0 lines: for _, line := range strings.Split(stdout, "\n") { fields, err := shellwords.Parse(line) - if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != forwarding.ChainPreRouting { + if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != ChainPreRouting { continue } num++ - rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family} + rule := Rule{Num: strconv.Itoa(num), Family: family} target := "" for index := 2; index < len(fields); index++ { var value *string @@ -609,7 +608,7 @@ lines: switch target { case "REDIRECT": rule.TargetIP = "127.0.0.1" - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { rule.TargetIP = "::1" } case "DNAT": diff --git a/agent/utils/firewall/forwarding/providers/nftables.go b/agent/utils/firewall/forwarding/nftables.go similarity index 74% rename from agent/utils/firewall/forwarding/providers/nftables.go rename to agent/utils/firewall/forwarding/nftables.go index 677178569b90..c9f31d2ef136 100644 --- a/agent/utils/firewall/forwarding/providers/nftables.go +++ b/agent/utils/firewall/forwarding/nftables.go @@ -1,4 +1,4 @@ -package providers +package forwarding import ( "encoding/base64" @@ -12,7 +12,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" "github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper" ) @@ -31,10 +30,10 @@ func newNftablesAdapter() *nftablesAdapter { func (n *nftablesAdapter) Name() string { return "nftables" } -func (n *nftablesAdapter) List() ([]forwarding.Rule, error) { - rules := make([]forwarding.Rule, 0) - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)) +func (n *nftablesAdapter) List() ([]Rule, error) { + rules := make([]Rule, 0) + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(ChainPreRouting)) if errors.Is(err, nftables_helper.ErrChainNotFound) { continue } @@ -46,7 +45,7 @@ func (n *nftablesAdapter) List() ([]forwarding.Rule, error) { return rules, nil } -func (n *nftablesAdapter) Reconcile(rules []forwarding.Rule) error { +func (n *nftablesAdapter) Reconcile(rules []Rule) error { if err := ensureNftForwardTables(); err != nil { return fmt.Errorf("initialize nftables forwarding table: %w", err) } @@ -69,7 +68,7 @@ func (n *nftablesAdapter) Enable() error { func (n *nftablesAdapter) Cleanup() error { commands := make([][]string, 0, 2) - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil { continue @@ -89,7 +88,7 @@ func (n *nftablesAdapter) Cleanup() error { } func (n *nftablesAdapter) InitStatus() (bool, bool, error) { - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { initialized, bound, err := n.FamilyStatus(family) if err != nil || !initialized || !bound { return initialized, bound, err @@ -100,14 +99,14 @@ func (n *nftablesAdapter) InitStatus() (bool, bool, error) { func (n *nftablesAdapter) FamilyStatus(family string) (bool, bool, error) { sysctlPath := "/proc/sys/net/ipv4/ip_forward" - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding" } data, err := n.system.ReadFile(sysctlPath) if err != nil { return false, false, fmt.Errorf("read %s forwarding status: %w", family, err) } - for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} { + for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { if _, err := nftRun("list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)); err != nil { return false, false, nil } @@ -123,7 +122,7 @@ func (n *nftablesAdapter) Replay() error { return err } allPresent := true - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil { allPresent = false } @@ -136,7 +135,7 @@ func (n *nftablesAdapter) Replay() error { func ensureNftForwardTables() error { commands := make([][]string, 0, 8) - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { tableFamily := nftTableFamily(family) tableExists := true if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil { @@ -146,9 +145,9 @@ func ensureNftForwardTables() error { chains := []struct { name, chainType, hook, priority string }{ - {nftForwardChain(forwarding.ChainPreRouting), "nat", "prerouting", "-100"}, - {nftForwardChain(forwarding.ChainPostRouting), "nat", "postrouting", "100"}, - {nftForwardChain(forwarding.ChainForward), "filter", "forward", "0"}, + {nftForwardChain(ChainPreRouting), "nat", "prerouting", "-100"}, + {nftForwardChain(ChainPostRouting), "nat", "postrouting", "100"}, + {nftForwardChain(ChainForward), "filter", "forward", "0"}, } for _, chain := range chains { if tableExists { @@ -168,15 +167,15 @@ func ensureNftForwardTables() error { return nftRunCommands(commands) } -func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) { +func rebuildNftForwardCommands(rules []Rule) ([][]string, error) { commands := make([][]string, 0, 6+len(rules)*4) - for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} { - for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} { + for _, family := range []string{FamilyIPv4, FamilyIPv6} { + for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} { commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)}) } } for _, rule := range rules { - normalized, err := forwarding.NormalizeRule(rule) + normalized, err := NormalizeRule(rule) if err != nil { return nil, err } @@ -189,18 +188,18 @@ func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) { interfaceMatch = append(interfaceMatch, "iifname", strconv.Quote(rule.Interface)) } if isRemoteTarget(rule.Family, rule.TargetIP) { - preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)} + preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPreRouting)} preRouting = append(preRouting, interfaceMatch...) preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "dnat", "to", forwardingTarget(rule), "comment", comment) commands = append(commands, preRouting, - []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment}, - []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment}, - []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment}, + []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment}, + []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment}, + []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment}, ) continue } - preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)} + preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPreRouting)} preRouting = append(preRouting, interfaceMatch...) preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "redirect", "to", ":"+rule.TargetPort, "comment", comment) commands = append(commands, preRouting) @@ -209,22 +208,22 @@ func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) { } func nftTableFamily(family string) string { - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { return "ip6" } return nftForwardFamily } func nftAddressKeyword(family string) string { - if family == forwarding.FamilyIPv6 { + if family == FamilyIPv6 { return "ip6" } return "ip" } -func encodeNftForwardRule(rule forwarding.Rule) string { +func encodeNftForwardRule(rule Rule) string { family, protocol := "4", "t" - if rule.Family == forwarding.FamilyIPv6 { + if rule.Family == FamilyIPv6 { family = "6" } if rule.Protocol == "udp" { @@ -236,9 +235,9 @@ func encodeNftForwardRule(rule forwarding.Rule) string { ) } -func decodeNftForwardRule(value string) (forwarding.Rule, bool) { +func decodeNftForwardRule(value string) (Rule, bool) { if !strings.HasPrefix(value, nftForwardMarker) { - return forwarding.Rule{}, false + return Rule{}, false } value = strings.TrimPrefix(value, nftForwardMarker) if strings.HasPrefix(value, "v2|") { @@ -247,19 +246,19 @@ func decodeNftForwardRule(value string) (forwarding.Rule, bool) { return decodeLegacyNftForwardRule(value) } -func decodeCompactNftForwardRule(value string) (forwarding.Rule, bool) { +func decodeCompactNftForwardRule(value string) (Rule, bool) { parts := strings.Split(value, "|") if len(parts) != 7 || parts[0] != "v2" { - return forwarding.Rule{}, false + return Rule{}, false } family, protocol := "", "" switch parts[1] { case "4": - family = forwarding.FamilyIPv4 + family = FamilyIPv4 case "6": - family = forwarding.FamilyIPv6 + family = FamilyIPv6 default: - return forwarding.Rule{}, false + return Rule{}, false } switch parts[2] { case "t": @@ -267,31 +266,31 @@ func decodeCompactNftForwardRule(value string) (forwarding.Rule, bool) { case "u": protocol = "udp" default: - return forwarding.Rule{}, false + return Rule{}, false } - return forwarding.Rule{ + return Rule{ Family: family, Protocol: protocol, Port: parts[3], TargetIP: parts[4], TargetPort: parts[5], Interface: parts[6], }, true } -func decodeLegacyNftForwardRule(value string) (forwarding.Rule, bool) { +func decodeLegacyNftForwardRule(value string) (Rule, bool) { parts := strings.Split(value, ".") if len(parts) != 6 { - return forwarding.Rule{}, false + return Rule{}, false } decoded := make([]string, len(parts)) for index, part := range parts { data, err := base64.RawURLEncoding.DecodeString(part) if err != nil { - return forwarding.Rule{}, false + return Rule{}, false } decoded[index] = string(data) } - return forwarding.Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true + return Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true } -func parseNftForwardRules(stdout string) []forwarding.Rule { - result := make([]forwarding.Rule, 0) +func parseNftForwardRules(stdout string) []Rule { + result := make([]Rule, 0) for _, line := range strings.Split(stdout, "\n") { commentStart := strings.Index(line, `comment "`+nftForwardMarker) handleStart := strings.LastIndex(line, "# handle ") diff --git a/agent/utils/firewall/forwarding/providers/factory.go b/agent/utils/firewall/forwarding/providers/factory.go deleted file mode 100644 index 0aba65939f73..000000000000 --- a/agent/utils/firewall/forwarding/providers/factory.go +++ /dev/null @@ -1,18 +0,0 @@ -package providers - -import ( - "errors" - - "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" -) - -func New(provider string) (forwarding.Adapter, error) { - switch provider { - case "iptables": - return newIptablesNATAdapter(provider), nil - case "nftables": - return newNftablesAdapter(), nil - default: - return nil, errors.New("unsupported forwarding provider: " + provider) - } -} diff --git a/agent/utils/firewall/forwarding/providers/sysctl.go b/agent/utils/firewall/forwarding/sysctl.go similarity index 99% rename from agent/utils/firewall/forwarding/providers/sysctl.go rename to agent/utils/firewall/forwarding/sysctl.go index adef90e6155d..64f22600efed 100644 --- a/agent/utils/firewall/forwarding/providers/sysctl.go +++ b/agent/utils/firewall/forwarding/sysctl.go @@ -1,4 +1,4 @@ -package providers +package forwarding import ( "errors" diff --git a/agent/utils/firewall/iptables_helper/command.go b/agent/utils/firewall/iptables_helper/command.go index 9493dc010c62..818f2568916f 100644 --- a/agent/utils/firewall/iptables_helper/command.go +++ b/agent/utils/firewall/iptables_helper/command.go @@ -145,3 +145,22 @@ func CheckChainExist(tab, chain string) (bool, error) { } return false, nil } + +func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) { + commands, err := lifecycle.ResolveIptablesCommands() + if err != nil { + return "", err + } + executable := commands.IPv4 + if ipv6 { + if !commands.IPv6Available() { + return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable) + } + executable = commands.IPv6 + } + output, err := runTables(ctx, executable, table, false, true, "-S") + if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) { + return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err) + } + return output, err +} diff --git a/agent/utils/firewall/iptables_helper/read.go b/agent/utils/firewall/iptables_helper/read.go deleted file mode 100644 index 2614fbdda9e7..000000000000 --- a/agent/utils/firewall/iptables_helper/read.go +++ /dev/null @@ -1,29 +0,0 @@ -package iptables_helper - -import ( - "context" - "fmt" - "strings" - - "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" - "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle" -) - -func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) { - commands, err := lifecycle.ResolveIptablesCommands() - if err != nil { - return "", err - } - executable := commands.IPv4 - if ipv6 { - if !commands.IPv6Available() { - return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable) - } - executable = commands.IPv6 - } - output, err := runTables(ctx, executable, table, false, true, "-S") - if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) { - return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err) - } - return output, err -} diff --git a/agent/utils/firewall/nftables_helper/read.go b/agent/utils/firewall/nftables_helper/read.go deleted file mode 100644 index 7dc64142b3aa..000000000000 --- a/agent/utils/firewall/nftables_helper/read.go +++ /dev/null @@ -1,16 +0,0 @@ -package nftables_helper - -import "errors" - -var ErrChainNotFound = errors.New("nftables chain is not initialized") - -func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) { - output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain) - if err != nil { - return "", err - } - if !exists { - return "", ErrChainNotFound - } - return output, nil -} diff --git a/agent/utils/firewall/nftables_helper/runtime.go b/agent/utils/firewall/nftables_helper/runtime.go index 1cbb6b84c024..b2362f94dd35 100644 --- a/agent/utils/firewall/nftables_helper/runtime.go +++ b/agent/utils/firewall/nftables_helper/runtime.go @@ -2,6 +2,7 @@ package nftables_helper import ( "context" + "errors" "fmt" "os" "slices" @@ -184,3 +185,16 @@ func loadFamilyInitStatus(family filter.Family) (bool, bool, error) { } return true, true, nil } + +var ErrChainNotFound = errors.New("nftables chain is not initialized") + +func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) { + output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain) + if err != nil { + return "", err + } + if !exists { + return "", ErrChainNotFound + } + return output, nil +} diff --git a/agent/utils/firewall/ping/ping.go b/agent/utils/firewall/ping.go similarity index 97% rename from agent/utils/firewall/ping/ping.go rename to agent/utils/firewall/ping.go index 3995574e70d3..39c16c23c67a 100644 --- a/agent/utils/firewall/ping/ping.go +++ b/agent/utils/firewall/ping.go @@ -1,4 +1,4 @@ -package ping +package firewall import ( "fmt" @@ -10,7 +10,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/cmd" ) -func LoadStatus() string { +func LoadPingStatus() string { data, err := os.ReadFile("/proc/sys/net/ipv4/icmp_echo_ignore_all") if err != nil { return constant.StatusNone @@ -28,7 +28,7 @@ func LoadStatus() string { return constant.StatusDisable } -func UpdateStatus(enable string) error { +func UpdatePingStatus(enable string) error { const confPath = "/etc/sysctl.conf" const panelSysctlPath = "/etc/sysctl.d/98-onepanel.conf"