Files
1Panel/agent/app/service/firewall_docker.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

491 lines
18 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"os"
"slices"
"sort"
"strconv"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/docker/docker/client"
"github.com/google/uuid"
)
const (
dockerGuardComposeProjectLabel = "com.docker.compose.project"
dockerGuardComposeCreatedBy = "createdBy"
dockerTrafficPathForward = "forward"
dockerTrafficPathInput = "input"
dockerTrafficPathUnknown = "unknown"
dockerManagementContainerGuard = "container_guard"
dockerManagementHostFirewall = "host_firewall"
dockerManagementNeedsDiagnosis = "needs_diagnosis"
dockerReasonNATInspectFailed = "nat_inspect_failed"
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
dockerReasonNoMatchingPath = "no_matching_path"
)
type DockerPortGuardService struct {
runtime dockerfirewall.Runtime
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
client func() (*client.Client, error)
version func(string) string
}
var dockerPortGuardServiceMu sync.Mutex
type IDockerPortGuardService interface {
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
Operate(context.Context, dto.DockerPortGuardOperation) error
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
Restore(context.Context) error
}
func NewIDockerPortGuardService() IDockerPortGuardService {
return newDockerPortGuardService()
}
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
families, err := loadFirewallFamilies()
if err != nil {
return dto.DockerPortGuardList{}, err
}
backend := selectedDockerFirewallBackend("")
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
if err != nil {
return dto.DockerPortGuardList{}, err
}
policies := dockerGuardInventoryEndpoints(inventory)
unavailable := func() dto.DockerPortGuardList {
backend := selectedDockerFirewallBackend("")
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
base.Message = i18n.Get("ErrDockerFailed")
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
}
cli, err := s.client()
if err != nil {
return unavailable(), nil
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return unavailable(), nil
}
detectedBackend := dockerFirewallBackend(info)
backend = selectedDockerFirewallBackend(detectedBackend)
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil {
return dto.DockerPortGuardList{}, err
}
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
sort.Slice(endpoints, func(i, j int) bool {
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
})
sort.Slice(orphanPolicies, func(i, j int) bool {
return fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[i].Family, orphanPolicies[i].HostIP, orphanPolicies[i].HostPort, orphanPolicies[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[j].Family, orphanPolicies[j].HostIP, orphanPolicies[j].HostPort, orphanPolicies[j].Protocol)
})
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
}
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
backend := string(provider)
if backend == "" {
backend = selectedDockerFirewallBackend("")
}
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
}
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
if err != nil {
return dto.FirewallSubsystemBackup{}, err
}
if inventory.Policies == nil {
inventory.Policies = []dockerfirewall.Policy{}
}
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
}
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
cli, err := s.client()
if err != nil {
return nil, buserr.WithDetail("ErrDockerFailed", err.Error(), err)
}
defer cli.Close()
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
return []dto.DockerPortGuardContainer{}, nil
}
}
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
if err != nil {
return nil, err
}
backend := selectedDockerFirewallBackend("")
if info, infoErr := cli.Info(ctx); infoErr == nil {
backend = dockerFirewallBackend(info)
}
annotateDockerEndpointManagement(ctx, endpoints, backend)
return groupDockerGuardContainers(endpoints), nil
}
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
switch request.Operation {
case "initialize":
return s.initialize(ctx, request, nil)
case "bind":
runtime, _, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
case "unbind":
var err error
if s.runtime != nil {
err = s.runtime.Unbind()
} else {
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
}
if err = errors.Join(err, ctx.Err()); err != nil {
return err
}
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
default:
return fmt.Errorf("unsupported Docker port guard operation: %s", request.Operation)
}
}
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
}
if request.Operation != "initialize" {
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
}
if request.BackupFile != "" {
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
return dto.FilterChainOperationResponse{}, err
}
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
return s.initialize(t.TaskCtx, request, t)
})
}
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
uuids, err := normalizeDockerFirewallUUIDs(request.UUIDs)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
ctx := t.TaskCtx
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
wanted := make(map[string]bool, len(uuids))
for _, id := range uuids {
wanted[id] = true
}
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
for _, policy := range inventory.Policies {
if wanted[policy.UUID] {
delete(wanted, policy.UUID)
} else {
remaining = append(remaining, policy)
}
}
if len(wanted) > 0 {
return filter.ErrRuleStale
}
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
})
}
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
if len(request.Policies) > filter.MaxAtomicExpansion {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
labels := make([]string, len(request.Policies))
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
count := 0
for i, policy := range request.Policies {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
normalized, err := normalizeDockerFirewallPolicy(dockerfirewall.Policy{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
})
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
}
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
count++
} else {
count += len(normalized.Sources)
if normalized.Mode == dockerfirewall.ModeAllow {
count++
}
}
if count > filter.MaxAtomicExpansion {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
}
normalized.UUID = uuid.NewString()
policies = append(policies, normalized)
}
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
ctx := t.TaskCtx
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
if request.Import {
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
merged, err := mergeDockerBackup(inventory, backup, backend, t)
if err != nil {
return err
}
current = merged.Policies
}
byEndpoint := make(map[string]int, len(current))
for i, policy := range current {
byEndpoint[dockerPolicyEndpointKey(policy)] = i
}
for i := range policies {
key := dockerPolicyEndpointKey(policies[i])
index, exists := byEndpoint[key]
if request.Import {
if !exists || current[index].UUID != policies[i].UUID {
labels[i] = ""
continue
}
} else if exists {
policies[i].UUID = current[index].UUID
current[index] = policies[i]
} else {
byEndpoint[key] = len(current)
current = append(current, policies[i])
}
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
})
}
if len(endpoints) == 0 {
return nil
}
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
return err
}
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
return err
}
return nil
})
}
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
dockerPortGuardServiceMu.Lock()
defer dockerPortGuardServiceMu.Unlock()
enabled, err := dockerPortGuardPersistedEnabled()
if err != nil || !enabled {
return err
}
runtime, backend, err := s.runtimeForDocker(ctx)
if err != nil {
return err
}
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
families, err := loadFirewallFamilies()
if err != nil {
return err
}
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
before := len(backup.Docker.Policies)
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
}
missing := make(map[string]bool)
needsBind := false
for _, family := range families {
status := runtime.Status(family)
if status.Reason == dockerfirewall.ReasonInspectFailed {
return fmt.Errorf("inspect Docker guard %s failed", family)
}
if status.Reason == dockerfirewall.ReasonCommandMissing {
continue
}
missing[family] = !status.Initialized
needsBind = needsBind || (status.Initialized && !status.Effective)
}
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
if needsBind {
return runtime.Bind(families...)
}
return nil
}
inventory, err := runtime.ListPolicies()
if err != nil {
return err
}
for _, policy := range inventory.Policies {
missing[policy.Family] = false
}
if inventory.RuleOrders == nil {
inventory.RuleOrders = make(map[string][]int64)
}
for _, policy := range backup.Docker.Policies {
if !missing[policy.Family] {
continue
}
inventory.Policies = append(inventory.Policies, policy)
key := policy.Family + "\x00" + policy.UUID
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
}
return runtime.Initialize(inventory.Policies, inventory, families...)
}
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
var ipv6 dockerfirewall.FamilyStatus
if ipv6Enabled {
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
}
version := "-"
if s.version != nil {
version = s.version(backend)
}
name := "iptables-docker"
if strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
name = "nftables-docker"
}
return dto.DockerPortGuardBase{
IPv6Enabled: ipv6Enabled,
Name: name,
Version: version,
Backend: backend,
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
Initialized: ipv4.Initialized || ipv6.Initialized,
Bound: ipv4.Bound || ipv6.Bound,
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
}
}
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
matched := make(map[int]bool, len(policies))
byEndpoint := make(map[string]int, len(policies))
for index, policy := range policies {
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
if _, exists := byEndpoint[key]; !exists {
byEndpoint[key] = index
}
}
for i := range endpoints {
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
index, exists := byEndpoint[key]
if !exists {
continue
}
policy := policies[index]
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
matched[index] = true
}
orphans := make([]dto.DockerPortGuardEndpoint, 0)
for i, policy := range policies {
if !matched[i] {
orphans = append(orphans, policy)
}
}
return endpoints, orphans
}
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
if s.client == nil || len(requested) == 0 {
return ctx.Err()
}
cli, err := s.client()
if err != nil {
return ctx.Err()
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return ctx.Err()
}
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
if err != nil {
return ctx.Err()
}
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
if err := ctx.Err(); err != nil {
return err
}
targets := make(map[string]string, len(endpoints))
for _, endpoint := range endpoints {
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
}
for _, endpoint := range requested {
target := targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
if target == dockerManagementHostFirewall {
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic is handled by the host input firewall", nil)
}
if target == dockerManagementNeedsDiagnosis {
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic management target requires diagnosis", nil)
}
}
return nil
}