mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 16:00:52 +00:00
397 lines
13 KiB
Go
397 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"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/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
|
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
|
)
|
|
|
|
var (
|
|
firewallRuleSyncTaskMu sync.Mutex
|
|
firewallRuleSyncTaskID string
|
|
)
|
|
|
|
type firewallDatabaseSyncAdapter interface {
|
|
previewRuleSync(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error)
|
|
syncRules(context.Context, dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error)
|
|
}
|
|
|
|
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
|
|
firewallWhitelistMu.Lock()
|
|
defer firewallWhitelistMu.Unlock()
|
|
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
provider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.syncPortWhitelist(ctx, provider, ports); err != nil {
|
|
return err
|
|
}
|
|
return s.removeTransferredSystemPortRules(ctx, provider, ports)
|
|
}
|
|
|
|
func (s *FirewallService) PreviewRuleSync(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
switch strings.TrimSpace(request.Subsystem) {
|
|
case "forwarding":
|
|
service := s.forwardingSync
|
|
if service == nil {
|
|
service = newForwardingService()
|
|
}
|
|
return service.previewRuleSync(ctx, request)
|
|
case "docker":
|
|
service := s.dockerSync
|
|
if service == nil {
|
|
service = newDockerPortGuardService()
|
|
}
|
|
return service.previewRuleSync(ctx, request)
|
|
}
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
_, rules, _, err := s.loadFirewallSyncRules(ctx, request)
|
|
preview := dto.FirewallRuleSyncPreview{Subsystem: "system", TargetProvider: request.TargetProvider, Items: make([]dto.FirewallRuleSyncItem, 0, len(rules))}
|
|
for _, rule := range rules {
|
|
preview.Add(rule.FirewallRuleSyncItem)
|
|
}
|
|
return preview, err
|
|
}
|
|
|
|
func (s *FirewallService) SyncRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
switch strings.TrimSpace(request.Subsystem) {
|
|
case "forwarding":
|
|
service := s.forwardingSync
|
|
if service == nil {
|
|
service = newForwardingService()
|
|
}
|
|
return service.syncRules(ctx, request)
|
|
case "docker":
|
|
service := s.dockerSync
|
|
if service == nil {
|
|
service = newDockerPortGuardService()
|
|
}
|
|
return service.syncRules(ctx, request)
|
|
default:
|
|
return s.syncSystemRules(ctx, clientIP, request)
|
|
}
|
|
}
|
|
|
|
func (s *FirewallService) CurrentRuleSyncTask() (dto.FirewallRuleSyncTask, error) {
|
|
firewallRuleSyncTaskMu.Lock()
|
|
defer firewallRuleSyncTaskMu.Unlock()
|
|
return currentFirewallRuleSyncTaskLocked()
|
|
}
|
|
|
|
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
return s.reconcileLocked(ctx)
|
|
}
|
|
|
|
func (s *ForwardingService) Restore(ctx context.Context) error {
|
|
forwardingMutationMu.Lock()
|
|
defer forwardingMutationMu.Unlock()
|
|
enabled, err := s.forwardingEnabled()
|
|
if err != nil || !enabled {
|
|
if err != nil {
|
|
recordForwardingSyncError(err)
|
|
}
|
|
return err
|
|
}
|
|
manager, err := s.clientFactory()
|
|
if err != nil {
|
|
recordForwardingSyncError(err)
|
|
return err
|
|
}
|
|
stored, err := s.rules.List(ctx)
|
|
if err != nil {
|
|
recordForwardingSyncError(err)
|
|
return err
|
|
}
|
|
if err := s.initializeForwarding(manager); err != nil {
|
|
recordForwardingSyncError(err)
|
|
return err
|
|
}
|
|
err = manager.ReplaceRules(forwardingRulesFromModels(stored))
|
|
recordForwardingSyncError(err)
|
|
return err
|
|
}
|
|
|
|
func (s *ForwardingService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncPreview{}, err
|
|
}
|
|
target, candidates, targetRules, _, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncPreview{}, err
|
|
}
|
|
return forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules), nil
|
|
}
|
|
|
|
func (s *ForwardingService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
forwardingMutationMu.Lock()
|
|
defer forwardingMutationMu.Unlock()
|
|
|
|
targetProvider, err := databaseRuleSyncTarget(request, "forwarding")
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
target, candidates, targetRules, targetInitialized, err := s.loadRuleSyncCandidates(ctx, targetProvider)
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
preview := forwardingSyncPreview(filter.Provider(target.Name()), candidates, targetRules)
|
|
desired := make([]forwarding.Rule, 0, len(candidates))
|
|
for _, candidate := range candidates {
|
|
if candidate.err == nil {
|
|
desired = append(desired, candidate.rule)
|
|
}
|
|
}
|
|
if preview.Blocked > 0 {
|
|
return firewallSyncResult(preview, nil, false), nil
|
|
}
|
|
if len(desired) == 0 && !targetInitialized {
|
|
return firewallSyncResult(preview, nil, true), nil
|
|
}
|
|
reconcileErr := func() error {
|
|
if len(desired) > 0 {
|
|
if err := s.persistForwardingEnabled(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.initializeForwarding(target); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := target.ReplaceRules(desired); err != nil {
|
|
return err
|
|
}
|
|
return verifyForwardingRuleSync(target, desired)
|
|
}()
|
|
result := firewallSyncResult(preview, reconcileErr, true)
|
|
recordForwardingSyncError(reconcileErr)
|
|
if reconcileErr != nil {
|
|
if preview.Ready == 0 {
|
|
return result, reconcileErr
|
|
}
|
|
return result, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) previewRuleSync(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncPreview, error) {
|
|
target, policies, runtime, err := s.loadRuleSyncCandidates(ctx, request)
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncPreview{}, err
|
|
}
|
|
targetInventory, err := runtime.ListPolicies()
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncPreview{}, err
|
|
}
|
|
return dockerSyncPreview(filter.Provider(target), policies, targetInventory), nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) syncRules(ctx context.Context, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
|
|
target, policies, targetRuntime, err := s.loadRuleSyncCandidates(ctx, request)
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
runtimePolicies := make([]dockerfirewall.Policy, 0, len(policies))
|
|
for _, policy := range policies {
|
|
sources := []string{}
|
|
_ = json.Unmarshal([]byte(policy.Sources), &sources)
|
|
runtimePolicies = append(runtimePolicies, dockerfirewall.Policy{
|
|
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
|
Protocol: policy.Protocol, Mode: policy.Mode, Sources: sources,
|
|
})
|
|
}
|
|
targetInventory, err := targetRuntime.ListPolicies()
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
preview := dockerSyncPreview(filter.Provider(target), policies, targetInventory)
|
|
for _, item := range preview.Items {
|
|
if item.Status == firewallsync.StatusBlocked && item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
|
return firewallSyncResult(preview, nil, false), nil
|
|
}
|
|
}
|
|
if preview.Ready == 0 && preview.Removed == 0 {
|
|
return firewallSyncResult(preview, nil, false), nil
|
|
}
|
|
reconcileErr := func() error {
|
|
if err := reconcileDockerFirewall(target, runtimePolicies, targetRuntime, targetInventory); err != nil {
|
|
return err
|
|
}
|
|
if err := verifyDockerFirewall(targetRuntime, runtimePolicies, targetInventory.ReadOnly); err != nil {
|
|
return err
|
|
}
|
|
if len(policies) == 0 {
|
|
return nil
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, target); err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
|
}()
|
|
result := firewallSyncResult(preview, reconcileErr, true)
|
|
if reconcileErr != nil {
|
|
return result, reconcileErr
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *FirewallService) syncSystemRules(ctx context.Context, clientIP string, request dto.FirewallRuleSyncRequest) (dto.FirewallRuleSyncResult, error) {
|
|
subsystem := strings.TrimSpace(request.Subsystem)
|
|
if subsystem == "" {
|
|
subsystem = "system"
|
|
}
|
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
defer firewallLifecycleTaskMu.Unlock()
|
|
firewallRuleSyncTaskMu.Lock()
|
|
defer firewallRuleSyncTaskMu.Unlock()
|
|
|
|
running, err := currentFirewallRuleSyncTaskLocked()
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, err
|
|
}
|
|
if running.Executing {
|
|
return dto.FirewallRuleSyncResult{
|
|
Subsystem: subsystem,
|
|
TargetProvider: request.TargetProvider,
|
|
TaskID: running.TaskID,
|
|
Queued: true,
|
|
}, nil
|
|
}
|
|
if subsystem != "system" {
|
|
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
|
|
}
|
|
taskItem, err := task.NewTask(firewallTaskName(task.TaskSync, firewallTaskHost, string(request.TargetProvider)), task.TaskSync, task.TaskScopeFirewall, "", 0)
|
|
if err != nil {
|
|
return dto.FirewallRuleSyncResult{}, fmt.Errorf("create firewall sync task: %w", err)
|
|
}
|
|
taskItem.AddSubTaskWithOps(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), func(t *task.Task) error {
|
|
result, err := s.syncRules(t.TaskCtx, clientIP, request, t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.Failed > 0 {
|
|
return errors.New(i18n.GetMsgWithMap("FirewallSyncFailed", map[string]interface{}{"failed": result.Failed}))
|
|
}
|
|
return nil
|
|
}, nil, 0, 0)
|
|
|
|
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
|
taskItem.LogFailedWithErr(taskItem.Name, err)
|
|
closeUnstartedFirewallTask(taskItem)
|
|
return dto.FirewallRuleSyncResult{}, fmt.Errorf("save firewall sync task: %w", err)
|
|
}
|
|
firewallRuleSyncTaskID = taskItem.TaskID
|
|
go func() {
|
|
defer func() {
|
|
firewallRuleSyncTaskMu.Lock()
|
|
if firewallRuleSyncTaskID == taskItem.TaskID {
|
|
firewallRuleSyncTaskID = ""
|
|
}
|
|
firewallRuleSyncTaskMu.Unlock()
|
|
}()
|
|
if err := taskItem.Execute(); err != nil && global.LOG != nil {
|
|
global.LOG.Errorf("firewall sync task %s failed: %v", taskItem.TaskID, err)
|
|
}
|
|
}()
|
|
return dto.FirewallRuleSyncResult{
|
|
Subsystem: "system",
|
|
TargetProvider: request.TargetProvider,
|
|
TaskID: taskItem.TaskID,
|
|
Queued: true,
|
|
}, nil
|
|
}
|
|
|
|
func verifyForwardingRuleSync(target forwarding.Adapter, desired []forwarding.Rule) error {
|
|
actual, err := target.List()
|
|
if err != nil {
|
|
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
|
}
|
|
actual, err = normalizeForwardingRuntimeRules(actual)
|
|
if err != nil {
|
|
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
|
}
|
|
if !firewallRuleStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
|
|
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reconcileDockerFirewall(backend string, policies []dockerfirewall.Policy, runtime dockerfirewall.Runtime, inventory dockerfirewall.PolicyInventory) error {
|
|
families := make(map[string]struct{}, len(policies))
|
|
needsInitialize, needsBind := false, false
|
|
for _, policy := range policies {
|
|
families[policy.Family] = struct{}{}
|
|
}
|
|
if len(families) == 0 {
|
|
initialized := false
|
|
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
|
status := runtime.Status(family)
|
|
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
|
return fmt.Errorf("inspect Docker firewall target %s for %s failed", backend, family)
|
|
}
|
|
initialized = initialized || status.Initialized
|
|
}
|
|
if initialized {
|
|
return runtime.ReplacePolicies(nil, inventory)
|
|
}
|
|
return nil
|
|
}
|
|
for family := range families {
|
|
status := runtime.Status(family)
|
|
needsInitialize = needsInitialize || !status.Initialized
|
|
needsBind = needsBind || !status.Bound || !status.Effective
|
|
}
|
|
var err error
|
|
if needsInitialize {
|
|
err = runtime.Initialize(policies, inventory)
|
|
} else {
|
|
if needsBind {
|
|
err = runtime.Bind()
|
|
}
|
|
if err == nil {
|
|
err = runtime.ReplacePolicies(policies, inventory)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for family := range families {
|
|
if !runtime.Status(family).Effective {
|
|
return fmt.Errorf("Docker firewall target %s is not effective for %s", backend, family)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
|
|
if err := ReconcileDockerPortGuard(ctx); err != nil {
|
|
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
|
|
}
|
|
}
|