mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +00:00
3001 lines
97 KiB
Go
3001 lines
97 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"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/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
filterfirewalld "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/firewalld"
|
|
filteriptables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/iptables"
|
|
filternftables "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/nftables"
|
|
filterufw "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/providers/ufw"
|
|
"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"
|
|
lifecycleproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
|
containertypes "github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/system"
|
|
"github.com/docker/docker/client"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
firewallTaskHost = "FirewallTaskHost"
|
|
firewallTaskForwarding = "FirewallTaskForwarding"
|
|
firewallTaskDocker = "FirewallTaskDocker"
|
|
)
|
|
|
|
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
|
|
|
|
type firewallLifecycleClient struct{ lifecycle.Client }
|
|
|
|
type firewallDockerRestartError struct {
|
|
Err error
|
|
}
|
|
|
|
type firewallCompletedOperationError struct {
|
|
Operation string
|
|
Err error
|
|
}
|
|
|
|
type firewallRuleBatchItem struct {
|
|
snapshot filter.RuleSet
|
|
change filter.RuleChange
|
|
}
|
|
|
|
func LoadPanelPort() string {
|
|
if !global.IsMaster {
|
|
return global.CONF.Base.Port
|
|
}
|
|
var portSetting model.Setting
|
|
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
|
|
return portSetting.Value
|
|
}
|
|
|
|
func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
|
|
if len(ports) == 0 {
|
|
return fmt.Errorf("firewall whitelist %s requires a port", serviceType)
|
|
}
|
|
firewallWhitelistMu.Lock()
|
|
defer firewallWhitelistMu.Unlock()
|
|
entries, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
servicePorts := make([]firewall.PortWhitelist, 0)
|
|
for index := range entries {
|
|
if entries[index].Type != serviceType {
|
|
continue
|
|
}
|
|
entries[index].Port = ports[0]
|
|
servicePorts = append(servicePorts, entries[index])
|
|
}
|
|
if len(servicePorts) == 0 {
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sources := []string{"0.0.0.0/0"}
|
|
if slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
sources = append(sources, "::/0")
|
|
}
|
|
servicePorts = append(servicePorts, firewall.PortWhitelist{
|
|
Type: serviceType, Port: ports[0], Protocol: "tcp",
|
|
Sources: sources,
|
|
})
|
|
entries = append(entries, servicePorts...)
|
|
}
|
|
entries, err = firewall.ValidatePortWhitelist(entries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value, err := json.Marshal(entries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.baseClient()
|
|
if err != nil && !errors.Is(err, lifecycle.ErrNotInstalled) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
allowances := make([]firewall.PortWhitelist, 0, len(servicePorts)*len(ports))
|
|
for _, rule := range servicePorts {
|
|
for _, port := range ports {
|
|
rule.Port = port
|
|
allowances = append(allowances, rule)
|
|
}
|
|
}
|
|
if err := s.applyPortWhitelist(ctx, allowances, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
|
}
|
|
|
|
func loadPortWhitelistSetting() ([]firewall.PortWhitelist, error) {
|
|
setting, err := settingRepo.Get(settingRepo.WithByKey(constant.FirewallPortWhiteList))
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
setting.Value = constant.FirewallPortWhiteListValue
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
var rules []firewall.PortWhitelist
|
|
if err = json.Unmarshal([]byte(setting.Value), &rules); err != nil {
|
|
return nil, err
|
|
}
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
rules = ipv4PortWhitelist(rules)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
|
|
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
|
provider = strings.TrimSpace(provider)
|
|
client, err := lifecycle.NewClient(provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if provider == "" {
|
|
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func loadFirewallInitStatus(provider, tab string) (bool, bool, error) {
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
if tab == "base" && len(families) == 1 {
|
|
return loadSystemFirewallFamilyStatus(provider, constant.FirewallFamilyIPv4)
|
|
}
|
|
switch provider {
|
|
case constant.FirewallProviderNftables:
|
|
return nftables_helper.LoadInitStatus(tab)
|
|
case constant.FirewallProviderIptables:
|
|
return iptables_helper.LoadInitStatus(tab)
|
|
default:
|
|
return false, false, fmt.Errorf("unsupported firewall provider: %s", provider)
|
|
}
|
|
}
|
|
|
|
func loadSystemFirewallOverview(provider, chainGroup string, families []string) (dto.FirewallSubsystemStatus, error) {
|
|
status := dto.FirewallSubsystemStatus{IPv6Enabled: len(families) > 1}
|
|
var ipv4Err, ipv6Err error
|
|
status.IPv4, ipv4Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv4)
|
|
if status.IPv6Enabled {
|
|
status.IPv6, ipv6Err = loadSystemFirewallFamilyInfo(provider, constant.FirewallFamilyIPv6)
|
|
}
|
|
if chainGroup != "base" {
|
|
return status, nil
|
|
}
|
|
if ipv4Err != nil {
|
|
return status, ipv4Err
|
|
}
|
|
if provider == constant.FirewallProviderIptables || !status.IPv6Enabled {
|
|
status.IsInit, status.IsBind = status.IPv4.Initialized, status.IPv4.Bound
|
|
return status, nil
|
|
}
|
|
if !status.IPv4.Initialized {
|
|
return status, nil
|
|
}
|
|
if !status.IPv4.Bound {
|
|
status.IsInit = true
|
|
return status, nil
|
|
}
|
|
if ipv6Err != nil {
|
|
return status, ipv6Err
|
|
}
|
|
if status.IPv6.Initialized {
|
|
status.IsInit, status.IsBind = true, status.IPv6.Bound
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func loadSystemFirewallFamilyInfo(provider, family string) (dto.FirewallBackendFamilyStatus, error) {
|
|
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
|
commands, err := lifecycle.ResolveIptablesCommands()
|
|
if err != nil || !commands.IPv6Available() {
|
|
return dto.FirewallBackendFamilyStatus{Reason: dockerfirewall.ReasonCommandMissing}, nil
|
|
}
|
|
}
|
|
var initialized, bound, partial bool
|
|
var err error
|
|
if provider == constant.FirewallProviderIptables {
|
|
initialized, bound, partial, err = iptables_helper.LoadFamilyState(family, "base")
|
|
} else {
|
|
initialized, bound, partial, err = nftables_helper.LoadFamilyState(filter.Family(family))
|
|
}
|
|
return dto.FirewallBackendFamilyStatus{
|
|
Available: err == nil,
|
|
Partial: partial,
|
|
Initialized: initialized,
|
|
Bound: bound,
|
|
}, err
|
|
}
|
|
|
|
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
|
switch provider {
|
|
case constant.FirewallProviderIptables:
|
|
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
|
case constant.FirewallProviderNftables:
|
|
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
|
|
default:
|
|
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
|
|
}
|
|
}
|
|
|
|
func (s *FirewallService) resolveRuntime(ctx context.Context, provider filter.Provider) (filter.Adapter, error) {
|
|
if s.selectedProvider != nil {
|
|
selected, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if selected != provider {
|
|
return nil, fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, provider)
|
|
}
|
|
}
|
|
return s.firewallAdapter(provider)
|
|
}
|
|
|
|
func (s *FirewallService) firewallAdapter(provider filter.Provider) (filter.Adapter, error) {
|
|
if s.adapters != nil {
|
|
if client := s.adapters[provider]; client != nil {
|
|
return client, nil
|
|
}
|
|
return nil, fmt.Errorf("%w: %s", filter.ErrAdapterUnavailable, provider)
|
|
}
|
|
switch provider {
|
|
case filter.ProviderUFW:
|
|
return filterufw.NewAdapter(), nil
|
|
case filter.ProviderFirewalld:
|
|
return filterfirewalld.NewAdapter(), nil
|
|
case filter.ProviderIptables:
|
|
return filteriptables.NewAdapter(), nil
|
|
case filter.ProviderNftables:
|
|
return filternftables.NewAdapter(), nil
|
|
default:
|
|
return nil, fmt.Errorf("%w: %s", filter.ErrAdapterUnavailable, provider)
|
|
}
|
|
}
|
|
|
|
func loadFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
|
ports, err := loadPortWhitelistSetting()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return firewall.ValidatePortWhitelist(ports)
|
|
}
|
|
|
|
func prepareFirewallBackendRule(ctx context.Context, client filter.Adapter, rule filter.FirewallRule) (filter.FirewallRule, error) {
|
|
if preparer, ok := client.(filter.RulePreparer); ok {
|
|
var err error
|
|
rule, err = preparer.PrepareRule(rule)
|
|
if err != nil {
|
|
return filter.FirewallRule{}, err
|
|
}
|
|
}
|
|
if checker, ok := client.(filter.RuleChecker); ok {
|
|
if err := checker.CheckRule(ctx, rule); err != nil {
|
|
return filter.FirewallRule{}, err
|
|
}
|
|
}
|
|
return rule, nil
|
|
}
|
|
|
|
func readMutableFirewallRules(client filter.Adapter, ctx context.Context, scope filter.Scope) (filter.RuleSet, error) {
|
|
snapshots, err := listFirewallRuleScopes(client, ctx, []filter.Scope{scope})
|
|
if err != nil {
|
|
return filter.RuleSet{}, err
|
|
}
|
|
snapshot := snapshots[0]
|
|
for _, notice := range snapshot.Notices {
|
|
if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing {
|
|
return filter.RuleSet{}, fmt.Errorf("%w: managed firewall scope is unavailable", filter.ErrProviderUnavailable)
|
|
}
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func firewallScopeReadGroups(scopes []filter.Scope) [][]filter.Scope {
|
|
groups := make([][]filter.Scope, 0)
|
|
indexes := make(map[string]int)
|
|
for _, scope := range scopes {
|
|
scope = scope.Normalize()
|
|
key := scope.Key()
|
|
if scope.Provider == filter.ProviderUFW {
|
|
key = string(scope.Provider)
|
|
}
|
|
if scope.Provider == filter.ProviderIptables || scope.Provider == filter.ProviderNftables {
|
|
key = string(scope.Provider) + ":" + string(scope.Family) + ":" + scope.Table
|
|
}
|
|
if index, ok := indexes[key]; ok {
|
|
groups[index] = append(groups[index], scope)
|
|
} else {
|
|
indexes[key] = len(groups)
|
|
groups = append(groups, []filter.Scope{scope})
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
|
|
func listFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
|
|
unique := make([]filter.Scope, 0, len(scopes))
|
|
seen := make(map[string]bool)
|
|
for _, scope := range scopes {
|
|
scope = scope.Normalize()
|
|
if err := scope.ValidateMVP(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !seen[scope.Key()] {
|
|
seen[scope.Key()] = true
|
|
unique = append(unique, scope)
|
|
}
|
|
}
|
|
if len(unique) == 0 {
|
|
return nil, nil
|
|
}
|
|
if reader, ok := client.(filter.MultiScopeReader); ok {
|
|
snapshots, err := reader.ListRuleScopes(ctx, unique)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(snapshots) != len(unique) {
|
|
return nil, filter.ErrInventoryUnavailable
|
|
}
|
|
return snapshots, nil
|
|
}
|
|
snapshots := make([]filter.RuleSet, 0, len(unique))
|
|
for _, scope := range unique {
|
|
snapshot, err := client.ListRules(ctx, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshots = append(snapshots, snapshot)
|
|
}
|
|
return snapshots, nil
|
|
}
|
|
|
|
func applyFirewallChanges(client filter.Adapter, ctx context.Context, snapshot filter.RuleSet, changes []filter.RuleChange) (filter.CommandBatch, error) {
|
|
plan, err := client.BuildCommands(snapshot, changes)
|
|
if err != nil {
|
|
return filter.CommandBatch{}, err
|
|
}
|
|
if err := client.RunCommands(ctx, plan); err != nil {
|
|
return plan, err
|
|
}
|
|
if saver, ok := client.(filter.RuleSaver); ok {
|
|
return plan, saver.SaveRules(ctx, plan.Scope)
|
|
}
|
|
return plan, nil
|
|
}
|
|
|
|
func persistFirewallRuleBatches(ctx context.Context, client filter.Adapter, plans []filter.CommandBatch) []error {
|
|
failures := make([]error, len(plans))
|
|
saver, ok := client.(filter.RuleSaver)
|
|
if !ok {
|
|
return failures
|
|
}
|
|
saved := make(map[string]error)
|
|
for index, plan := range plans {
|
|
key := plan.Scope.Key()
|
|
if plan.Provider == filter.ProviderNftables {
|
|
key = string(plan.Provider)
|
|
}
|
|
err, exists := saved[key]
|
|
if !exists {
|
|
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
|
err = saver.SaveRules(saveCtx, plan.Scope)
|
|
cancel()
|
|
saved[key] = err
|
|
}
|
|
failures[index] = err
|
|
}
|
|
return failures
|
|
}
|
|
|
|
func executeFirewallRuleBatches(ctx context.Context, runtime filter.Adapter, items []firewallRuleBatchItem) []error {
|
|
groups := make([][]int, 0)
|
|
groupIndexes := make(map[string]int)
|
|
for index, item := range items {
|
|
key := string(item.change.Operation) + ":" + item.snapshot.Scope.Key()
|
|
if runtime.Provider() == filter.ProviderUFW {
|
|
key = string(item.change.Operation)
|
|
} else if runtime.Provider() == filter.ProviderFirewalld {
|
|
key += ":" + string(item.change.After.NativeKind)
|
|
}
|
|
group, exists := groupIndexes[key]
|
|
if !exists {
|
|
group = len(groups)
|
|
groupIndexes[key] = group
|
|
groups = append(groups, nil)
|
|
}
|
|
groups[group] = append(groups[group], index)
|
|
}
|
|
failures := make([]error, len(items))
|
|
type appliedBatch struct {
|
|
plan filter.CommandBatch
|
|
sourceIndexes []int
|
|
}
|
|
var applied []appliedBatch
|
|
for _, group := range groups {
|
|
for start := 0; start < len(group); {
|
|
end := start + 1
|
|
if runtime.Provider() != filter.ProviderUFW {
|
|
end = min(start+filter.MaxAtomicExpansion, len(group))
|
|
}
|
|
batch := group[start:end]
|
|
start = end
|
|
changes := make([]filter.RuleChange, 0, len(batch))
|
|
for _, index := range batch {
|
|
change := items[index].change
|
|
change.CommandOnly = true
|
|
changes = append(changes, change)
|
|
}
|
|
var plan filter.CommandBatch
|
|
err := ctx.Err()
|
|
if err == nil {
|
|
plan, err = runtime.BuildCommands(items[batch[0]].snapshot, changes)
|
|
}
|
|
if err == nil {
|
|
plan.CommandOnly = true
|
|
err = runtime.RunCommands(ctx, plan)
|
|
}
|
|
if err != nil {
|
|
for _, index := range batch {
|
|
failures[index] = err
|
|
}
|
|
continue
|
|
}
|
|
applied = append(applied, appliedBatch{plan: plan, sourceIndexes: batch})
|
|
}
|
|
}
|
|
if len(applied) == 0 {
|
|
return failures
|
|
}
|
|
plans := make([]filter.CommandBatch, len(applied))
|
|
for index, batch := range applied {
|
|
plans[index] = batch.plan
|
|
}
|
|
saved := persistFirewallRuleBatches(ctx, runtime, plans)
|
|
for index, batch := range applied {
|
|
for _, item := range batch.sourceIndexes {
|
|
failures[item] = saved[index]
|
|
}
|
|
}
|
|
return failures
|
|
}
|
|
|
|
func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested filter.Provider) error {
|
|
selected, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if selected != requested {
|
|
return fmt.Errorf("%w: selected provider is %s, requested %s", filter.ErrProviderUnavailable, selected, requested)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) {
|
|
var firstFailure error
|
|
defer func() {
|
|
summary := i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{
|
|
"succeeded": result.Succeeded, "failed": result.Failed, "skipped": result.Skipped,
|
|
})
|
|
if t != nil {
|
|
t.Log(summary)
|
|
}
|
|
if result.Failed > 0 {
|
|
taskErr = errors.Join(errors.New(summary), taskErr, firstFailure)
|
|
}
|
|
}()
|
|
type createItem struct {
|
|
index, part, count int
|
|
request dto.FirewallRuleCreateItem
|
|
}
|
|
describe := func(rule filter.FirewallRule) string {
|
|
return fmt.Sprintf("%s %s %s:%s -> %s:%s %s", rule.Scope.Family, rule.Protocol,
|
|
rule.SourceAddress, rule.SourcePort, rule.DestinationAddress, rule.DestinationPort, rule.Action)
|
|
}
|
|
record := func(item createItem, status string, err error) {
|
|
rule := item.request.Rule
|
|
label := fmt.Sprintf("[%d/%d]", item.index+1, len(request.Items))
|
|
if item.count > 1 {
|
|
label += fmt.Sprintf("[%d/%d]", item.part+1, item.count)
|
|
}
|
|
label += fmt.Sprintf(" %s %s", rule.Scope.Provider, describe(rule))
|
|
switch status {
|
|
case "succeeded":
|
|
result.Succeeded++
|
|
if t != nil {
|
|
t.LogSuccess(label)
|
|
}
|
|
case "failed":
|
|
if firstFailure == nil {
|
|
firstFailure = err
|
|
}
|
|
result.Failed++
|
|
if t != nil {
|
|
t.LogFailedWithErr(label, err)
|
|
}
|
|
case "skipped":
|
|
result.Skipped++
|
|
if t != nil {
|
|
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
result.Errors = append(result.Errors, dto.FirewallRuleCreateFailure{
|
|
Index: item.index, Status: status, Rule: rule, Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
selected, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
for index, item := range request.Items {
|
|
record(createItem{index: index, request: item}, "skipped", err)
|
|
}
|
|
return result, err
|
|
}
|
|
runtime, err := s.firewallAdapter(selected)
|
|
if err != nil {
|
|
for index, item := range request.Items {
|
|
record(createItem{index: index, request: item}, "skipped", err)
|
|
}
|
|
return result, err
|
|
}
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
preparedItems := make([]*createItem, 0, len(request.Items))
|
|
var stop error
|
|
for index, item := range request.Items {
|
|
if stop == nil {
|
|
stop = ctx.Err()
|
|
}
|
|
if stop != nil {
|
|
record(createItem{index: index, request: item}, "skipped", stop)
|
|
continue
|
|
}
|
|
item.Rule.OrderIndex = nil
|
|
rules, err := expandFirewallCreateRule(item, selected)
|
|
if err != nil {
|
|
status := "failed"
|
|
if item.SourceKind == constant.FirewallRuleSourceImported && errors.Is(err, filter.ErrUnsupportedScope) {
|
|
status = "skipped"
|
|
}
|
|
record(createItem{index: index, request: item}, status, err)
|
|
continue
|
|
}
|
|
if item.SourceKind == constant.FirewallRuleSourceImported && t != nil {
|
|
t.Log(i18n.GetMsgWithMap("FirewallImportRuleConversion", map[string]interface{}{
|
|
"index": index + 1, "total": len(request.Items), "source": item.Rule.Scope.Provider,
|
|
"target": selected, "rule": describe(item.Rule), "count": len(rules),
|
|
}))
|
|
}
|
|
for part, rule := range rules {
|
|
child := item
|
|
child.Rule = rule
|
|
entry := createItem{index: index, part: part, count: len(rules), request: child}
|
|
if (request.Initialize || item.SourceKind == constant.FirewallRuleSourceImported) && rule.Scope.Family == filter.FamilyIPv6 && !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
record(entry, "skipped", fmt.Errorf("IPv6 firewall support is disabled"))
|
|
continue
|
|
}
|
|
prepared, err := prepareFirewallCreateRule(ctx, runtime, child)
|
|
if err != nil {
|
|
status := "failed"
|
|
if item.SourceKind == constant.FirewallRuleSourceImported && (errors.Is(err, filter.ErrUnsupportedScope) || errors.Is(err, filter.ErrInvalidRule)) {
|
|
status = "skipped"
|
|
}
|
|
record(entry, status, err)
|
|
continue
|
|
}
|
|
entry.request = prepared
|
|
if child.ParseStatus == "" || child.ParseStatus == filter.ParseStatusSupported {
|
|
entry.request.Raw = ""
|
|
}
|
|
preparedItems = append(preparedItems, &entry)
|
|
}
|
|
}
|
|
if len(preparedItems) == 0 {
|
|
return result, stop
|
|
}
|
|
if ruleState == nil {
|
|
ruleState = make(map[string]filter.RuleSet)
|
|
}
|
|
if len(ruleState) == 0 {
|
|
if err := loadFirewallRuleState(ctx, runtime, ruleState); err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
existing := make(map[string]bool)
|
|
ufwActions := make(map[string]map[filter.Action]bool)
|
|
for _, snapshot := range ruleState {
|
|
for _, observed := range snapshot.Rules {
|
|
key, err := firewallRuleDuplicateKey(observed, false)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
existing[key] = true
|
|
if selected == filter.ProviderUFW && observed.ParseStatus == filter.ParseStatusSupported {
|
|
matchKey, err := filter.RuleMatchKey(observed.Rule)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if ufwActions[matchKey] == nil {
|
|
ufwActions[matchKey] = make(map[filter.Action]bool)
|
|
}
|
|
ufwActions[matchKey][observed.Rule.Action] = true
|
|
}
|
|
if selected == filter.ProviderFirewalld && observed.ParseStatus == filter.ParseStatusSupported {
|
|
families := []filter.Family{observed.Rule.Scope.Family}
|
|
if families[0] == filter.FamilyInet {
|
|
families = append(families, filter.FamilyIPv4, filter.FamilyIPv6)
|
|
}
|
|
for _, family := range families {
|
|
candidate := observed
|
|
candidate.Rule.Scope.Family = family
|
|
key, err := firewallRuleDuplicateKey(candidate, true)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
existing[key] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mergedItems := make([]*createItem, 0, len(preparedItems))
|
|
pending := make(map[string]bool)
|
|
priority := 0
|
|
for _, item := range preparedItems {
|
|
converted := item.request.SourceKind == constant.FirewallRuleSourceImported && selected == filter.ProviderFirewalld && request.Items[item.index].Rule.Scope.Provider != selected
|
|
observed := filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus}
|
|
key, err := firewallRuleDuplicateKey(observed, converted)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if pending[key] {
|
|
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
|
continue
|
|
}
|
|
if converted {
|
|
priority++
|
|
}
|
|
if existing[key] {
|
|
pending[key] = true
|
|
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
|
continue
|
|
}
|
|
if selected == filter.ProviderUFW && item.request.ParseStatus == filter.ParseStatusSupported {
|
|
matchKey, err := filter.RuleMatchKey(item.request.Rule)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
actions := ufwActions[matchKey]
|
|
if len(actions) > 1 || len(actions) == 1 && !actions[item.request.Rule.Action] {
|
|
record(*item, "failed", buserr.New("ErrFirewallRuleConflict"))
|
|
continue
|
|
}
|
|
ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true}
|
|
}
|
|
if converted {
|
|
if priority > 32767 {
|
|
return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit)
|
|
}
|
|
value := priority
|
|
item.request.Rule.Priority = &value
|
|
prepared, err := prepareFirewallCreateRule(ctx, runtime, item.request)
|
|
if err != nil {
|
|
status := "failed"
|
|
if errors.Is(err, filter.ErrUnsupportedScope) || errors.Is(err, filter.ErrInvalidRule) {
|
|
status = "skipped"
|
|
}
|
|
record(*item, status, err)
|
|
continue
|
|
}
|
|
item.request = prepared
|
|
actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: prepared.Rule, ParseStatus: prepared.ParseStatus}, false)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
pending[key] = true
|
|
if existing[actualKey] || pending[actualKey] {
|
|
record(*item, "skipped", buserr.New("ErrRecordExist"))
|
|
continue
|
|
}
|
|
pending[actualKey] = true
|
|
}
|
|
pending[key] = true
|
|
mergedItems = append(mergedItems, item)
|
|
}
|
|
for _, item := range mergedItems {
|
|
item.request.Rule.UUID = uuid.NewString()
|
|
}
|
|
creates := mergedItems
|
|
rawScopes := make([]filter.Scope, 0)
|
|
rawByScope := make(map[string]filter.RuleSet)
|
|
for _, item := range creates {
|
|
if item.request.Raw != "" {
|
|
scope := item.request.Rule.Scope
|
|
if snapshot, exists := ruleState[scope.Key()]; exists {
|
|
rawByScope[scope.Key()] = snapshot
|
|
} else {
|
|
rawScopes = append(rawScopes, scope)
|
|
}
|
|
}
|
|
}
|
|
rawSnapshots, err := listFirewallRuleScopes(runtime, ctx, rawScopes)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
for _, snapshot := range rawSnapshots {
|
|
rawByScope[snapshot.Scope.Key()] = snapshot
|
|
}
|
|
changes := make([]firewallRuleBatchItem, 0, len(creates))
|
|
for _, item := range creates {
|
|
rule := &item.request.Rule
|
|
snapshot := filter.RuleSet{Scope: rule.Scope}
|
|
if current, exists := rawByScope[rule.Scope.Key()]; exists {
|
|
snapshot = current
|
|
}
|
|
changes = append(changes, firewallRuleBatchItem{
|
|
snapshot: snapshot,
|
|
change: filter.RuleChange{Operation: filter.ChangeCreate, After: rule, Append: true, Raw: item.request.Raw},
|
|
})
|
|
}
|
|
descriptions := make([]model.CommonDescription, 0, len(creates))
|
|
for index, err := range executeFirewallRuleBatches(ctx, runtime, changes) {
|
|
item := creates[index]
|
|
if err != nil {
|
|
if errors.Is(err, filterfirewalld.ErrAlreadyEnabled) || errors.Is(err, filterufw.ErrAlreadyEnabled) {
|
|
record(*item, "skipped", err)
|
|
} else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
record(*item, "skipped", err)
|
|
stop = err
|
|
} else {
|
|
record(*item, "failed", fmt.Errorf("%s: %w", i18n.GetMsgByKey("FirewallCreateRuleExecutionFailed"), err))
|
|
}
|
|
continue
|
|
}
|
|
scope := item.request.Rule.Scope
|
|
snapshot := ruleState[scope.Key()]
|
|
snapshot.Scope = scope
|
|
snapshot.Rules = append(snapshot.Rules, filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus})
|
|
ruleState[scope.Key()] = snapshot
|
|
if item.request.Rule.Description != "" {
|
|
observed := filter.ObservedRule{Rule: item.request.Rule, Raw: item.request.Raw, ParseStatus: item.request.ParseStatus}
|
|
id, err := filter.DescriptionID(observed)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
descriptions = append(descriptions, model.CommonDescription{ID: id, Type: "firewall", Description: item.request.Rule.Description})
|
|
}
|
|
record(*item, "succeeded", nil)
|
|
}
|
|
if len(descriptions) > 0 {
|
|
persistCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
|
err := settingRepo.SaveDescriptions(persistCtx, descriptions)
|
|
cancel()
|
|
if err != nil {
|
|
if t != nil {
|
|
t.Logf("save firewall rule descriptions: %v", err)
|
|
}
|
|
global.LOG.Warnf("save firewall rule descriptions: %v", err)
|
|
}
|
|
}
|
|
return result, stop
|
|
}
|
|
|
|
func validateFirewallCreateBatch(request dto.FirewallRuleCreate, provider filter.Provider) error {
|
|
if len(request.Items) == 0 && !request.Initialize {
|
|
return filter.ErrInvalidRule
|
|
}
|
|
count := 0
|
|
importing := true
|
|
for _, item := range request.Items {
|
|
importing = importing && item.SourceKind == constant.FirewallRuleSourceImported
|
|
rules, err := expandFirewallCreateRule(item, provider)
|
|
if errors.Is(err, filter.ErrExpansionLimit) {
|
|
return err
|
|
}
|
|
count += len(rules)
|
|
}
|
|
if !importing && count > filter.MaxAtomicExpansion {
|
|
return filter.ErrExpansionLimit
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func expandFirewallCreateRule(item dto.FirewallRuleCreateItem, provider filter.Provider) ([]filter.FirewallRule, error) {
|
|
rule := item.Rule
|
|
if item.Raw != "" && item.ParseStatus != "" && item.ParseStatus != filter.ParseStatusSupported {
|
|
if item.SourceKind != constant.FirewallRuleSourceImported || rule.Scope.Provider != provider {
|
|
return nil, fmt.Errorf("%w: native rule can only be imported into its original backend", filter.ErrUnsupportedScope)
|
|
}
|
|
return []filter.FirewallRule{rule}, rule.Scope.ValidateMVP()
|
|
}
|
|
source := rule.Scope.Provider
|
|
if item.SourceKind != constant.FirewallRuleSourceImported || source == "" || source == provider {
|
|
return filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(rule, provider))
|
|
}
|
|
if rule.NativeKind == filter.NativeKindOpaque || rule.NativeKind == filter.NativeKindZoneService || rule.NativeKind == filter.NativeKindUFWApplication {
|
|
return nil, fmt.Errorf("%w: native rule cannot be converted to %s", filter.ErrUnsupportedScope, provider)
|
|
}
|
|
if provider == filter.ProviderFirewalld {
|
|
priority := 0
|
|
rule.Priority = &priority
|
|
}
|
|
families := []filter.Family{rule.Scope.Family}
|
|
if families[0] == filter.FamilyInet && provider != filter.ProviderFirewalld {
|
|
switch {
|
|
case strings.Contains(rule.SourceAddress, ":") || strings.Contains(rule.DestinationAddress, ":") || rule.Protocol == "icmpv6":
|
|
families = []filter.Family{filter.FamilyIPv6}
|
|
case rule.SourceAddress != "" || rule.DestinationAddress != "":
|
|
families = []filter.Family{filter.FamilyIPv4}
|
|
default:
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
}
|
|
var result []filter.FirewallRule
|
|
for _, family := range families {
|
|
converted := rule
|
|
converted.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
|
|
if (source == filter.ProviderIptables || source == filter.ProviderNftables) && (provider == filter.ProviderIptables || provider == filter.ProviderNftables) {
|
|
converted.Scope.Chain = rule.Scope.Chain
|
|
}
|
|
converted.NativeKind, converted.OrderBucket = "", ""
|
|
converted.UUID, converted.OrderIndex = "", nil
|
|
if provider != filter.ProviderFirewalld {
|
|
converted.Priority = nil
|
|
}
|
|
expanded, err := filter.ExpandAtomicRules(applySelectedProviderScopeDefaults(converted, provider))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, expanded...)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func applySelectedProviderScopeDefaults(rule filter.FirewallRule, selected filter.Provider) filter.FirewallRule {
|
|
scope := rule.Scope
|
|
if scope.Provider == "" {
|
|
scope.Provider = selected
|
|
}
|
|
if scope.Provider != selected {
|
|
return rule
|
|
}
|
|
if scope.Direction == "" {
|
|
scope.Direction = filter.DirectionInput
|
|
}
|
|
if scope.Family == "" {
|
|
switch {
|
|
case strings.EqualFold(strings.TrimSpace(rule.Protocol), "icmpv6"), strings.Contains(rule.SourceAddress, ":"), strings.Contains(rule.DestinationAddress, ":"):
|
|
scope.Family = filter.FamilyIPv6
|
|
case selected == filter.ProviderFirewalld:
|
|
scope.Family = filter.FamilyInet
|
|
default:
|
|
scope.Family = filter.FamilyIPv4
|
|
}
|
|
}
|
|
switch selected {
|
|
case filter.ProviderIptables, filter.ProviderNftables:
|
|
if scope.Table == "" {
|
|
scope.Table = "filter"
|
|
}
|
|
if scope.Chain == "" {
|
|
scope.Chain = filter.IptablesInputChain
|
|
}
|
|
case filter.ProviderFirewalld:
|
|
if scope.Zone == "" {
|
|
scope.Zone = filter.FirewalldInputZone
|
|
}
|
|
case filter.ProviderUFW:
|
|
if scope.Chain == "" {
|
|
scope.Chain = filter.UFWInputChain
|
|
}
|
|
}
|
|
rule.Scope = scope
|
|
return rule
|
|
}
|
|
|
|
func prepareFirewallCreateRule(ctx context.Context, runtime filter.Adapter, request dto.FirewallRuleCreateItem) (dto.FirewallRuleCreateItem, error) {
|
|
request.Rule.Description = strings.TrimSpace(request.Rule.Description)
|
|
selected := runtime.Provider()
|
|
if (selected == filter.ProviderIptables || selected == filter.ProviderNftables) && request.Rule.Scope.Chain != "" && request.Rule.Scope.Chain != filter.IptablesInputChain && request.Rule.Description != "" {
|
|
return dto.FirewallRuleCreateItem{}, filter.ErrRuleOperation
|
|
}
|
|
if request.Raw != "" && request.ParseStatus != "" && request.ParseStatus != filter.ParseStatusSupported {
|
|
if request.Rule.Scope.Provider != selected {
|
|
return dto.FirewallRuleCreateItem{}, filter.ErrUnsupportedScope
|
|
}
|
|
plan, err := runtime.BuildCommands(filter.RuleSet{Scope: request.Rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &request.Rule, Raw: request.Raw, CommandOnly: true, Append: true}})
|
|
if err != nil {
|
|
return dto.FirewallRuleCreateItem{}, err
|
|
}
|
|
description := request.Rule.Description
|
|
request.Rule = plan.Rules[0].Expected.Rule
|
|
request.Rule.Description = description
|
|
if plan.Rules[0].Expected.ParseStatus == filter.ParseStatusSupported {
|
|
request.ParseStatus, request.Raw = filter.ParseStatusSupported, ""
|
|
}
|
|
return request, nil
|
|
}
|
|
rule, err := filter.NormalizeRule(applySelectedProviderScopeDefaults(request.Rule, selected))
|
|
if err != nil {
|
|
return dto.FirewallRuleCreateItem{}, err
|
|
}
|
|
if rule.Scope.Provider != selected {
|
|
return dto.FirewallRuleCreateItem{}, fmt.Errorf("%w: selected provider is %s", filter.ErrInvalidRule, selected)
|
|
}
|
|
rule, err = prepareFirewallBackendRule(ctx, runtime, rule)
|
|
if err != nil {
|
|
return dto.FirewallRuleCreateItem{}, err
|
|
}
|
|
rule.UUID = ""
|
|
request.Rule = rule
|
|
request.ParseStatus = filter.ParseStatusSupported
|
|
if request.SourceKind == "" {
|
|
request.SourceKind = constant.FirewallRuleSourceUser
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func readFirewallRuleScopes(client filter.Adapter, ctx context.Context, scopes []filter.Scope) ([]filter.RuleSet, error) {
|
|
snapshots, err := listFirewallRuleScopes(client, ctx, scopes)
|
|
if err != nil || len(snapshots) == 0 {
|
|
return snapshots, err
|
|
}
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for index := range snapshots {
|
|
snapshots[index], err = filter.ProtectRuleSet(snapshots[index], ports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return snapshots, nil
|
|
}
|
|
|
|
func firewallTaskName(operation, subsystem, backend string) string {
|
|
name := i18n.GetMsgByKey(subsystem)
|
|
if backend != "" {
|
|
name += " · " + backend
|
|
}
|
|
key := "FirewallRule" + operation
|
|
if operation == task.TaskExec {
|
|
key = "FirewallTaskInitialize"
|
|
}
|
|
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
|
|
}
|
|
|
|
func closeUnstartedFirewallTask(t *task.Task) {
|
|
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
|
|
cancel()
|
|
}
|
|
global.RemoveTaskCancel(t.TaskID)
|
|
if closer, ok := t.Logger.Out.(io.Closer); ok {
|
|
_ = closer.Close()
|
|
}
|
|
}
|
|
|
|
func loadFirewallRuleState(ctx context.Context, runtime filter.Adapter, state map[string]filter.RuleSet) error {
|
|
for _, group := range firewallScopeReadGroups(filter.ManagedInputScopes(runtime.Provider())) {
|
|
snapshots, err := listFirewallRuleScopes(runtime, ctx, group)
|
|
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, snapshot := range snapshots {
|
|
missing := false
|
|
for _, notice := range snapshot.Notices {
|
|
missing = missing || notice.Code == filter.ScopeNoticeManagedScopeMissing || notice.Code == filter.ScopeNoticeManagedScopeInactive
|
|
}
|
|
if !missing {
|
|
state[snapshot.Scope.Key()] = snapshot
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func firewallRuleDuplicateKey(observed filter.ObservedRule, converted bool) (string, error) {
|
|
if converted && observed.ParseStatus == filter.ParseStatusSupported && observed.Rule.Scope.Provider == filter.ProviderFirewalld {
|
|
priority := 0
|
|
observed.Rule.Priority = &priority
|
|
observed.Rule.NativeKind, observed.Rule.OrderBucket = filter.NativeKindRichRule, ""
|
|
}
|
|
key, err := filter.DescriptionID(observed)
|
|
if converted {
|
|
key = "converted:" + key
|
|
}
|
|
return key, err
|
|
}
|
|
|
|
func (s *FirewallService) syncPortWhitelist(ctx context.Context, ruleState map[string]filter.RuleSet) error {
|
|
firewallWhitelistMu.Lock()
|
|
defer firewallWhitelistMu.Unlock()
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.applyPortWhitelist(ctx, ports, ruleState)
|
|
}
|
|
|
|
func (s *FirewallService) applyPortWhitelist(ctx context.Context, ports []firewall.PortWhitelist, ruleState map[string]filter.RuleSet, families ...filter.Family) error {
|
|
required, err := firewall.RequiredPortWhitelist(ports)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
provider, err := s.selectedProvider(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
direct := provider == filter.ProviderIptables || provider == filter.ProviderNftables
|
|
if !direct {
|
|
client, err := s.baseClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
active, err := client.Status()
|
|
if err != nil || !active {
|
|
return err
|
|
}
|
|
}
|
|
runtime, err := s.firewallAdapter(provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
if ruleState == nil {
|
|
ruleState = make(map[string]filter.RuleSet)
|
|
if err := loadFirewallRuleState(ctx, runtime, ruleState); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
keys := make(map[string]bool)
|
|
for _, snapshot := range ruleState {
|
|
for _, observed := range snapshot.Rules {
|
|
if observed.ParseStatus != filter.ParseStatusSupported {
|
|
continue
|
|
}
|
|
families := []filter.Family{observed.Rule.Scope.Family}
|
|
if families[0] == filter.FamilyInet {
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
for _, family := range families {
|
|
rule := observed.Rule
|
|
rule.Scope.Family = family
|
|
key, err := filter.RuleMatchKey(rule)
|
|
if err == nil {
|
|
keys[string(rule.Action)+key] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
|
|
var changes []firewallRuleBatchItem
|
|
for _, rule := range rules {
|
|
if len(families) > 0 && !slices.Contains(families, rule.Scope.Family) {
|
|
continue
|
|
}
|
|
item, err := prepareFirewallCreateRule(ctx, runtime, dto.FirewallRuleCreateItem{Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rule = item.Rule
|
|
snapshot, available := ruleState[rule.Scope.Key()]
|
|
if !available {
|
|
continue
|
|
}
|
|
key, err := filter.RuleMatchKey(rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key = string(rule.Action) + key
|
|
if keys[key] {
|
|
continue
|
|
}
|
|
keys[key] = true
|
|
rule.UUID = uuid.NewString()
|
|
if provider != filter.ProviderFirewalld {
|
|
position := int64(1)
|
|
rule.OrderIndex = &position
|
|
}
|
|
changes = append(changes, firewallRuleBatchItem{snapshot: snapshot, change: filter.RuleChange{Operation: filter.ChangeCreate, After: &rule}})
|
|
}
|
|
var failures []error
|
|
for _, err := range executeFirewallRuleBatches(ctx, runtime, changes) {
|
|
if err != nil && !errors.Is(err, filterfirewalld.ErrAlreadyEnabled) && !errors.Is(err, filterufw.ErrAlreadyEnabled) {
|
|
failures = append(failures, err)
|
|
}
|
|
}
|
|
return errors.Join(failures...)
|
|
}
|
|
|
|
func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule {
|
|
rules := make([]filter.FirewallRule, 0, len(ports)+len(required))
|
|
for _, port := range required {
|
|
rule := firewall.RuleForSystemPort(provider, firewall.SystemPort(port))
|
|
if provider == filter.ProviderIptables || provider == filter.ProviderNftables {
|
|
rule.Scope.Chain = filter.BasicBeforeChain
|
|
}
|
|
rules = append(rules, rule)
|
|
}
|
|
for _, port := range ports {
|
|
rules = append(rules, firewall.RuleForSystemPort(provider, firewall.SystemPort(port)))
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
|
|
result := make([]firewall.PortWhitelist, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.Type == "" {
|
|
result = append(result, entry)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func loadForwardingFirewallOverview(manager forwarding.Adapter, families []string) (dto.FirewallSubsystemStatus, error) {
|
|
ipv4, ipv4Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv4)
|
|
if len(families) == 1 {
|
|
return dto.FirewallSubsystemStatus{IPv6Enabled: false, IsInit: ipv4.Initialized, IsBind: ipv4.Bound, IPv4: ipv4}, ipv4Err
|
|
}
|
|
ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6)
|
|
if ipv6.Available {
|
|
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile, forwarding.LoadDockerIPv4BridgePorts)
|
|
var pathError *os.PathError
|
|
switch {
|
|
case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6":
|
|
ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false
|
|
case err != nil:
|
|
global.LOG.Warnf("IPv6 forwarding RA check failed: %v", err)
|
|
ipv6.Reason = "ipv6_ra_check_failed"
|
|
ipv6.Bound = false
|
|
case len(interfaces) > 0:
|
|
global.LOG.Warnf("IPv6 forwarding RA risk: interfaces %s may depend on RA/SLAAC with accept_ra=1", strings.Join(interfaces, ", "))
|
|
ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces
|
|
ipv6.Bound = false
|
|
case !ipv6.Bound:
|
|
ipv6.Reason = "ipv6_forwarding_not_enabled"
|
|
}
|
|
}
|
|
return dto.FirewallSubsystemStatus{IPv6Enabled: len(families) > 1, IsInit: ipv4.Initialized || ipv6.Initialized, IsBind: ipv4.Bound || ipv6.Bound, IPv4: ipv4, IPv6: ipv6}, errors.Join(ipv4Err, ipv6Err)
|
|
}
|
|
|
|
func loadForwardingFamilyInfo(manager forwarding.Adapter, family string) (dto.FirewallBackendFamilyStatus, error) {
|
|
initialized, bound, partial, err := manager.FamilyState(family)
|
|
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
return dto.FirewallBackendFamilyStatus{}, nil
|
|
}
|
|
available := err == nil
|
|
if manager.Name() == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
|
commands, commandErr := lifecycle.ResolveIptablesCommands()
|
|
available = available && commandErr == nil && commands.IPv6Available()
|
|
}
|
|
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound, Partial: partial}, err
|
|
}
|
|
|
|
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate, t *task.Task, initialize bool, families ...string) (resultErr error) {
|
|
defer func() { resultErr = errors.Join(resultErr, ctx.Err()) }()
|
|
forwardingMutationMu.Lock()
|
|
defer forwardingMutationMu.Unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
var allowed []string
|
|
if initialize || request.Import {
|
|
var err error
|
|
allowed, err = loadFirewallFamilies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
skippedIPv6 := 0
|
|
type operationBatch struct {
|
|
operation forwarding.OperationType
|
|
rules []forwarding.Rule
|
|
}
|
|
groups := make([]operationBatch, 0)
|
|
for _, operation := range request.Rules {
|
|
kind := forwarding.OperationType(operation.Operation)
|
|
if kind != forwarding.OperationAdd && kind != forwarding.OperationRemove {
|
|
return fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
|
|
}
|
|
if len(groups) == 0 || groups[len(groups)-1].operation != kind {
|
|
groups = append(groups, operationBatch{operation: kind})
|
|
}
|
|
for _, protocol := range strings.Split(operation.Protocol, "/") {
|
|
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
|
Family: operation.Family, Protocol: protocol, Port: operation.Port,
|
|
TargetIP: operation.TargetIP, TargetPort: operation.TargetPort, Interface: operation.Interface,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(allowed) > 0 && rule.Family == forwarding.FamilyIPv6 && !slices.Contains(allowed, forwarding.FamilyIPv6) {
|
|
skippedIPv6++
|
|
continue
|
|
}
|
|
groups[len(groups)-1].rules = append(groups[len(groups)-1].rules, rule)
|
|
}
|
|
}
|
|
logFirewallIPv6Skipped(t, "forwarding", skippedIPv6)
|
|
groups = slices.DeleteFunc(groups, func(group operationBatch) bool { return len(group.rules) == 0 })
|
|
if !initialize && len(groups) == 0 && skippedIPv6 > 0 {
|
|
return nil
|
|
}
|
|
manager, err := s.clientFactory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if initialize {
|
|
if err := initializeForwarding(manager, allowed); err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(allowed, forwarding.FamilyIPv6) && t != nil {
|
|
t.Logf("IPv6 firewall support is disabled; skipping IPv6 forwarding chain initialization")
|
|
}
|
|
for _, family := range families {
|
|
if !slices.Contains(allowed, family) {
|
|
continue
|
|
}
|
|
if err := manager.OperateFamily(family, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
initialized, _, err := manager.InitStatus()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !initialized {
|
|
return filter.ErrProviderUnavailable
|
|
}
|
|
checkedFamilies := make(map[string]bool)
|
|
for _, group := range groups {
|
|
for _, rule := range group.rules {
|
|
if checkedFamilies[rule.Family] {
|
|
continue
|
|
}
|
|
checkedFamilies[rule.Family] = true
|
|
ready, _, err := manager.FamilyStatus(rule.Family)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ready {
|
|
if !initialize {
|
|
return fmt.Errorf("%s forwarding chains are not initialized", rule.Family)
|
|
}
|
|
if err := manager.OperateFamily(rule.Family, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stored, err := manager.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stored = slices.Clone(stored)
|
|
counts := make(map[string]int, len(stored))
|
|
for index, rule := range stored {
|
|
normalized, err := forwarding.NormalizeRule(rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stored[index] = normalized
|
|
counts[normalized.Identity()]++
|
|
}
|
|
succeeded, failed, skipped := 0, 0, 0
|
|
defer func() {
|
|
if t != nil {
|
|
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{"succeeded": succeeded, "failed": failed}))
|
|
if skipped > 0 {
|
|
t.Logf("%s: %d", i18n.GetMsgByKey("FirewallCreateRuleSkipped"), skipped)
|
|
}
|
|
}
|
|
}()
|
|
record := func(operation forwarding.OperationType, rule forwarding.Rule, status string, cause error) {
|
|
label := fmt.Sprintf("%s %s %s %s -> %s:%s", operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
|
|
switch status {
|
|
case "skipped":
|
|
skipped++
|
|
if t != nil {
|
|
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
|
|
}
|
|
case "failed":
|
|
failed++
|
|
if t != nil {
|
|
t.LogFailedWithErr(label, cause)
|
|
}
|
|
default:
|
|
succeeded++
|
|
if t != nil {
|
|
t.LogSuccess(label)
|
|
}
|
|
}
|
|
}
|
|
var failures []error
|
|
var interfaces map[string]bool
|
|
for _, group := range groups {
|
|
if group.operation != forwarding.OperationAdd {
|
|
continue
|
|
}
|
|
for _, rule := range group.rules {
|
|
if rule.Interface == "" {
|
|
continue
|
|
}
|
|
if counts[rule.Identity()] > 0 {
|
|
continue
|
|
}
|
|
if interfaces == nil {
|
|
available, err := net.Interfaces()
|
|
if err != nil {
|
|
return fmt.Errorf("load forwarding interfaces: %w", err)
|
|
}
|
|
interfaces = make(map[string]bool, len(available))
|
|
for _, item := range available {
|
|
interfaces[item.Name] = true
|
|
}
|
|
}
|
|
if !interfaces[rule.Interface] {
|
|
err := buserr.WithMap("ErrForwardInterfaceNotFound", map[string]interface{}{"name": rule.Interface}, nil)
|
|
failures = append(failures, err)
|
|
record(group.operation, rule, "failed", err)
|
|
}
|
|
}
|
|
}
|
|
if len(failures) > 0 {
|
|
return errors.Join(failures...)
|
|
}
|
|
isEdit := len(request.Rules) == 2 && len(groups) == 2 && groups[0].operation == forwarding.OperationRemove && groups[1].operation == forwarding.OperationAdd
|
|
if isEdit {
|
|
old := make(map[string]bool, len(groups[0].rules))
|
|
for _, rule := range groups[0].rules {
|
|
old[rule.Identity()] = true
|
|
}
|
|
unchanged := len(old) == len(groups[1].rules)
|
|
duplicate := false
|
|
next := make(map[string]bool, len(groups[1].rules))
|
|
for _, rule := range groups[1].rules {
|
|
key := rule.Identity()
|
|
next[key] = true
|
|
unchanged = unchanged && old[key]
|
|
if counts[key] > 0 && !old[key] {
|
|
duplicate = true
|
|
}
|
|
}
|
|
if unchanged || duplicate {
|
|
for _, group := range groups {
|
|
for _, rule := range group.rules {
|
|
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, rule := range groups[0].rules {
|
|
if counts[rule.Identity()] == 0 {
|
|
record(forwarding.OperationRemove, rule, "failed", filter.ErrRuleStale)
|
|
return filter.ErrRuleStale
|
|
}
|
|
}
|
|
removals := make([]forwarding.Rule, 0, len(groups[0].rules))
|
|
for _, rule := range groups[0].rules {
|
|
if !next[rule.Identity()] {
|
|
removals = append(removals, rule)
|
|
}
|
|
}
|
|
groups[0], groups[1] = groups[1], operationBatch{operation: forwarding.OperationRemove, rules: removals}
|
|
}
|
|
for _, group := range groups {
|
|
byFamily := make(map[string][]forwarding.Rule, 2)
|
|
seen := make(map[string]bool, len(group.rules))
|
|
for _, rule := range group.rules {
|
|
key := rule.Identity()
|
|
exists := counts[key] > 0
|
|
if seen[key] || (group.operation == forwarding.OperationAdd && exists) {
|
|
record(group.operation, rule, "skipped", buserr.New("ErrRecordExist"))
|
|
continue
|
|
}
|
|
if group.operation == forwarding.OperationRemove && !exists {
|
|
failures = append(failures, filter.ErrRuleStale)
|
|
record(group.operation, rule, "failed", filter.ErrRuleStale)
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
copies := 1
|
|
if group.operation == forwarding.OperationRemove && manager.Name() == constant.FirewallProviderIptables {
|
|
copies = counts[key]
|
|
}
|
|
for i := 0; i < copies; i++ {
|
|
byFamily[rule.Family] = append(byFamily[rule.Family], rule)
|
|
}
|
|
}
|
|
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
|
rules := byFamily[family]
|
|
if len(rules) == 0 {
|
|
continue
|
|
}
|
|
err := ctx.Err()
|
|
|
|
if err == nil {
|
|
if group.operation == forwarding.OperationAdd {
|
|
err = manager.CreateRules(ctx, rules)
|
|
} else {
|
|
err = manager.DeleteRules(ctx, rules)
|
|
}
|
|
}
|
|
if err != nil {
|
|
failures = append(failures, err)
|
|
for _, rule := range rules {
|
|
record(group.operation, rule, "failed", err)
|
|
}
|
|
continue
|
|
}
|
|
for _, rule := range rules {
|
|
if group.operation == forwarding.OperationAdd {
|
|
counts[rule.Identity()]++
|
|
stored = append(stored, rule)
|
|
} else {
|
|
delete(counts, rule.Identity())
|
|
stored = slices.DeleteFunc(stored, func(item forwarding.Rule) bool { return item.Identity() == rule.Identity() })
|
|
}
|
|
record(group.operation, rule, "succeeded", nil)
|
|
}
|
|
}
|
|
if len(failures) > 0 && (isEdit || group.operation == forwarding.OperationRemove) {
|
|
break
|
|
}
|
|
}
|
|
return errors.Join(errors.Join(failures...), persistForwardingRules(manager, stored))
|
|
}
|
|
|
|
func initializeForwarding(manager forwarding.Adapter, families []string) error {
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, manager.Name()); err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(families, forwarding.FamilyIPv6) {
|
|
global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 forwarding chain initialization")
|
|
}
|
|
if err := manager.Enable(families...); err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
|
}
|
|
|
|
func newForwardingService() *ForwardingService {
|
|
return &ForwardingService{
|
|
clientFactory: newForwardingAdapter,
|
|
}
|
|
}
|
|
|
|
func newForwardingAdapter(ctx context.Context) (forwarding.Adapter, error) {
|
|
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
|
selected = strings.TrimSpace(selected)
|
|
if selected == "" {
|
|
selected = constant.FirewallProviderIptables
|
|
}
|
|
return newForwardingAdapterFor(ctx, selected)
|
|
}
|
|
|
|
func newForwardingAdapterFor(ctx context.Context, backend string) (forwarding.Adapter, error) {
|
|
client, err := lifecycle.NewClient(backend)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"%w: selected forwarding backend %s: %w",
|
|
errForwardingBackendUnavailable, backend, err,
|
|
)
|
|
}
|
|
switch client.Name() {
|
|
case constant.FirewallProviderIptables:
|
|
return forwarding.NewIptables(ctx, client.Name()), nil
|
|
case constant.FirewallProviderNftables:
|
|
return forwarding.NewNftables(ctx), nil
|
|
default:
|
|
return nil, errForwardingBackendUnavailable
|
|
}
|
|
}
|
|
|
|
func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerfirewall.Runtime, string, error) {
|
|
if s.runtime != nil {
|
|
return s.runtime, selectedDockerFirewallBackend(constant.FirewallProviderIptables), nil
|
|
}
|
|
cli, err := s.client()
|
|
if err != nil {
|
|
return nil, "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
|
|
}
|
|
defer cli.Close()
|
|
info, err := cli.Info(ctx)
|
|
if err != nil {
|
|
return nil, "", buserr.WithDetail("ErrDockerFailed", err.Error(), err)
|
|
}
|
|
backend := selectedDockerFirewallBackend(dockerFirewallBackend(info))
|
|
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
|
return nil, backend, fmt.Errorf("Docker firewall backend %q is not supported", backend)
|
|
}
|
|
return s.guardRuntime(ctx, backend), backend, nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) guardRuntime(ctx context.Context, backend string) dockerfirewall.Runtime {
|
|
if s.runtimeForBackend != nil {
|
|
return s.runtimeForBackend(ctx, backend)
|
|
}
|
|
if s.runtime != nil {
|
|
return s.runtime
|
|
}
|
|
return newDockerFirewallRuntime(ctx, backend)
|
|
}
|
|
|
|
func newDockerFirewallRuntime(ctx context.Context, backend string) dockerfirewall.Runtime {
|
|
if backend == constant.FirewallProviderNftables {
|
|
return dockerfirewall.NewNftables(ctx)
|
|
}
|
|
return dockerfirewall.NewIptables(ctx)
|
|
}
|
|
|
|
func selectedDockerFirewallBackend(fallback string) string {
|
|
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
|
selected = strings.ToLower(strings.TrimSpace(selected))
|
|
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
|
return selected
|
|
}
|
|
fallback = strings.ToLower(strings.TrimSpace(fallback))
|
|
if fallback == constant.FirewallProviderNftables {
|
|
return fallback
|
|
}
|
|
return constant.FirewallProviderIptables
|
|
}
|
|
|
|
func dockerFirewallBackend(info system.Info) string {
|
|
if info.FirewallBackend == nil || info.FirewallBackend.Driver == "" {
|
|
return constant.FirewallProviderIptables
|
|
}
|
|
return strings.ToLower(info.FirewallBackend.Driver)
|
|
}
|
|
|
|
func dockerPortGuardPersistedEnabled() (bool, error) {
|
|
status, err := settingRepo.GetValueByKey(constant.FirewallDockerPortGuardStatusKey)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return false, nil
|
|
}
|
|
return status == constant.StatusEnable, err
|
|
}
|
|
|
|
func newDockerPortGuardService() *DockerPortGuardService {
|
|
return &DockerPortGuardService{
|
|
client: docker.NewDockerClient,
|
|
version: dockerFirewallVersion,
|
|
}
|
|
}
|
|
|
|
func dockerFirewallVersion(backend string) string {
|
|
client, err := lifecycle.NewClient(backend)
|
|
if err != nil {
|
|
return "-"
|
|
}
|
|
version, err := client.Version()
|
|
if err != nil || strings.TrimSpace(version) == "" {
|
|
return "-"
|
|
}
|
|
return version
|
|
}
|
|
|
|
func firewallDockerActive() (bool, error) {
|
|
if !cmd.Which("docker") {
|
|
return false, nil
|
|
}
|
|
return controller.CheckActive("docker")
|
|
}
|
|
|
|
func restoreFirewalldDependents(ctx context.Context, reason string, restoreDocker bool, restoreForwarding func(context.Context) error, restoreDockerGuard func(context.Context) error) error {
|
|
var errs []error
|
|
if err := restoreForwarding(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("restore port forwarding %s: %w", reason, err))
|
|
}
|
|
if restoreDocker {
|
|
if err := restoreDockerGuard(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("restore Docker port guard %s: %w", reason, err))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func operateFirewallLifecycle(client lifecycle.Client, operation string, withDockerRestart bool, prepareStart func(lifecycle.Client) error, t *task.Task) error {
|
|
run := func(operation, name string, action func() error) error {
|
|
if t != nil {
|
|
return runFirewallLifecycleAction(t, task.GetTaskName(name, operation, ""), action)
|
|
}
|
|
return action()
|
|
}
|
|
switch operation {
|
|
case string(lifecycle.OperationStart):
|
|
if err := run("Start", client.Name(), client.Start); err != nil {
|
|
return err
|
|
}
|
|
case string(lifecycle.OperationRestart):
|
|
if err := run("TaskRestart", client.Name(), client.Restart); err != nil {
|
|
return err
|
|
}
|
|
case string(lifecycle.OperationStop):
|
|
return stopFirewallLifecycle(client, withDockerRestart, nil, t)
|
|
default:
|
|
return fmt.Errorf("not supported operation: %s", operation)
|
|
}
|
|
var recoveryErrors []error
|
|
if prepareStart != nil {
|
|
err := prepareStart(client)
|
|
if err == nil && client.Name() == constant.FirewallProviderFirewalld {
|
|
err = lifecycleproviders.RemoveFirewalldSSHService()
|
|
}
|
|
if err != nil {
|
|
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err))
|
|
}
|
|
}
|
|
if withDockerRestart {
|
|
if err := run("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
|
|
recoveryErrors = append(recoveryErrors, &firewallDockerRestartError{Err: err})
|
|
}
|
|
}
|
|
if client.Name() == constant.FirewallProviderFirewalld && operation == string(lifecycle.OperationStart) {
|
|
if err := run("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil {
|
|
recoveryErrors = append(recoveryErrors, err)
|
|
}
|
|
}
|
|
if err := errors.Join(recoveryErrors...); err != nil {
|
|
return &firewallCompletedOperationError{Operation: operation, Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c firewallLifecycleClient) Start() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Start()
|
|
}
|
|
|
|
func (c firewallLifecycleClient) Restart() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Restart()
|
|
}
|
|
|
|
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
|
|
t.Log(i18n.GetWithName("TaskStart", name))
|
|
started := time.Now()
|
|
err := t.TaskCtx.Err()
|
|
if err == nil {
|
|
err = action()
|
|
}
|
|
t.LogWithStatus(fmt.Sprintf("%s (%.2fs)", name, time.Since(started).Seconds()), err)
|
|
return err
|
|
}
|
|
|
|
func stopFirewallLifecycle(client lifecycle.Client, withDockerRestart bool, prepareStop func() error, t *task.Task) error {
|
|
if client.Name() == constant.FirewallProviderFirewalld {
|
|
if err := rememberFail2BanBeforeFirewallStop(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if prepareStop != nil {
|
|
if err := prepareStop(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
stop := client.Stop
|
|
if t != nil {
|
|
stop = func() error {
|
|
return runFirewallLifecycleAction(t, task.GetTaskName(client.Name(), "Stop", ""), client.Stop)
|
|
}
|
|
}
|
|
if err := stop(); err != nil {
|
|
return err
|
|
}
|
|
if withDockerRestart {
|
|
restart := func() error { return controller.HandleRestart("docker") }
|
|
var err error
|
|
if t != nil {
|
|
err = runFirewallLifecycleAction(t, task.GetTaskName("Docker", "TaskRestart", ""), restart)
|
|
} else {
|
|
err = restart()
|
|
}
|
|
if err != nil {
|
|
return &firewallDockerRestartError{Err: err}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c firewallLifecycleClient) Stop() error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return c.Client.Stop()
|
|
}
|
|
|
|
func rememberFail2BanBeforeFirewallStop() error {
|
|
exists, err := controller.CheckExist("fail2ban.service")
|
|
if err != nil {
|
|
global.LOG.Warnf("check fail2ban.service installation before stopping the firewall failed: %v", err)
|
|
}
|
|
if !exists {
|
|
return nil
|
|
}
|
|
active, err := controller.CheckActive("fail2ban.service")
|
|
if err != nil {
|
|
global.LOG.Warnf("check fail2ban.service status before stopping the firewall failed: %v", err)
|
|
}
|
|
if !active {
|
|
return nil
|
|
}
|
|
if err := os.WriteFile(fail2BanRestoreWithFirewallMarker, nil, 0600); err != nil {
|
|
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func restoreFail2BanAfterFirewallStart() error {
|
|
if _, err := os.Stat(fail2BanRestoreWithFirewallMarker); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
|
|
}
|
|
if err := controller.HandleStart("fail2ban.service"); err != nil {
|
|
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
|
|
}
|
|
if err := os.Remove(fail2BanRestoreWithFirewallMarker); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FirewallService) operateFilterChainBase(provider string, request dto.FilterChainOperation) error {
|
|
firewallRuleMutationMu.Lock()
|
|
defer firewallRuleMutationMu.Unlock()
|
|
return s.operateFilterChainBaseLocked(provider, request)
|
|
}
|
|
|
|
func (s *FirewallService) operateFilterChainBaseLocked(provider string, request dto.FilterChainOperation) error {
|
|
if err := s.checkSelectedProvider(context.Background(), filter.Provider(provider)); err != nil {
|
|
return err
|
|
}
|
|
if provider != constant.FirewallProviderIptables && provider != constant.FirewallProviderNftables {
|
|
return fmt.Errorf("filter chain operations are not supported for %s", provider)
|
|
}
|
|
operation := firewall.BaseOperation(request.Operate)
|
|
var ports []firewall.PortWhitelist
|
|
if operation == firewall.BaseOperationInit {
|
|
var err error
|
|
ports, err = LoadRequiredFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if operation == firewall.BaseOperationInit && !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 host chain initialization")
|
|
}
|
|
if operation == firewall.BaseOperationBind {
|
|
bound := false
|
|
for _, family := range families {
|
|
status, err := loadSystemFirewallFamilyInfo(provider, family)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !status.Initialized {
|
|
continue
|
|
}
|
|
if provider == constant.FirewallProviderNftables {
|
|
err = nftables_helper.OperateFamily(filter.Family(family), false, nil)
|
|
} else {
|
|
err = iptables_helper.OperateFamily(family, false, nil)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bound = true
|
|
}
|
|
if !bound {
|
|
return filter.ErrProviderUnavailable
|
|
}
|
|
} else if len(families) == 1 && operation != firewall.BaseOperationUnbind {
|
|
initialize := operation == firewall.BaseOperationInit
|
|
if provider == constant.FirewallProviderNftables {
|
|
err = nftables_helper.OperateFamily(filter.FamilyIPv4, initialize, ports)
|
|
} else {
|
|
err = iptables_helper.OperateFamily(constant.FirewallFamilyIPv4, initialize, ports)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if provider == constant.FirewallProviderNftables {
|
|
if err := nftables_helper.Operate(operation, ports); err != nil {
|
|
return err
|
|
}
|
|
} else if err := iptables_helper.Operate(operation, ports); err != nil {
|
|
return err
|
|
}
|
|
status := constant.StatusEnable
|
|
if operation == firewall.BaseOperationUnbind {
|
|
status = constant.StatusDisable
|
|
}
|
|
return settingRepo.Update("IptablesStatus", status)
|
|
}
|
|
|
|
func LoadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return firewall.RequiredPortWhitelist(ports)
|
|
}
|
|
|
|
func lockFirewallLifecycleIdle() error {
|
|
if !firewallLifecycleTaskMu.TryLock() {
|
|
return buserr.New("TaskIsExecuting")
|
|
}
|
|
if firewallLifecycleTaskID != "" {
|
|
firewallLifecycleTaskMu.Unlock()
|
|
return buserr.New("TaskIsExecuting")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cleanupInactiveSystemBackend(backend string) error {
|
|
switch backend {
|
|
case constant.FirewallProviderIptables:
|
|
return iptables_helper.Cleanup()
|
|
case constant.FirewallProviderNftables:
|
|
return nftables_helper.Cleanup()
|
|
default:
|
|
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
|
}
|
|
}
|
|
|
|
func cleanupSystemBackend(backend string) error {
|
|
switch backend {
|
|
case constant.FirewallProviderIptables:
|
|
if err := iptables_helper.Cleanup(); err != nil {
|
|
return err
|
|
}
|
|
case constant.FirewallProviderNftables:
|
|
if err := nftables_helper.Cleanup(); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
|
}
|
|
return settingRepo.Update("IptablesStatus", constant.StatusDisable)
|
|
}
|
|
|
|
func resetServiceFirewallBackend(provider string, withDockerRestart bool) error {
|
|
client, err := lifecycle.NewClient(provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return resetServiceFirewallClient(client, withDockerRestart, func(
|
|
client lifecycle.Client,
|
|
restartDocker bool,
|
|
prepareStop func() error,
|
|
) error {
|
|
return stopFirewallLifecycle(client, restartDocker, prepareStop, nil)
|
|
})
|
|
}
|
|
|
|
func resetServiceFirewallClient(client lifecycle.Client, withDockerRestart bool, stop func(lifecycle.Client, bool, func() error) error) error {
|
|
resetter, ok := client.(lifecycle.Resetter)
|
|
if !ok {
|
|
return fmt.Errorf("firewall provider %s does not support reset", client.Name())
|
|
}
|
|
if resetBeforeStop, ok := client.(lifecycle.PreStopResetter); ok {
|
|
if err := stop(client, withDockerRestart, resetBeforeStop.ResetBeforeStop); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return resetter.Reset()
|
|
}
|
|
|
|
func firewallInventoryPositionRanges(provider filter.Provider, chain string, items []filter.InventoryItem) (ipv4, ipv6 filter.PositionRange) {
|
|
if provider == filter.ProviderFirewalld {
|
|
return filter.PositionRange{Min: -32768, Max: 32767}, filter.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 != filter.DirectionInput {
|
|
continue
|
|
}
|
|
position := *item.Observed.Locator.Position
|
|
if position < 1 {
|
|
continue
|
|
}
|
|
if (provider == filter.ProviderIptables || provider == filter.ProviderNftables) &&
|
|
(scope.Table != "filter" || scope.Chain != chain) {
|
|
continue
|
|
}
|
|
bounds := &ipv4
|
|
if scope.Family == filter.FamilyIPv6 {
|
|
bounds = &ipv6
|
|
} else if scope.Family != filter.FamilyIPv4 {
|
|
continue
|
|
}
|
|
if bounds.Min == 0 || position < bounds.Min {
|
|
bounds.Min = position
|
|
}
|
|
bounds.Max = max(bounds.Max, position)
|
|
if provider != filter.ProviderUFW {
|
|
bounds.Min = 1
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
|
|
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
|
|
return samePortWhitelistRule(rule, target)
|
|
})
|
|
if index < 0 {
|
|
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
|
|
}
|
|
return index, nil
|
|
}
|
|
|
|
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
|
|
if reflect.DeepEqual(left, right) {
|
|
return true
|
|
}
|
|
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
slices.Sort(normalizedLeft[0].Sources)
|
|
slices.Sort(normalizedRight[0].Sources)
|
|
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
|
|
}
|
|
|
|
func loadSSHWhitelistPortFrom(path string) (string, error) {
|
|
directives, _, err := parseSSHConfigTree(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return defaultSSHPort, nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return loadSSHPortValues(directives)[0], nil
|
|
}
|
|
|
|
func newFirewallService() *FirewallService {
|
|
return &FirewallService{
|
|
adapters: nil,
|
|
selectedProvider: firewallRuleSelectedProvider,
|
|
requiredPorts: LoadRequiredFirewallPortWhiteList,
|
|
cleanupBackend: cleanupSystemBackend,
|
|
cleanupInactiveBackend: cleanupInactiveSystemBackend,
|
|
resetBackend: resetServiceFirewallBackend,
|
|
dockerActive: firewallDockerActive,
|
|
restoreForwarding: func(ctx context.Context) error {
|
|
return newForwardingService().Restore(ctx)
|
|
},
|
|
restoreDockerGuard: RestoreDockerPortGuard,
|
|
baseClient: NewSelectedSystemFirewallClient,
|
|
}
|
|
}
|
|
|
|
func firewallRuleSelectedProvider(context.Context) (filter.Provider, error) {
|
|
client, err := NewSelectedSystemFirewallClient()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: %v", filter.ErrProviderUnavailable, err)
|
|
}
|
|
return filter.Provider(client.Name()), nil
|
|
}
|
|
|
|
func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) ([]dto.DockerPortGuardEndpoint, error) {
|
|
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: all})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoints := make([]dto.DockerPortGuardEndpoint, 0)
|
|
for _, item := range containers {
|
|
name := ""
|
|
if len(item.Names) > 0 {
|
|
name = strings.TrimPrefix(item.Names[0], "/")
|
|
}
|
|
compose := item.Labels[dockerGuardComposeProjectLabel]
|
|
application := ""
|
|
if created, ok := item.Labels[dockerGuardComposeCreatedBy]; ok && created == "Apps" {
|
|
application = compose
|
|
}
|
|
for _, port := range item.Ports {
|
|
if port.PublicPort == 0 || (port.Type != "tcp" && port.Type != "udp") {
|
|
continue
|
|
}
|
|
family := dockerfirewall.FamilyIPv4
|
|
hostIP := port.IP
|
|
if addr, err := netip.ParseAddr(hostIP); err == nil && addr.Is6() {
|
|
family = dockerfirewall.FamilyIPv6
|
|
} else if hostIP == "" {
|
|
hostIP = "0.0.0.0"
|
|
}
|
|
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerState: item.State, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
|
|
}
|
|
}
|
|
return endpoints, nil
|
|
}
|
|
|
|
func annotateDockerEndpointManagement(ctx context.Context, endpoints []dto.DockerPortGuardEndpoint, backend string) {
|
|
rules := map[string]dockerfirewall.DNATRules{
|
|
constant.FirewallFamilyIPv4: dockerfirewall.ReadDNATRules(ctx, backend, constant.FirewallFamilyIPv4),
|
|
constant.FirewallFamilyIPv6: dockerfirewall.ReadDNATRules(ctx, backend, constant.FirewallFamilyIPv6),
|
|
}
|
|
proxies := dockerfirewall.ReadProxyEndpoints(ctx)
|
|
inspections := make(map[string]dockerfirewall.EndpointInspection, len(rules))
|
|
for family, familyRules := range rules {
|
|
inspections[family] = dockerfirewall.InspectEndpoints(backend, family, familyRules, proxies)
|
|
}
|
|
for i := range endpoints {
|
|
endpoints[i].TrafficPath, endpoints[i].ManagementTarget, endpoints[i].ManagementReason =
|
|
dockerEndpointManagement(inspections[endpoints[i].Family], endpoints[i])
|
|
}
|
|
}
|
|
|
|
func dockerEndpointManagement(inspection dockerfirewall.EndpointInspection, endpoint dto.DockerPortGuardEndpoint) (string, string, string) {
|
|
if !inspection.DNATInspected {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATInspectFailed
|
|
}
|
|
dnatMatched := inspection.DNATMatches(endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)
|
|
if dnatMatched && inspection.IngressReachable {
|
|
return dockerTrafficPathForward, dockerManagementContainerGuard, ""
|
|
}
|
|
if !inspection.ProxyInspected {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonProxyInspectFailed
|
|
}
|
|
if inspection.ProxyMatches(endpoint.HostIP, endpoint.HostPort, endpoint.Protocol) {
|
|
return dockerTrafficPathInput, dockerManagementHostFirewall, ""
|
|
}
|
|
if dnatMatched {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATChainUnreachable
|
|
}
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNoMatchingPath
|
|
}
|
|
|
|
func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.DockerPortGuardContainer {
|
|
containers := make(map[string]*dto.DockerPortGuardContainer)
|
|
order := make([]string, 0)
|
|
for _, endpoint := range endpoints {
|
|
key := endpoint.ContainerID
|
|
if key == "" {
|
|
key = "__orphan__"
|
|
}
|
|
container, ok := containers[key]
|
|
if !ok {
|
|
container = &dto.DockerPortGuardContainer{
|
|
Key: key, Name: endpoint.ContainerName, Compose: endpoint.Compose,
|
|
Application: endpoint.Application, Endpoints: []dto.DockerPortGuardEndpoint{},
|
|
}
|
|
containers[key] = container
|
|
order = append(order, key)
|
|
}
|
|
container.Endpoints = append(container.Endpoints, endpoint)
|
|
}
|
|
sort.Slice(order, func(i, j int) bool {
|
|
return containers[order[i]].Name < containers[order[j]].Name
|
|
})
|
|
|
|
result := make([]dto.DockerPortGuardContainer, 0, len(order))
|
|
for _, key := range order {
|
|
container := containers[key]
|
|
items := make([]docker.PortRangeItem, 0, len(container.Endpoints))
|
|
for i, endpoint := range container.Endpoints {
|
|
sources := append([]string(nil), endpoint.Sources...)
|
|
sort.Strings(sources)
|
|
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.ManagementTarget, endpoint.ManagementReason)
|
|
items = append(items, docker.PortRangeItem{
|
|
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
|
|
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
|
|
HasPrivatePort: endpoint.ContainerPort != 0, Position: i,
|
|
})
|
|
}
|
|
container.PortGroups = make([]dto.DockerPortGuardPortGroup, 0, len(items))
|
|
for _, portRange := range docker.MergePortRanges(items) {
|
|
start := container.Endpoints[portRange.Start.Position]
|
|
address := start.HostIP
|
|
if strings.Contains(address, ":") {
|
|
address = "[" + address + "]"
|
|
}
|
|
ports := fmt.Sprintf("%d", portRange.Start.PublicPort)
|
|
if portRange.Start.PublicPort != portRange.End.PublicPort {
|
|
ports = fmt.Sprintf("%d-%d", portRange.Start.PublicPort, portRange.End.PublicPort)
|
|
}
|
|
container.PortGroups = append(container.PortGroups, dto.DockerPortGuardPortGroup{
|
|
Key: fmt.Sprintf("%s|%d-%d", portRange.Start.Key, portRange.Start.PublicPort, portRange.End.PublicPort),
|
|
Label: fmt.Sprintf("%s:%s/%s", address, ports, start.Protocol), Endpoint: start,
|
|
Endpoints: func() []dto.DockerPortGuardEndpoint {
|
|
members := make([]dto.DockerPortGuardEndpoint, 0, len(portRange.Items))
|
|
for _, item := range portRange.Items {
|
|
members = append(members, container.Endpoints[item.Position])
|
|
}
|
|
return members
|
|
}(),
|
|
})
|
|
}
|
|
result = append(result, *container)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeDockerFirewallUUIDs(values []string) ([]string, error) {
|
|
uuids := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, policyUUID := range values {
|
|
policyUUID = strings.TrimSpace(policyUUID)
|
|
if policyUUID == "" {
|
|
return nil, buserr.WithDetail("ErrInvalidParams", "policy UUID cannot be empty", nil)
|
|
}
|
|
if _, exists := seen[policyUUID]; exists {
|
|
continue
|
|
}
|
|
seen[policyUUID] = struct{}{}
|
|
uuids = append(uuids, policyUUID)
|
|
}
|
|
if len(uuids) == 0 {
|
|
return nil, buserr.WithDetail("ErrInvalidParams", "policy UUIDs cannot be empty", nil)
|
|
}
|
|
return uuids, nil
|
|
}
|
|
|
|
func queueFirewallRuleTask(subsystem, operation, taskID string, labels []string, apply func(*task.Task) error) (dto.FilterChainOperationResponse, error) {
|
|
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, taskID, 0)
|
|
if err != nil {
|
|
return dto.FilterChainOperationResponse{}, err
|
|
}
|
|
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
|
if labels != nil {
|
|
t.Logf("rules=%d", len(labels))
|
|
}
|
|
err := t.TaskCtx.Err()
|
|
if err == nil {
|
|
err = apply(t)
|
|
}
|
|
err = errors.Join(err, t.TaskCtx.Err())
|
|
if labels == nil {
|
|
return err
|
|
}
|
|
succeeded, failed, skipped := 0, 0, 0
|
|
for _, label := range labels {
|
|
if label == "" {
|
|
skipped++
|
|
continue
|
|
}
|
|
if err != nil {
|
|
failed++
|
|
t.LogFailedWithErr(label, err)
|
|
} else {
|
|
succeeded++
|
|
t.LogSuccess(label)
|
|
}
|
|
}
|
|
t.Log(i18n.GetMsgWithMap("FirewallCreateRulesResult", map[string]interface{}{
|
|
"succeeded": succeeded, "failed": failed, "skipped": skipped,
|
|
}))
|
|
return err
|
|
}, nil, 0, 0)
|
|
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
|
taskItem.LogFailedWithErr(taskItem.Name, err)
|
|
closeUnstartedFirewallTask(taskItem)
|
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
|
|
}
|
|
go func() { _ = taskItem.Execute() }()
|
|
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
|
}
|
|
|
|
func normalizeDockerFirewallPolicy(policy dockerfirewall.Policy) (dockerfirewall.Policy, error) {
|
|
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family))
|
|
policy.HostIP = strings.TrimSpace(policy.HostIP)
|
|
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol))
|
|
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode))
|
|
if policy.HostPort == 0 ||
|
|
(policy.Protocol != "tcp" && policy.Protocol != "udp") ||
|
|
(policy.Family != dockerfirewall.FamilyIPv4 && policy.Family != dockerfirewall.FamilyIPv6) ||
|
|
(policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeSources && policy.Mode != dockerfirewall.ModeAllow && policy.Mode != dockerfirewall.ModeAcceptSources && policy.Mode != dockerfirewall.ModeAcceptAll) {
|
|
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "invalid policy fields", nil)
|
|
}
|
|
address, err := netip.ParseAddr(policy.HostIP)
|
|
if err != nil || (policy.Family == dockerfirewall.FamilyIPv4) != address.Is4() {
|
|
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "host IP does not match address family", nil)
|
|
}
|
|
policy.HostIP = address.String()
|
|
normalizedSources := make([]string, 0, len(policy.Sources))
|
|
seen := make(map[string]struct{}, len(policy.Sources))
|
|
for _, source := range policy.Sources {
|
|
source = strings.TrimSpace(source)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
prefix, err := netip.ParsePrefix(source)
|
|
if err != nil {
|
|
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil {
|
|
bits := 128
|
|
if sourceAddress.Is4() {
|
|
bits = 32
|
|
}
|
|
prefix = netip.PrefixFrom(sourceAddress, bits)
|
|
} else {
|
|
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("invalid source address %q", source), nil)
|
|
}
|
|
}
|
|
if (policy.Family == dockerfirewall.FamilyIPv4) != prefix.Addr().Is4() {
|
|
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("source %q does not match address family", source), nil)
|
|
}
|
|
canonical := prefix.Masked().String()
|
|
if _, exists := seen[canonical]; !exists {
|
|
seen[canonical] = struct{}{}
|
|
normalizedSources = append(normalizedSources, canonical)
|
|
}
|
|
}
|
|
if policy.Mode != dockerfirewall.ModeAll && policy.Mode != dockerfirewall.ModeAcceptAll && len(normalizedSources) == 0 {
|
|
return dockerfirewall.Policy{}, buserr.WithDetail("ErrInvalidParams", "source-based modes require at least one source", nil)
|
|
}
|
|
if policy.Mode == dockerfirewall.ModeAll || policy.Mode == dockerfirewall.ModeAcceptAll {
|
|
normalizedSources = []string{}
|
|
}
|
|
sort.Strings(normalizedSources)
|
|
policy.Sources = normalizedSources
|
|
return policy, nil
|
|
}
|
|
|
|
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool {
|
|
if len(operations) == 0 {
|
|
return false
|
|
}
|
|
for _, operation := range operations {
|
|
if operation.Operation != string(forwarding.OperationRemove) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (e *firewallDockerRestartError) Error() string {
|
|
return fmt.Sprintf("failed to restart Docker: %v", e.Err)
|
|
}
|
|
|
|
func (e *firewallDockerRestartError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
func (e *firewallCompletedOperationError) Error() string {
|
|
return fmt.Sprintf("firewall %s completed with recovery errors: %v", e.Operation, e.Err)
|
|
}
|
|
|
|
func (e *firewallCompletedOperationError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
|
entries = slices.Clone(entries)
|
|
var sshPort string
|
|
for i := range entries {
|
|
entry := &entries[i]
|
|
if entry.Type == "" || entry.Port != "" {
|
|
continue
|
|
}
|
|
switch entry.Type {
|
|
case firewall.PortWhitelistTypePanel:
|
|
entry.Port = LoadPanelPort()
|
|
case firewall.PortWhitelistTypeSSH:
|
|
if sshPort == "" {
|
|
var err error
|
|
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
entry.Port = sshPort
|
|
}
|
|
}
|
|
return firewall.ValidatePortWhitelist(entries)
|
|
}
|
|
func (s *FirewallService) readFirewallInventory(ctx context.Context, provider filter.Provider, scopes []filter.Scope) (dto.FirewallRuleInventoryResponse, error) {
|
|
response := dto.FirewallRuleInventoryResponse{Items: make([]filter.InventoryItem, 0)}
|
|
runtime, err := s.firewallAdapter(provider)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
for _, scope := range scopes {
|
|
if scope.Provider != provider {
|
|
return response, filter.ErrInvalidScope
|
|
}
|
|
}
|
|
for _, group := range firewallScopeReadGroups(scopes) {
|
|
snapshots, err := listFirewallRuleScopes(runtime, ctx, group)
|
|
if errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
response.Notices = append(response.Notices, filter.ScopeNotice{Code: filter.ScopeNoticeFamilyUnavailable, Values: []string{string(group[0].Family), err.Error()}})
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
for _, snapshot := range snapshots {
|
|
response.Notices = append(response.Notices, snapshot.Notices...)
|
|
for _, observed := range snapshot.Rules {
|
|
observed.InstanceKey, err = filter.InstanceKey(observed)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
response.Items = append(response.Items, filter.InventoryItem{Rule: observed.Rule, Observed: &observed})
|
|
}
|
|
}
|
|
}
|
|
if provider == filter.ProviderUFW {
|
|
sort.SliceStable(response.Items, func(i, j int) bool {
|
|
left, right := response.Items[i].Observed.Locator.Position, response.Items[j].Observed.Locator.Position
|
|
return left != nil && right != nil && *left < *right
|
|
})
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func saveFirewallDescription(ctx context.Context, observed filter.ObservedRule, description string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
id, err := filter.DescriptionID(observed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
description = strings.TrimSpace(description)
|
|
if description == "" {
|
|
return settingRepo.DelDescription(id)
|
|
}
|
|
existing, err := settingRepo.GetDescription(settingRepo.WithByDescriptionID(id), repo.WithByType("firewall"))
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return settingRepo.CreateDescription(&model.CommonDescription{ID: id, Type: "firewall", Description: description})
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.UpdateDescription(existing.ID, map[string]interface{}{"description": description})
|
|
}
|
|
|
|
func (s *FirewallService) updateObservedRule(ctx context.Context, request dto.FirewallRuleUpdate) error {
|
|
metadata := request.Description != nil || request.OrderIndex != nil || request.Priority != nil
|
|
if (request.Rule != nil) == metadata || request.OrderIndex != nil && request.Priority != nil {
|
|
return filter.ErrInvalidRule
|
|
}
|
|
runtime, err := s.resolveRuntime(ctx, request.Scope.Provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot, err := readMutableFirewallRules(runtime, ctx, request.Scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
observed, err := filter.FindCandidate(snapshot.Rules, request.InstanceKey)
|
|
if err != nil {
|
|
return filter.ErrRuleStale
|
|
}
|
|
if err := filter.GuardMutation(observed); err != nil {
|
|
return err
|
|
}
|
|
if request.Rule != nil {
|
|
ports, err := loadFirewallPortWhiteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if filter.NewPortWhitelistIndex(ports).Matches(observed.Rule) {
|
|
return filter.ErrProtectedRule
|
|
}
|
|
}
|
|
if request.Rule == nil && request.OrderIndex == nil && request.Priority == nil {
|
|
return saveFirewallDescription(ctx, observed, *request.Description)
|
|
}
|
|
if observed.ParseStatus != filter.ParseStatusSupported {
|
|
return filter.ErrUnsupportedScope
|
|
}
|
|
before := observed.Rule
|
|
before.UUID = uuid.NewString()
|
|
after := before
|
|
operation := filter.ChangeReorder
|
|
if runtime.Provider() == filter.ProviderFirewalld {
|
|
operation = filter.ChangeUpdate
|
|
}
|
|
if request.Rule != nil {
|
|
after, err = prepareFirewallBackendRule(ctx, runtime, *request.Rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
operation = filter.ChangeUpdate
|
|
} else {
|
|
after.OrderIndex, after.Priority = request.OrderIndex, before.Priority
|
|
if request.Priority != nil {
|
|
after.Priority = request.Priority
|
|
}
|
|
}
|
|
if after.Scope.Normalize().Key() != before.Scope.Key() {
|
|
return filter.ErrUnsupportedScope
|
|
}
|
|
after.UUID = before.UUID
|
|
after, err = filter.NormalizeRule(after)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
beforeKey, err := firewallRuleDuplicateKey(observed, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
afterKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: after, ParseStatus: filter.ParseStatusSupported}, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if beforeKey == afterKey {
|
|
if after.OrderIndex == nil || observed.Locator.Position != nil && *after.OrderIndex == int64(*observed.Locator.Position) {
|
|
if request.Rule != nil {
|
|
return saveFirewallDescription(ctx, observed, request.Rule.Description)
|
|
}
|
|
if request.Description != nil {
|
|
return saveFirewallDescription(ctx, observed, *request.Description)
|
|
}
|
|
return nil
|
|
}
|
|
} else {
|
|
var matchKey string
|
|
if runtime.Provider() == filter.ProviderUFW {
|
|
matchKey, err = filter.RuleMatchKey(after)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, candidate := range snapshot.Rules {
|
|
instanceKey, err := filter.InstanceKey(candidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if instanceKey == request.InstanceKey {
|
|
continue
|
|
}
|
|
key, err := firewallRuleDuplicateKey(candidate, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if key == afterKey {
|
|
return buserr.New("ErrRecordExist")
|
|
}
|
|
if runtime.Provider() == filter.ProviderUFW && candidate.ParseStatus == filter.ParseStatusSupported {
|
|
candidateMatch, err := filter.RuleMatchKey(candidate.Rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if candidateMatch == matchKey && candidate.Rule.Action != after.Action {
|
|
return buserr.New("ErrFirewallRuleConflict")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
description := before.Description
|
|
id, err := filter.DescriptionID(observed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stored, err := settingRepo.GetDescription(settingRepo.WithByDescriptionID(id), repo.WithByType("firewall"))
|
|
if err == nil {
|
|
description = stored.Description
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if request.Rule != nil {
|
|
description = request.Rule.Description
|
|
}
|
|
if request.Description != nil {
|
|
description = *request.Description
|
|
}
|
|
plan, err := applyFirewallChanges(runtime, ctx, snapshot, []filter.RuleChange{{
|
|
Operation: operation, Before: &before, After: &after, Locator: &observed.Locator, PreviousMarker: observed.Marker,
|
|
}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveFirewallDescription(ctx, plan.Rules[0].Expected, description)
|
|
}
|
|
|
|
func firewallBackupDirectory(subsystem string) (string, error) {
|
|
var directory string
|
|
switch subsystem {
|
|
case "system":
|
|
directory = "host"
|
|
case "docker":
|
|
directory = "docker"
|
|
case "forwarding":
|
|
directory = "forward"
|
|
default:
|
|
return "", filter.ErrInvalidRule
|
|
}
|
|
return filepath.Join(global.Dir.BaseDir, "1panel", "backup", "firewall", directory), nil
|
|
}
|
|
|
|
func readFirewallBackup(name string) ([]dto.FirewallRuleExportItem, error) {
|
|
if name == "" || name != filepath.Base(name) || filepath.Ext(name) != ".json" {
|
|
return nil, filter.ErrInvalidRule
|
|
}
|
|
directory, err := firewallBackupDirectory("system")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := os.OpenInRoot(directory, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, filter.ErrInvalidRule
|
|
}
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var items []dto.FirewallRuleExportItem
|
|
if err := json.Unmarshal(data, &items); err != nil {
|
|
return nil, err
|
|
}
|
|
if items == nil {
|
|
return nil, filter.ErrInvalidRule
|
|
}
|
|
for _, item := range items {
|
|
if err := item.Scope.ValidateMVP(); err != nil {
|
|
return nil, err
|
|
}
|
|
switch item.ParseStatus {
|
|
case "", filter.ParseStatusSupported:
|
|
if _, err := filter.NormalizeRule(item.FirewallRule); err != nil {
|
|
return nil, err
|
|
}
|
|
case filter.ParseStatusPartial, filter.ParseStatusOpaque:
|
|
if item.Raw == "" {
|
|
return nil, filter.ErrInvalidRule
|
|
}
|
|
default:
|
|
return nil, filter.ErrInvalidRule
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func writeFirewallJSON(directory, name string, value any) (string, error) {
|
|
data, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
return "", err
|
|
}
|
|
file, err := os.CreateTemp(directory, ".firewall-*.tmp")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
temporary := file.Name()
|
|
defer os.Remove(temporary)
|
|
_, writeErr := file.Write(data)
|
|
syncErr := file.Sync()
|
|
closeErr := file.Close()
|
|
if err := errors.Join(writeErr, syncErr, closeErr); err != nil {
|
|
return "", err
|
|
}
|
|
target := filepath.Join(directory, name)
|
|
if err := os.Rename(temporary, target); err != nil {
|
|
return "", err
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func readFirewallSubsystemBackup(name, subsystem string) (dto.FirewallSubsystemBackup, error) {
|
|
var backup dto.FirewallSubsystemBackup
|
|
if name == "" || name != filepath.Base(name) || (filepath.Ext(name) != ".json" && name != subsystem+"-iptables.rules" && name != subsystem+"-nftables.rules") {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
directory, err := firewallBackupDirectory(subsystem)
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
if filepath.Ext(name) == ".rules" {
|
|
directory = global.Dir.FirewallDir
|
|
}
|
|
file, err := os.OpenInRoot(directory, name)
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
if err := json.Unmarshal(data, &backup); err != nil {
|
|
return backup, err
|
|
}
|
|
if backup.Subsystem != subsystem || (backup.Provider != filter.ProviderIptables && backup.Provider != filter.ProviderNftables) {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
for _, family := range backup.Families {
|
|
if family != forwarding.FamilyIPv4 && family != forwarding.FamilyIPv6 {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
}
|
|
switch subsystem {
|
|
case "forwarding":
|
|
if backup.Forwarding == nil || backup.Docker != nil {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
for i, rule := range backup.Forwarding {
|
|
normalized, err := forwarding.NormalizeRule(rule)
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
backup.Forwarding[i] = normalized
|
|
}
|
|
case "docker":
|
|
if backup.Docker == nil || len(backup.Forwarding) > 0 {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
seen := make(map[string]bool)
|
|
for i, policy := range backup.Docker.Policies {
|
|
if _, err := uuid.Parse(policy.UUID); err != nil {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
normalized := policy
|
|
if len(policy.NativeRules) == 0 {
|
|
var err error
|
|
normalized, err = normalizeDockerFirewallPolicy(policy)
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
}
|
|
if seen[normalized.UUID] {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
seen[normalized.UUID] = true
|
|
backup.Docker.Policies[i] = normalized
|
|
}
|
|
for _, policy := range backup.Docker.Policies {
|
|
for _, rule := range policy.NativeRules {
|
|
if rule.Family != dockerfirewall.FamilyIPv4 && rule.Family != dockerfirewall.FamilyIPv6 {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
if len(rule.Tokens) == 0 {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
for _, token := range rule.Tokens {
|
|
if strings.ContainsAny(token, "\r\n\x00") {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
}
|
|
if backup.Provider == filter.ProviderIptables && (len(rule.Tokens) < 3 || rule.Tokens[0] != "-A" || rule.Tokens[1] != dockerfirewall.Chain) {
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
return backup, filter.ErrInvalidRule
|
|
}
|
|
return backup, nil
|
|
}
|
|
|
|
func forwardingBackup(manager forwarding.Adapter, rules []forwarding.Rule) (dto.FirewallSubsystemBackup, error) {
|
|
if rules == nil {
|
|
rules = []forwarding.Rule{}
|
|
}
|
|
backup := dto.FirewallSubsystemBackup{Subsystem: "forwarding", Provider: filter.Provider(manager.Name()), Forwarding: rules}
|
|
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
|
|
initialized, _, err := manager.FamilyStatus(family)
|
|
if family == forwarding.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return backup, err
|
|
}
|
|
if initialized {
|
|
backup.Families = append(backup.Families, family)
|
|
}
|
|
}
|
|
return backup, nil
|
|
}
|
|
|
|
func persistForwardingRules(manager forwarding.Adapter, rules []forwarding.Rule) error {
|
|
backup, err := forwardingBackup(manager, rules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = writeFirewallJSON(global.Dir.FirewallDir, "forwarding-"+manager.Name()+".rules", backup)
|
|
return err
|
|
}
|
|
|
|
func persistDockerRules(backend string, inventory dockerfirewall.PolicyInventory) error {
|
|
_, err := writeFirewallJSON(global.Dir.FirewallDir, "docker-"+backend+".rules", dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory})
|
|
return err
|
|
}
|
|
|
|
func dockerPolicyEndpointKey(policy dockerfirewall.Policy) string {
|
|
return strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
|
|
}
|
|
|
|
func dockerGuardInventoryEndpoints(inventory dockerfirewall.PolicyInventory) []dto.DockerPortGuardEndpoint {
|
|
result := make([]dto.DockerPortGuardEndpoint, 0, len(inventory.Policies))
|
|
for _, policy := range inventory.Policies {
|
|
if _, err := normalizeDockerFirewallPolicy(policy); err != nil {
|
|
policy.Mode = ""
|
|
}
|
|
result = append(result, dto.DockerPortGuardEndpoint{Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: policy.Sources, TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func applyDockerPolicies(ctx context.Context, runtime dockerfirewall.Runtime, backend string, inventory dockerfirewall.PolicyInventory, desired []dockerfirewall.Policy) error {
|
|
families := make(map[string]bool)
|
|
for _, policy := range desired {
|
|
families[policy.Family] = true
|
|
}
|
|
for family := range families {
|
|
initialized, err := runtime.Initialized(family)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !initialized {
|
|
return filter.ErrProviderUnavailable
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.ReplacePolicies(desired, inventory); err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
inventory.Policies = desired
|
|
return persistDockerRules(backend, inventory)
|
|
}
|
|
|
|
func (s *DockerPortGuardService) initialize(ctx context.Context, request dto.DockerPortGuardOperation, t *task.Task) error {
|
|
runtime, backend, err := s.runtimeForDocker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inventory, err := runtime.ListPolicies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if request.BackupFile != "" {
|
|
backup, err := readFirewallSubsystemBackup(request.BackupFile, "docker")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inventory, err = mergeDockerBackup(inventory, backup, backend, t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
if t != nil {
|
|
t.Logf("IPv6 firewall support is disabled; skipping IPv6 Docker chain initialization")
|
|
} else {
|
|
global.LOG.Info("IPv6 firewall support is disabled; skipping IPv6 Docker chain initialization")
|
|
}
|
|
}
|
|
if err := runtime.Initialize(inventory.Policies, inventory, families...); err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := persistDockerRules(backend, inventory); err != nil {
|
|
return err
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
|
return err
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mergeDockerBackup(current dockerfirewall.PolicyInventory, backup dto.FirewallSubsystemBackup, backend string, t *task.Task) (dockerfirewall.PolicyInventory, error) {
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return current, err
|
|
}
|
|
skippedIPv6 := 0
|
|
defer func() { logFirewallIPv6Skipped(t, "Docker", skippedIPv6) }()
|
|
empty := len(current.Policies) == 0
|
|
byEndpoint := make(map[string]bool, len(current.Policies))
|
|
byUUID := make(map[string]bool, len(current.Policies))
|
|
for _, policy := range current.Policies {
|
|
byEndpoint[dockerPolicyEndpointKey(policy)] = true
|
|
byUUID[policy.UUID] = true
|
|
}
|
|
if current.RuleOrders == nil {
|
|
current.RuleOrders = make(map[string][]int64)
|
|
}
|
|
var maxOrder int64
|
|
for _, existing := range current.Policies {
|
|
for _, rule := range existing.NativeRules {
|
|
maxOrder = max(maxOrder, rule.Order)
|
|
}
|
|
for _, order := range current.RuleOrders[existing.Family+"\x00"+existing.UUID] {
|
|
maxOrder = max(maxOrder, order)
|
|
}
|
|
}
|
|
for _, policy := range backup.Docker.Policies {
|
|
if policy.Family == dockerfirewall.FamilyIPv6 && !slices.Contains(families, dockerfirewall.FamilyIPv6) {
|
|
skippedIPv6++
|
|
continue
|
|
}
|
|
if byEndpoint[dockerPolicyEndpointKey(policy)] {
|
|
if t != nil {
|
|
t.Logf("%s %s %s:%d %s: %v", policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), buserr.New("ErrRecordExist"))
|
|
}
|
|
continue
|
|
}
|
|
if byUUID[policy.UUID] {
|
|
return current, filter.ErrRuleStale
|
|
}
|
|
if string(backup.Provider) != backend {
|
|
policy, err = normalizeDockerFirewallPolicy(policy)
|
|
if err != nil {
|
|
return current, err
|
|
}
|
|
policy, err = dockerfirewall.ConvertPolicyBackend(policy, string(backup.Provider), backend)
|
|
if err != nil {
|
|
return current, err
|
|
}
|
|
}
|
|
if !empty {
|
|
policy.NativeRules = append([]dockerfirewall.NativeRule(nil), policy.NativeRules...)
|
|
for i := range policy.NativeRules {
|
|
maxOrder++
|
|
policy.NativeRules[i].Order = maxOrder
|
|
}
|
|
}
|
|
current.Policies = append(current.Policies, policy)
|
|
byEndpoint[dockerPolicyEndpointKey(policy)], byUUID[policy.UUID] = true, true
|
|
key := policy.Family + "\x00" + policy.UUID
|
|
if empty {
|
|
current.RuleOrders[key] = backup.Docker.RuleOrders[key]
|
|
}
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func RestoreDockerPortGuard(ctx context.Context) error {
|
|
return newDockerPortGuardService().Restore(ctx)
|
|
}
|
|
|
|
func RestoreDockerPortGuardBestEffort(ctx context.Context) {
|
|
if err := RestoreDockerPortGuard(ctx); err != nil {
|
|
global.LOG.Warnf("restore Docker port guard failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func loadFirewallFamilies() ([]string, error) {
|
|
value, err := settingRepo.GetValueByKey(constant.FirewallIPv6SupportKey)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
if value == constant.StatusDisable {
|
|
return []string{constant.FirewallFamilyIPv4}, nil
|
|
}
|
|
return []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6}, nil
|
|
}
|
|
|
|
func ipv4PortWhitelist(rules []firewall.PortWhitelist) []firewall.PortWhitelist {
|
|
result := make([]firewall.PortWhitelist, 0, len(rules))
|
|
for _, rule := range rules {
|
|
sources := make([]string, 0, len(rule.Sources))
|
|
for _, source := range rule.Sources {
|
|
if !strings.Contains(source, ":") {
|
|
sources = append(sources, source)
|
|
}
|
|
}
|
|
if len(sources) == 0 {
|
|
continue
|
|
}
|
|
rule.Sources = sources
|
|
result = append(result, rule)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func validateFirewallWhitelistFamilies(rules []firewall.PortWhitelist) error {
|
|
families, err := loadFirewallFamilies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
|
for _, rule := range rules {
|
|
for _, source := range rule.Sources {
|
|
if strings.Contains(source, ":") {
|
|
return fmt.Errorf("IPv6 firewall support is disabled")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func logFirewallIPv6Skipped(t *task.Task, subsystem string, count int) {
|
|
if count == 0 {
|
|
return
|
|
}
|
|
message := fmt.Sprintf("IPv6 firewall support is disabled; skipped %d IPv6 %s rule(s)", count, subsystem)
|
|
if t != nil {
|
|
t.Log(message)
|
|
} else {
|
|
global.LOG.Info(message)
|
|
}
|
|
}
|