fix(firewall): recover rules after upgrade (#13680)

This commit is contained in:
ssongliu
2026-09-01 14:53:01 +08:00
committed by GitHub
parent deddd392ba
commit fb377d2e99
29 changed files with 670 additions and 142 deletions
+17 -15
View File
@@ -159,21 +159,23 @@ type DockerPortGuardFamilyStatus struct {
}
type DockerPortGuardEndpoint struct {
Family string `json:"family"`
HostIP string `json:"hostIP"`
HostPort uint16 `json:"hostPort"`
Protocol string `json:"protocol"`
ContainerID string `json:"containerID"`
ContainerName string `json:"containerName"`
ContainerPort uint16 `json:"containerPort"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
Family string `json:"family"`
HostIP string `json:"hostIP"`
HostPort uint16 `json:"hostPort"`
Protocol string `json:"protocol"`
ContainerID string `json:"containerID"`
ContainerName string `json:"containerName"`
ContainerPort uint16 `json:"containerPort"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"`
}
type DockerPortGuardPortGroup struct {
+75 -29
View File
@@ -33,11 +33,18 @@ import (
)
const (
dockerGuardComposeProjectLabel = "com.docker.compose.project"
dockerGuardComposeCreatedBy = "createdBy"
dockerTrafficPathForward = "forward"
dockerTrafficPathInput = "input"
dockerTrafficPathUnknown = "unknown"
dockerGuardComposeProjectLabel = "com.docker.compose.project"
dockerGuardComposeCreatedBy = "createdBy"
dockerTrafficPathForward = "forward"
dockerTrafficPathInput = "input"
dockerTrafficPathUnknown = "unknown"
dockerManagementContainerGuard = "container_guard"
dockerManagementHostFirewall = "host_firewall"
dockerManagementNeedsDiagnosis = "needs_diagnosis"
dockerReasonNATInspectFailed = "nat_inspect_failed"
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
dockerReasonNoMatchingPath = "no_matching_path"
)
type dockerProxyEndpoint struct {
@@ -51,6 +58,11 @@ type dockerForwardRules struct {
inspected bool
}
type dockerProxyEndpoints struct {
items []dockerProxyEndpoint
inspected bool
}
type dockerGuardRuntime = docker_guard.Runtime
type DockerPortGuardService struct {
@@ -121,7 +133,7 @@ func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.
if info, infoErr := cli.Info(ctx); infoErr == nil {
backend = dockerFirewallBackend(info)
}
annotateDockerEndpointTrafficPaths(endpoints, backend)
annotateDockerEndpointManagement(endpoints, backend)
return groupDockerGuardContainers(endpoints), nil
}
@@ -155,7 +167,7 @@ func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPo
if err != nil {
return dto.DockerPortGuardList{}, err
}
annotateDockerEndpointTrafficPaths(endpoints, detectedBackend)
annotateDockerEndpointManagement(endpoints, detectedBackend)
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
sort.Slice(endpoints, func(i, j int) bool {
return guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < guardEndpointKey(endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
@@ -183,7 +195,7 @@ func matchDockerGuardPolicies(
}
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, docker_guard.DecodeSources(policy.Sources)
endpoints[i].Description = policy.Description
endpoints[i].Effective = endpoints[i].TrafficPath == dockerTrafficPathForward &&
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
((policy.Family == docker_guard.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == docker_guard.FamilyIPv6 && base.IPv6.Effective))
delete(byEndpoint, key)
}
@@ -192,7 +204,8 @@ func matchDockerGuardPolicies(
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
})
}
return endpoints, orphanPolicies
@@ -369,15 +382,19 @@ func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(
if err != nil {
return nil
}
annotateDockerEndpointTrafficPaths(endpoints, dockerFirewallBackend(info))
paths := make(map[string]string, len(endpoints))
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
targets := make(map[string]string, len(endpoints))
for _, endpoint := range endpoints {
paths[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.TrafficPath
targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
}
for _, endpoint := range requested {
if paths[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] == dockerTrafficPathInput {
target := targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
if target == dockerManagementHostFirewall {
return fmt.Errorf("%w: endpoint traffic is handled by the host input firewall", ErrDockerGuardInvalid)
}
if target == dockerManagementNeedsDiagnosis {
return fmt.Errorf("%w: endpoint traffic management target requires diagnosis", ErrDockerGuardInvalid)
}
}
return nil
}
@@ -456,7 +473,8 @@ func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortG
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
TrafficPath: dockerTrafficPathUnknown,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}
}
@@ -464,7 +482,8 @@ func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGu
return &dto.DockerPortGuardEndpoint{
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...),
TrafficPath: dockerTrafficPathUnknown,
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
ManagementReason: dockerReasonNoMatchingPath,
}
}
@@ -696,6 +715,7 @@ func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.Do
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
})
}
return endpoints
@@ -731,7 +751,7 @@ func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.D
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.Description, endpoint.TrafficPath)
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description, endpoint.ManagementTarget, endpoint.ManagementReason)
items = append(items, docker.PortRangeItem{
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
@@ -777,7 +797,7 @@ func firstGuardString(values []string) string {
return values[0]
}
func annotateDockerEndpointTrafficPaths(endpoints []dto.DockerPortGuardEndpoint, backend string) {
func annotateDockerEndpointManagement(endpoints []dto.DockerPortGuardEndpoint, backend string) {
rules := map[string]dockerForwardRules{
constant.FirewallFamilyIPv4: loadDockerDNATRules(backend, constant.FirewallFamilyIPv4),
constant.FirewallFamilyIPv6: loadDockerDNATRules(backend, constant.FirewallFamilyIPv6),
@@ -785,21 +805,34 @@ func annotateDockerEndpointTrafficPaths(endpoints []dto.DockerPortGuardEndpoint,
proxies := loadDockerProxyEndpoints()
for i := range endpoints {
familyRules := rules[endpoints[i].Family]
endpoints[i].TrafficPath = dockerEndpointTrafficPath(backend, familyRules.output, familyRules.inspected, proxies, endpoints[i])
endpoints[i].TrafficPath, endpoints[i].ManagementTarget, endpoints[i].ManagementReason =
dockerEndpointManagement(backend, familyRules, proxies, endpoints[i])
}
}
func dockerEndpointTrafficPath(backend, rules string, inspected bool, proxies []dockerProxyEndpoint, endpoint dto.DockerPortGuardEndpoint) string {
if !inspected {
return dockerTrafficPathUnknown
func dockerEndpointManagement(
backend string,
rules dockerForwardRules,
proxies dockerProxyEndpoints,
endpoint dto.DockerPortGuardEndpoint,
) (string, string, string) {
if !rules.inspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATInspectFailed
}
if dockerDNATRuleMatches(backend, rules, endpoint) {
return dockerTrafficPathForward
dnatMatched := dockerDNATRuleMatches(backend, rules.output, endpoint)
if dnatMatched && dockerDNATIngressReachable(backend, rules.output) {
return dockerTrafficPathForward, dockerManagementContainerGuard, ""
}
if dockerProxyEndpointMatches(proxies, endpoint) {
return dockerTrafficPathInput
if !proxies.inspected {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonProxyInspectFailed
}
return dockerTrafficPathUnknown
if dockerProxyEndpointMatches(proxies.items, endpoint) {
return dockerTrafficPathInput, dockerManagementHostFirewall, ""
}
if dnatMatched {
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATChainUnreachable
}
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNoMatchingPath
}
func loadDockerDNATRules(backend, family string) dockerForwardRules {
@@ -834,13 +867,13 @@ func loadDockerDNATRules(backend, family string) dockerForwardRules {
return dockerForwardRules{output: output, inspected: err == nil}
}
func loadDockerProxyEndpoints() []dockerProxyEndpoint {
func loadDockerProxyEndpoints() dockerProxyEndpoints {
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
if err != nil {
return nil
return dockerProxyEndpoints{}
}
return parseDockerProxyEndpoints(output)
return dockerProxyEndpoints{items: parseDockerProxyEndpoints(output), inspected: true}
}
func parseDockerProxyEndpoints(output string) []dockerProxyEndpoint {
@@ -902,6 +935,19 @@ func dockerDNATRuleMatches(backend, output string, endpoint dto.DockerPortGuardE
return iptablesDNATRuleMatches(output, endpoint)
}
func dockerDNATIngressReachable(backend, output string) bool {
if backend == constant.FirewallProviderNftables {
return strings.Contains(output, "hook prerouting")
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == "-A" && fields[1] == "PREROUTING" && commandFlagValue(fields, "-j") == "DOCKER" {
return true
}
}
return false
}
func iptablesDNATRuleMatches(output string, endpoint dto.DockerPortGuardEndpoint) bool {
port := strconv.Itoa(int(endpoint.HostPort))
for _, line := range strings.Split(output, "\n") {
+86
View File
@@ -19,6 +19,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
"gorm.io/gorm"
)
@@ -140,6 +141,91 @@ func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provid
return fmt.Errorf("restore database firewall rules: %s", strings.Join(messages, "; "))
}
func AdoptLegacyHostFirewallRuleOwnership(ctx context.Context) error {
return newFirewallService().adoptLegacyHostFirewallRuleOwnership(ctx)
}
func (s *FirewallService) adoptLegacyHostFirewallRuleOwnership(ctx context.Context) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
selected, err := s.selectedProvider(ctx)
if err != nil {
return err
}
if selected != filter.ProviderIptables && selected != filter.ProviderUFW {
return fmt.Errorf("%w: selected provider %s does not require legacy ownership transfer", filter.ErrProviderUnavailable, selected)
}
runtime, err := s.adapters.Resolve(selected)
if err != nil {
return err
}
stored, err := s.rules.List(ctx)
if err != nil {
return err
}
for _, scope := range filter.ManagedInputScopes(selected) {
desired, err := s.desiredFirewallRulesForScope(ctx, stored, scope)
if err != nil {
return err
}
if len(desired) == 0 {
continue
}
snapshot, err := runtime.ObserveMutation(ctx, scope)
if err != nil {
return err
}
for {
items, err := filter.MergeInventory(filter.InventoryMergeInput{
Observed: snapshot.Rules,
Desired: desired,
})
if err != nil {
return err
}
var candidate *filter.InventoryItem
for index := range items {
item := &items[index]
if item.Match != filter.InventoryMatchChanged || item.Desired == nil || item.Observed == nil ||
item.Desired.Origin != filter.RuleOriginAdopted || strings.TrimSpace(item.Desired.Marker) == "" ||
strings.TrimSpace(item.Observed.Marker) != "" || item.Observed.Protected ||
!filter.ObservedRuleMatchesExpected(*item.Observed, item.Desired.Rule) {
continue
}
candidate = item
break
}
if candidate == nil {
break
}
after := candidate.Desired.Rule
before := firewallsync.ObservedRule(*candidate.Observed)
locator := candidate.Observed.Locator
_, verification, err := runtime.Execute(ctx, snapshot, []filter.DesiredChange{{
Operation: filter.ChangeAdopt,
Before: &before,
After: &after,
Locator: &locator,
PreviousMarker: candidate.Observed.Marker,
}})
if err != nil {
return err
}
if !verification.Matched {
return filter.ErrVerificationFailed
}
snapshot = verification.Snapshot
}
}
if selected == filter.ProviderIptables {
if err := iptables_helper.CleanupLegacyAdvancedChains(ctx); err != nil {
return err
}
}
return nil
}
func (s *FirewallService) loadFirewallRuleSyncPlan(
ctx context.Context,
clientIP string,
+3
View File
@@ -30,6 +30,9 @@ func Init() {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
return
}
if err := migrationutils.TransferLegacyHostFirewallRuleOwnership(ctx, clientName, service.AdoptLegacyHostFirewallRuleOwnership); err != nil {
global.LOG.Warnf("transfer legacy host firewall rule ownership failed, err: %v", err)
}
if err := migrationutils.TransferFirewallForwarding(ctx); err != nil {
global.LOG.Errorf("transfer legacy forwarding rules failed, err: %v", err)
return
+12 -2
View File
@@ -1705,12 +1705,22 @@ var NormalizeFirewallBackendSelections = &gormigrate.Migration{
ID: "20260826-normalize-firewall-backend-selections",
Migrate: func(tx *gorm.DB) error {
return tx.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Setting{}).
Where(
"key = ? AND value NOT IN ?",
constant.FirewallDockerBackendKey,
[]string{constant.FirewallProviderIptables, constant.FirewallProviderNftables},
).
Update("value", constant.FirewallProviderIptables)
if result.Error != nil {
return result.Error
}
if err := tx.Where("key = ?", constant.FirewallDockerBackendKey).FirstOrCreate(&model.Setting{
Key: constant.FirewallDockerBackendKey, Value: "",
Key: constant.FirewallDockerBackendKey, Value: constant.FirewallProviderIptables,
}).Error; err != nil {
return err
}
result := tx.Model(&model.Setting{}).
result = tx.Model(&model.Setting{}).
Where(
"key = ? AND value NOT IN ?",
constant.FirewallForwardingBackendKey,
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"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"
@@ -311,5 +312,27 @@ func cleanupLegacyFirewalldForwarding(rules []legacyFirewalldForward) error {
if err := manager.Run("firewall-cmd", "--reload"); err != nil {
return fmt.Errorf("reload firewalld after forwarding transfer: %w", err)
}
return restartDockerAfterFirewalldReload(cmd.Which, controller.CheckActive, controller.HandleRestart)
}
func restartDockerAfterFirewalldReload(
which func(string) bool,
checkActive func(string) (bool, error),
restart func(string) error,
) error {
const service = "docker"
if !which(service) {
return nil
}
active, err := checkActive(service)
if err != nil {
return fmt.Errorf("check Docker status after reloading firewalld: %w", err)
}
if !active {
return nil
}
if err := restart(service); err != nil {
return fmt.Errorf("restart Docker after reloading firewalld: %w", err)
}
return nil
}
@@ -34,9 +34,6 @@ type legacyHostFirewallRecord struct {
Description string
}
// TransferHostFirewall imports the legacy firewalls table into firewall_rules.
// It intentionally does not inspect or mutate the system firewall: the normal
// inventory merge associates imported rows with observed rules by RuleKey.
func TransferHostFirewall(ctx context.Context, provider string) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
@@ -44,6 +41,47 @@ func TransferHostFirewall(ctx context.Context, provider string) error {
return transferHostFirewall(ctx, global.DB, filter.Provider(strings.ToLower(strings.TrimSpace(provider))))
}
func TransferLegacyHostFirewallRuleOwnership(ctx context.Context, provider string, transfer func(context.Context) error) error {
if global.DB == nil {
return errors.New("host firewall transfer database is required")
}
return transferLegacyHostFirewallRuleOwnership(
ctx,
global.DB,
filter.Provider(strings.ToLower(strings.TrimSpace(provider))),
transfer,
)
}
func transferLegacyHostFirewallRuleOwnership(
ctx context.Context,
db *gorm.DB,
provider filter.Provider,
transfer func(context.Context) error,
) error {
if !legacyHostFirewallOwnershipProvider(provider) {
return nil
}
if db == nil {
return errors.New("host firewall transfer database is required")
}
completed, err := migrationRecordExists(db, hostFirewallTransferMigrationID)
if err != nil || completed {
return err
}
if transfer == nil {
return errors.New("legacy host firewall ownership transfer is required")
}
if err := transfer(ctx); err != nil {
return fmt.Errorf("transfer legacy host firewall rule ownership: %w", err)
}
return markMigrationRecord(db, hostFirewallTransferMigrationID)
}
func legacyHostFirewallOwnershipProvider(provider filter.Provider) bool {
return provider == filter.ProviderIptables || provider == filter.ProviderUFW
}
func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Provider) error {
if db == nil {
return errors.New("host firewall transfer database is required")
@@ -69,6 +107,9 @@ func transferHostFirewall(ctx context.Context, db *gorm.DB, provider filter.Prov
if err := importLegacyHostFirewallRules(tx, models); err != nil {
return err
}
if legacyHostFirewallOwnershipProvider(provider) {
return nil
}
return markMigrationRecord(tx, hostFirewallTransferMigrationID)
})
}
@@ -144,7 +185,7 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
rule.SourcePort = ""
rule.DestinationPort = ""
default:
if provider != filter.ProviderIptables {
if provider != filter.ProviderIptables || legacyIptablesAdvancedChain(record.Chain) {
return nil, fmt.Errorf("%w: advanced rule for provider %q", errUnsupportedLegacyHostFirewallRule, provider)
}
}
@@ -166,6 +207,15 @@ func legacyHostFirewallRules(record legacyHostFirewallRecord, provider filter.Pr
return filter.ExpandAtomicRules(rule)
}
func legacyIptablesAdvancedChain(chain string) bool {
switch strings.ToUpper(strings.TrimSpace(chain)) {
case "1PANEL_INPUT", "1PANEL_OUTPUT":
return true
default:
return false
}
}
func legacyIptablesChain(record legacyHostFirewallRecord) string {
typeName := strings.ToLower(strings.TrimSpace(record.Type))
if typeName == "port" || typeName == "address" || typeName == "ip" {
@@ -201,6 +251,9 @@ func legacyUFWHostRules(record legacyHostFirewallRecord, rule filter.FirewallRul
if strings.EqualFold(strings.TrimSpace(record.Type), "address") || strings.EqualFold(strings.TrimSpace(record.Type), "ip") {
rule.SourceAddress, rule.DestinationAddress = splitLegacyUFWAddress(rule.SourceAddress)
}
if legacyUFWSinglePortAllProtocols(record, rule.DestinationPort) {
rule.Protocol = "all"
}
if legacyAddressIsEmpty(rule.SourceAddress) && legacyAddressIsEmpty(rule.DestinationAddress) {
rule.Scope.Family = filter.FamilyInet
} else {
@@ -209,6 +262,18 @@ func legacyUFWHostRules(record legacyHostFirewallRecord, rule filter.FirewallRul
return filter.ExpandAtomicRules(rule)
}
func legacyUFWSinglePortAllProtocols(record legacyHostFirewallRecord, port string) bool {
if !strings.EqualFold(strings.TrimSpace(record.Type), "port") {
return false
}
protocol := strings.ToLower(strings.TrimSpace(record.Protocol))
if protocol != "tcp/udp" && protocol != "udp/tcp" {
return false
}
port = strings.TrimSpace(port)
return port != "" && !strings.Contains(port, ",") && !strings.Contains(port, "-")
}
func expandLegacyFamilies(rule filter.FirewallRule, families ...filter.Family) ([]filter.FirewallRule, error) {
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
@@ -320,11 +320,14 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
position = *target.Locator.Position
plan.Previous = &target
plan.Expected = observedForRule(normalized, marker, position)
// UFW updates the comment of an existing rule when the same rule is added
// without insert/prepend. This keeps the rule active and in its original
// position throughout adoption.
plan.Commands = []filter.NativeCommand{commentCommand(normalized, marker)}
plan.RollbackCommands = []filter.NativeCommand{commentCommand(target.Rule, observedComment(target))}
plan.Commands = []filter.NativeCommand{
deletePositionCommand(position),
insertCommand(position, normalized, marker),
}
plan.RollbackCommands = []filter.NativeCommand{
insertCommand(position, target.Rule, observedComment(target)),
deleteRuleCommand(normalized, marker),
}
case filter.ChangeUpdate:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true)
if targetErr != nil {
@@ -0,0 +1,84 @@
package iptables_helper
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
)
const (
LegacyInputChain = "1PANEL_INPUT"
LegacyOutputChain = "1PANEL_OUTPUT"
legacyInputFileName = "1panel_input.rules"
legacyOutputFileName = "1panel_out.rules"
)
func CleanupLegacyAdvancedChains(ctx context.Context) error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
output, err := RunWithStdContext(ctx, FilterTab, "-S")
if err != nil {
return fmt.Errorf("inspect legacy iptables advanced chains: %w", err)
}
if script := buildLegacyAdvancedChainCleanupScript(output); script != "" {
if err := restoreRules(commands.Restore4, script); err != nil {
return fmt.Errorf("remove legacy iptables advanced chains: %w", err)
}
}
for _, name := range []string{legacyInputFileName, legacyOutputFileName} {
file := filepath.Join(global.Dir.FirewallDir, name)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove legacy iptables rules file %s: %w", file, err)
}
}
return nil
}
func buildLegacyAdvancedChainCleanupScript(output string) string {
legacyChains := map[string]struct{}{LegacyInputChain: {}, LegacyOutputChain: {}}
existing := make(map[string]bool, len(legacyChains))
deletions := make([]string, 0)
for _, raw := range strings.Split(output, "\n") {
line := strings.TrimSpace(raw)
fields, err := shellwords.Parse(line)
if err != nil {
continue
}
if len(fields) == 2 && fields[0] == "-N" {
if _, legacy := legacyChains[fields[1]]; legacy {
existing[fields[1]] = true
}
continue
}
if len(fields) < 4 || fields[0] != "-A" {
continue
}
for index := 2; index+1 < len(fields); index++ {
if fields[index] != "-j" && fields[index] != "-g" {
continue
}
if _, legacy := legacyChains[fields[index+1]]; legacy {
deletions = append(deletions, strings.Replace(line, "-A ", "-D ", 1))
}
break
}
}
for _, chain := range []string{LegacyInputChain, LegacyOutputChain} {
if existing[chain] {
deletions = append(deletions, "-F "+chain, "-X "+chain)
}
}
if len(deletions) == 0 {
return ""
}
return "*filter\n" + strings.Join(deletions, "\n") + "\nCOMMIT\n"
}
+2
View File
@@ -404,6 +404,8 @@ export namespace Firewall {
effective: boolean;
description?: string;
trafficPath: 'forward' | 'input' | 'unknown';
managementTarget?: 'container_guard' | 'host_firewall' | 'needs_diagnosis';
managementReason?: 'nat_inspect_failed' | 'nat_chain_unreachable' | 'proxy_inspect_failed' | 'no_matching_path';
}
export interface DockerGuardPortGroup {
key: string;
+1 -1
View File
@@ -4094,7 +4094,7 @@ const message = {
scopeInactive: 'The managed scope is inactive. New rules may not affect current traffic.',
scopeMissing: 'Managed scope {0} is missing and will be created safely when the first rule is applied.',
scopeUnmanagedActive: 'Other active scopes were detected: {0}. 1Panel will not modify their rules.',
scopeRuntimeMismatch: 'The runtime and permanent configurations differ for: {0}.',
scopeRuntimeMismatch: 'The active and permanent firewalld configurations differ. Restart the firewall.',
dockerRestart: 'Firewall operations require restarting the Docker service',
firewallHelper: '{0} system firewall',
firewallNotStart: 'The system firewall is not enabled at present. Enable it first.',
+2 -1
View File
@@ -4137,7 +4137,8 @@ const message = {
scopeInactive: 'El ámbito gestionado está inactivo. Las reglas nuevas podrían no afectar al tráfico actual.',
scopeMissing: 'Falta el ámbito gestionado {0}; se creará de forma segura al aplicar la primera regla.',
scopeUnmanagedActive: 'Se detectaron otros ámbitos activos: {0}. 1Panel no modificará sus reglas.',
scopeRuntimeMismatch: 'Las configuraciones activa y permanente difieren en: {0}.',
scopeRuntimeMismatch:
'Las configuraciones activa y permanente de firewalld no coinciden. Reinicie el firewall.',
dockerRestart: 'Las operaciones del firewall requieren reiniciar el servicio de Docker',
firewallHelper: 'Firewall del sistema {0}',
firewallNotStart: 'El firewall del sistema no está habilitado actualmente. Actívalo primero.',
+1 -1
View File
@@ -4049,7 +4049,7 @@ const message = {
scopeInactive: 'محدودهٔ مدیریت‌شده فعال نیست. قوانین جدید ممکن است بر ترافیک فعلی اثر نگذارند.',
scopeMissing: 'محدودهٔ مدیریت‌شدهٔ {0} وجود ندارد و هنگام اعمال نخستین قانون به‌صورت امن ایجاد می‌شود.',
scopeUnmanagedActive: 'محدوده‌های فعال دیگری یافت شد: {0}. 1Panel قوانین آن‌ها را تغییر نمی‌دهد.',
scopeRuntimeMismatch: 'پیکربندی فعال و دائمی در این موارد متفاوت است: {0}.',
scopeRuntimeMismatch: 'پیکربندی‌های فعال و دائمی firewalld متفاوت‌اند. دیواره آتش را مجدداً راه‌اندازی کنید.',
dockerRestart: 'عملیات دیواره آتش نیاز به راه‌اندازی مجدد سرویس Docker دارد',
firewallHelper: 'دیواره آتش سیستم {0}',
firewallNotStart: 'دیواره آتش سیستم در حال حاضر فعال نیست. ابتدا آن را فعال کنید.',
+2 -1
View File
@@ -4075,7 +4075,8 @@ const message = {
scopeInactive: '管理対象の範囲が有効ではありません新しいルールが現在の通信に適用されない場合があります',
scopeMissing: '管理対象スコープ {0} がありません最初のルール適用時に安全に作成されます',
scopeUnmanagedActive: '他の有効な範囲が見つかりました{0}1Panel はそのルールを変更しません',
scopeRuntimeMismatch: '実行中と永続設定で次の項目が一致しません{0}',
scopeRuntimeMismatch:
'firewalld の実行中の設定と永続設定が一致しませんファイアウォールを再起動してください',
dockerRestart: 'ファイアウォール操作にはDockerサービスの再起動が必要です',
firewallHelper: '{0}システムファイアウォール',
firewallNotStart: '現在システムファイアウォールは有効になっていません最初に有効にします',
+1 -1
View File
@@ -4002,7 +4002,7 @@ const message = {
scopeInactive: '관리 범위가 비활성 상태입니다. 규칙이 현재 트래픽에 적용되지 않을 있습니다.',
scopeMissing: '관리 범위 {0}() 없으며 규칙을 적용할 안전하게 생성됩니다.',
scopeUnmanagedActive: '다른 활성 범위가 감지되었습니다: {0}. 1Panel은 해당 규칙을 변경하지 않습니다.',
scopeRuntimeMismatch: '실행 구성과 영구 구성 항목이 다릅니다: {0}.',
scopeRuntimeMismatch: 'firewalld의 실행 구성과 영구 구성 릅니다. 방화벽을 재시작하세요.',
dockerRestart: '방화벽 작업에는 Docker 서비스 재시작이 필요합니다',
firewallHelper: '{0} 시스템 방화벽',
firewallNotStart: '현재 시스템 방화벽이 활성화되지 않았습니다. 먼저 활성화하세요.',
+1 -1
View File
@@ -3971,7 +3971,7 @@ const message = {
scopeInactive: 'ຂອບເຂດທີ່ຈັດການບໍ່ໄດ້ເປີດໃຊ້. ກົດໃໝ່ອາດບໍ່ມີຜົນຕໍ່ການຈະລາຈອນປັດຈຸບັນ.',
scopeMissing: 'ບໍ່ພົບຂອບເຂດຈັດການ {0}; ລະບົບຈະສ້າງຢ່າງປອດໄພເມື່ອນຳໃຊ້ກົດທຳອິດ.',
scopeUnmanagedActive: 'ພົບຂອບເຂດອື່ນທີ່ໃຊ້ງານ: {0}. 1Panel ຈະບໍ່ແກ້ໄຂກົດເຫຼົ່ານັ້ນ.',
scopeRuntimeMismatch: 'ຄ່າທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນໃນ: {0}.',
scopeRuntimeMismatch: 'ການຕັ້ງຄ່າ firewalld ທີ່ກຳລັງໃຊ້ງານ ແລະ ແບບຖາວອນບໍ່ກົງກັນ. ກະລຸນາເລີ່ມຕົ້ນໄຟວໍໃໝ່.',
dockerRestart: 'ການຈັດການໄຟວໍຕ້ອງມີການເລີ່ມຕົ້ນບໍລິການ Docker ໃໝ່',
firewallHelper: 'ໄຟວໍລະບົບ {0}',
firewallNotStart: 'ໄຟວໍລະບົບຍັງບໍ່ໄດ້ເປີດໃຊ້. ກະລຸນາເປີດກ່ອນ.',
+1 -1
View File
@@ -4158,7 +4158,7 @@ const message = {
scopeInactive: 'Skop terurus tidak aktif. Peraturan baharu mungkin tidak mempengaruhi trafik semasa.',
scopeMissing: 'Skop terurus {0} tiada dan akan dicipta dengan selamat apabila peraturan pertama digunakan.',
scopeUnmanagedActive: 'Skop aktif lain dikesan: {0}. 1Panel tidak akan mengubah peraturannya.',
scopeRuntimeMismatch: 'Konfigurasi aktif dan kekal berbeza bagi: {0}.',
scopeRuntimeMismatch: 'Konfigurasi firewalld aktif dan kekal tidak sepadan. Mulakan semula firewall.',
dockerRestart: 'Operasi firewall memerlukan memulakan semula perkhidmatan Docker',
firewallHelper: '{0} firewall sistem',
firewallNotStart: 'Firewall sistem belum diaktifkan. Aktifkannya dahulu.',
+1 -1
View File
@@ -4178,7 +4178,7 @@ const message = {
scopeInactive: 'O escopo gerenciado está inativo. Novas regras podem não afetar o tráfego atual.',
scopeMissing: 'O escopo gerenciado {0} não existe e será criado com segurança ao aplicar a primeira regra.',
scopeUnmanagedActive: 'Outros escopos ativos foram detectados: {0}. O 1Panel não modificará suas regras.',
scopeRuntimeMismatch: 'As configurações ativa e permanente diferem em: {0}.',
scopeRuntimeMismatch: 'As configurações ativa e permanente do firewalld não coincidem. Reinicie o firewall.',
dockerRestart: 'Operações de firewall exigem reinicialização do serviço Docker',
firewallHelper: 'Firewall do sistema {0}',
firewallNotStart: 'O firewall do sistema não está habilitado atualmente. Habilite-o primeiro.',
+1 -1
View File
@@ -4145,7 +4145,7 @@ const message = {
scopeInactive: 'Управляемая область не активна. Новые правила могут не влиять на текущий трафик.',
scopeMissing: 'Управляемая область {0} отсутствует и будет безопасно создана при применении первого правила.',
scopeUnmanagedActive: 'Обнаружены другие активные области: {0}. 1Panel не будет изменять их правила.',
scopeRuntimeMismatch: 'Активная и постоянная конфигурации различаются в следующих элементах: {0}.',
scopeRuntimeMismatch: 'Текущая и постоянная конфигурации firewalld не совпадают. Перезапустите брандмауэр.',
dockerRestart: 'Операции с брандмауэром требуют перезапуска службы Docker',
firewallHelper: '{0} межсетевой экран',
firewallNotStart: 'Межсетевой экран в настоящее время не включен. Сначала включите его.',
+2 -1
View File
@@ -4158,7 +4158,8 @@ const message = {
scopeInactive: 'Yönetilen kapsam etkin değil. Yeni kurallar mevcut trafiği etkilemeyebilir.',
scopeMissing: 'Yönetilen {0} kapsamı eksik ve ilk kural uygulanırken güvenli şekilde oluşturulacak.',
scopeUnmanagedActive: 'Başka etkin kapsamlar algılandı: {0}. 1Panel bunların kurallarını değiştirmez.',
scopeRuntimeMismatch: 'Etkin ve kalıcı yapılandırmalar şu öğelerde farklı: {0}.',
scopeRuntimeMismatch:
'Etkin ve kalıcı firewalld yapılandırmaları eşleşmiyor. Güvenlik duvarını yeniden başlatın.',
dockerRestart: 'Güvenlik duvarı işlemleri Docker hizmetinin yeniden başlatılmasını gerektirir',
firewallHelper: '{0} sistem güvenlik duvarı',
firewallNotStart: 'Sistem güvenlik duvarı şu anda etkin değil. Önce etkinleştirin.',
+1 -1
View File
@@ -3821,7 +3821,7 @@ const message = {
scopeInactive: '目前託管範圍未啟用新增規則可能不會作用於現有流量',
scopeMissing: '託管範圍 {0} 尚未建立套用第一條規則時將以安全方式建立',
scopeUnmanagedActive: '偵測到其他使用中範圍{0}1Panel 不會修改其中的規則',
scopeRuntimeMismatch: '執行設定與永久設定在下列項目不一致{0}',
scopeRuntimeMismatch: 'firewalld 執行設定與永久設定不一致請重新啟動防火牆',
dockerRestart: '防火牆操作需要重新啟動 Docker 服務',
firewallHelper: '{0}系統防火牆',
firewallNotStart: '尚未啟用系統防火牆請先啟用',
+1 -1
View File
@@ -3869,7 +3869,7 @@ const message = {
scopeInactive: '当前托管范围未激活新增规则可能不会作用于现有流量',
scopeMissing: '托管范围 {0} 尚未创建应用首条规则时将以安全方式创建',
scopeUnmanagedActive: '检测到其他活动范围{0}1Panel 不会修改其中的规则',
scopeRuntimeMismatch: '运行配置与永久配置在以下项目中不一致{0}',
scopeRuntimeMismatch: 'firewalld 运行配置与永久配置不一致请重启防火墙',
dockerRestart: '防火墙操作需要重启 Docker 服务',
firewallHelper: '{0}系统防火墙',
firewallNotStart: '当前未开启系统防火墙请先开启',
@@ -16,6 +16,8 @@
{{ $t('commons.button.selectAll') }}
</el-checkbox>
<el-button
v-permission
v-node-admin
type="primary"
:disabled="selectedEndpoints.length === 0"
@click="openPolicy(selectedEndpoints)"
@@ -62,11 +64,20 @@
</el-tooltip>
</div>
<div class="port-card-actions">
<el-button type="primary" link size="small" @click.stop="openPolicy(group.endpoints)">
<el-button
v-permission
v-node-admin
type="primary"
link
size="small"
@click.stop="openPolicy(group.endpoints)"
>
{{ $t('commons.button.set') }}
</el-button>
<el-button
v-if="group.endpoint.policyUUID"
v-permission
v-node-admin
type="primary"
link
size="small"
@@ -167,7 +178,11 @@ import { deleteDockerPortGuardPolicies, upsertDockerPortGuardPolicies } from '@/
import i18n from '@/lang';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { dockerGuardEndpointStatusMessage, isValidDockerGuardSource } from '@/views/host/firewall/docker/model';
import {
dockerGuardEndpointStatusMessage,
dockerGuardManagementTarget,
isValidDockerGuardSource,
} from '@/views/host/firewall/docker/model';
import { formatHostAddress, formatHostAddressList, splitTagValues } from '@/views/host/firewall/utils/validation';
const props = defineProps<{ base: Firewall.DockerGuardBase; containers: Firewall.DockerGuardContainer[] }>();
@@ -279,17 +294,17 @@ const toggleSelection = (key: string) => {
};
const openPolicy = (endpoints: Firewall.DockerGuardEndpoint[]) => {
if (!endpoints.length) return;
const paths = new Set(endpoints.map((endpoint) => endpoint.trafficPath || 'unknown'));
if (paths.size !== 1) {
const targets = new Set(endpoints.map(dockerGuardManagementTarget));
if (targets.size !== 1) {
MsgWarning(i18n.global.t('firewall.dockerTrafficPathMixed'));
return;
}
const path = [...paths][0];
if (path === 'input') {
const target = [...targets][0];
if (target === 'host_firewall') {
MsgWarning(i18n.global.t('firewall.dockerInputUseHostFirewall'));
return;
}
if (path !== 'forward') {
if (target !== 'container_guard') {
MsgWarning(i18n.global.t('firewall.dockerTrafficPathUnknown'));
return;
}
@@ -379,8 +394,9 @@ const portMappingLabel = (row: Firewall.DockerGuardPortGroup) => {
return `${publishedEndpoint}${value}/${row.endpoint.protocol}`;
};
const protectionSummary = (row: Firewall.DockerGuardEndpoint) => {
if (row.trafficPath === 'input') return i18n.global.t('firewall.dockerInputUseHostFirewall');
if (row.trafficPath === 'unknown') return i18n.global.t('firewall.dockerTrafficPathPending');
const target = dockerGuardManagementTarget(row);
if (target === 'host_firewall') return i18n.global.t('firewall.dockerInputUseHostFirewall');
if (target === 'needs_diagnosis') return i18n.global.t('firewall.dockerTrafficPathUnknown');
if (!row.policyUUID) return i18n.global.t('firewall.dockerGuardUnprotected');
let summary = i18n.global.t('firewall.denyAll');
if (row.mode === 'deny_sources') {
@@ -1,21 +1,36 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="w-70">
<el-alert class="mb-3" type="info" :closable="false" :title="$t('commons.msg.importHelper')" />
<el-upload
ref="uploadRef"
v-model:file-list="uploaderFiles"
action="#"
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
>
<el-button type="primary">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<el-card class="mt-3 w-full" v-loading="loading">
<ComplexTable v-model:selects="selects" :data="policies" :height="420">
<div class="import-file-bar mt-3">
<el-upload
ref="uploadRef"
v-model:file-list="uploaderFiles"
action="#"
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
>
<el-button type="primary" icon="Upload">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<div v-if="uploaderFiles.length" class="import-file-info">
<el-icon><Document /></el-icon>
<span class="import-file-name">{{ uploaderFiles[0].name }}</span>
</div>
<el-text v-else type="info">.json</el-text>
</div>
<el-card class="mt-3 w-full" shadow="never" v-loading="loading">
<template #header>
<div class="import-preview-header">
<span>{{ $t('commons.button.preview') }}</span>
<el-tag v-if="policies.length" type="info" effect="plain">
{{ $t('commons.table.total', [policies.length]) }}
</el-tag>
</div>
</template>
<ComplexTable v-model:selects="selects" :data="policies" :height="300">
<el-table-column type="selection" fix />
<el-table-column label="IP" prop="family" min-width="65">
<template #default="{ row }">{{ row.family === 'ipv6' ? 'IPv6' : 'IPv4' }}</template>
@@ -50,6 +65,7 @@ import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type Up
import { ref } from 'vue';
import { dockerGuardEndpointKey, normalizeDockerGuardPolicy } from '@/views/host/firewall/docker/model';
import { formatHostAddressList } from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
const emit = defineEmits<{ (event: 'search'): void }>();
const visible = ref(false);
@@ -63,6 +79,8 @@ const displaySources = (policy: Firewall.DockerGuardPolicy) => formatHostAddress
const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
if (!uploadFile.raw) return;
loading.value = true;
policies.value = [];
selects.value = [];
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
reader.onload = (event) => {
@@ -140,11 +158,43 @@ const modeLabel = (mode: Firewall.DockerGuardPolicy['mode']) => {
};
const acceptParams = () => {
loading.value = false;
policies.value = [];
selects.value = [];
uploaderFiles.value = [];
uploadRef.value?.clearFiles();
visible.value = true;
};
defineExpose({ acceptParams });
</script>
<style scoped lang="scss">
.import-file-bar {
display: flex;
min-height: 32px;
align-items: center;
gap: 12px;
}
.import-file-info {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: var(--el-text-color-regular);
}
.import-file-name {
overflow: hidden;
max-width: 420px;
text-overflow: ellipsis;
white-space: nowrap;
}
.import-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
@@ -239,7 +239,11 @@ import { ElMessageBox } from 'element-plus';
import { Lock } from '@element-plus/icons-vue';
import { downloadWithContent } from '@/utils/file';
import { getCurrentDateFormatted } from '@/utils/date';
import { dockerGuardEndpointKey, dockerGuardEndpointStatusMessage } from '@/views/host/firewall/docker/model';
import {
dockerGuardEndpointKey,
dockerGuardEndpointStatusMessage,
dockerGuardManagementTarget,
} from '@/views/host/firewall/docker/model';
import { formatHostAddressList } from '@/views/host/firewall/utils/validation';
import { newUUID } from '@/utils/id';
@@ -403,10 +407,11 @@ const displaySources = (endpoint: Firewall.DockerGuardEndpoint) =>
const endpointStatusMessage = (endpoint: Firewall.DockerGuardEndpoint) =>
dockerGuardEndpointStatusMessage(data.base, endpoint);
const isDockerPolicyEndpoint = (endpoint: Firewall.DockerGuardEndpoint) =>
endpoint.trafficPath === 'forward' && Boolean(endpoint.policyUUID);
dockerGuardManagementTarget(endpoint) === 'container_guard' && Boolean(endpoint.policyUUID);
const endpointPrompt = (endpoint: Firewall.DockerGuardEndpoint) => {
if (endpoint.trafficPath === 'input') return i18n.global.t('firewall.dockerInputUseHostFirewall');
if (endpoint.trafficPath === 'unknown') return i18n.global.t('firewall.dockerTrafficPathPending');
const target = dockerGuardManagementTarget(endpoint);
if (target === 'host_firewall') return i18n.global.t('firewall.dockerInputUseHostFirewall');
if (target === 'needs_diagnosis') return i18n.global.t('firewall.dockerTrafficPathUnknown');
return i18n.global.t('firewall.dockerGuardUnprotected');
};
@@ -7,6 +7,15 @@ type DockerGuardEndpointIdentity = Pick<Firewall.DockerGuardEndpoint, 'family' |
export const dockerGuardEndpointKey = (endpoint: DockerGuardEndpointIdentity): string =>
`${endpoint.family}|${endpoint.hostIP}|${endpoint.hostPort}|${endpoint.protocol}`;
export const dockerGuardManagementTarget = (
endpoint: Firewall.DockerGuardEndpoint,
): NonNullable<Firewall.DockerGuardEndpoint['managementTarget']> => {
if (endpoint.managementTarget) return endpoint.managementTarget;
if (endpoint.trafficPath === 'forward') return 'container_guard';
if (endpoint.trafficPath === 'input') return 'host_firewall';
return 'needs_diagnosis';
};
export const isValidDockerGuardSource = (family: Firewall.DockerGuardEndpoint['family'], value: string): boolean =>
isValidAddressForFamily(family, value);
@@ -34,8 +43,9 @@ export const dockerGuardEndpointStatusMessage = (
endpoint: Firewall.DockerGuardEndpoint,
): string => {
if (!endpoint.policyUUID || endpoint.effective) return '';
if (endpoint.trafficPath === 'input') return i18n.global.t('firewall.dockerInputPolicyNotEffective');
if (endpoint.trafficPath === 'unknown') return i18n.global.t('firewall.dockerTrafficPathUnknown');
const target = dockerGuardManagementTarget(endpoint);
if (target === 'host_firewall') return i18n.global.t('firewall.dockerInputPolicyNotEffective');
if (target === 'needs_diagnosis') return i18n.global.t('firewall.dockerTrafficPathUnknown');
const ipv6 = endpoint.family === 'ipv6';
return dockerGuardFamilyStatusMessage(base, ipv6 ? 'IPv6' : 'IPv4', ipv6 ? base.ipv6 : base.ipv4);
};
@@ -1,33 +1,47 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="w-70">
<div>
<el-alert :closable="false" show-icon type="info">
<template #default>
<div>{{ $t('commons.msg.importHelper') }}</div>
</template>
</el-alert>
<el-upload
action="#"
:auto-upload="false"
ref="uploadRef"
class="float-left mt-2"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
v-model:file-list="uploaderFiles"
>
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<div class="import-file-bar mt-3">
<el-upload
ref="uploadRef"
v-model:file-list="uploaderFiles"
action="#"
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
>
<el-button type="primary" icon="Upload">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<div v-if="uploaderFiles.length" class="import-file-info">
<el-icon><Document /></el-icon>
<span class="import-file-name">{{ uploaderFiles[0].name }}</span>
</div>
<el-text v-else type="info">.json</el-text>
</div>
<el-card class="mt-2 w-full" v-loading="loading">
<el-card class="mt-3 w-full" shadow="never" v-loading="loading">
<template #header>
<div class="import-preview-header">
<span>{{ $t('commons.button.preview') }}</span>
<el-tag v-if="displayData.length" type="info" effect="plain">
{{ $t('commons.table.total', [displayData.length]) }}
</el-tag>
</div>
</template>
<ComplexTable
:pagination-config="paginationConfig"
@search="search"
v-model:selects="selects"
:data="pageData"
:height="440"
:height="300"
>
<el-table-column type="selection" fix />
<el-table-column label="IP" :min-width="60" prop="family">
@@ -77,8 +91,8 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
import { reactive, ref } from 'vue';
import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type UploadRawFile } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { getNetworkOptions } from '@/api/modules/host';
@@ -90,6 +104,7 @@ import {
isValidPortRange,
normalizePortRange,
} from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -102,8 +117,8 @@ const currentFireName = ref('');
const availableInterfaces = ref<string[]>([]);
const uploadRef = ref();
const uploaderFiles = ref();
const pageData = ref([]);
const uploaderFiles = ref<UploadFile[]>([]);
const pageData = ref<any[]>([]);
const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
@@ -111,10 +126,18 @@ const paginationConfig = reactive({
});
const acceptParams = async (fireName: string): Promise<void> => {
visible.value = true;
loading.value = false;
displayData.value = [];
selects.value = [];
currentRules.value = [];
availableInterfaces.value = [];
uploaderFiles.value = [];
pageData.value = [];
paginationConfig.currentPage = 1;
paginationConfig.total = 0;
uploadRef.value?.clearFiles();
currentFireName.value = fireName;
visible.value = true;
loadCurrentData(fireName);
};
@@ -139,8 +162,13 @@ const search = () => {
};
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
if (!_uploadFile.raw) return;
loading.value = true;
displayData.value = [];
pageData.value = [];
selects.value = [];
paginationConfig.currentPage = 1;
paginationConfig.total = 0;
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
@@ -268,3 +296,33 @@ defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.import-file-bar {
display: flex;
min-height: 32px;
align-items: center;
gap: 12px;
}
.import-file-info {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: var(--el-text-color-regular);
}
.import-file-name {
overflow: hidden;
max-width: 420px;
text-overflow: ellipsis;
white-space: nowrap;
}
.import-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
@@ -1,21 +1,36 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="w-70">
<el-alert class="mb-3" type="info" :closable="false" :title="$t('firewall.importBackendHelper', [provider])" />
<el-upload
ref="uploadRef"
v-model:file-list="uploaderFiles"
action="#"
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
>
<el-button type="primary">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<el-card class="mt-3 w-full" v-loading="loading">
<ComplexTable v-model:selects="selects" :data="rules" :height="420">
<div class="import-file-bar mt-3">
<el-upload
ref="uploadRef"
v-model:file-list="uploaderFiles"
action="#"
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".json"
:on-change="fileOnChange"
:on-exceed="handleExceed"
>
<el-button type="primary" icon="Upload">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<div v-if="uploaderFiles.length" class="import-file-info">
<el-icon><Document /></el-icon>
<span class="import-file-name">{{ uploaderFiles[0].name }}</span>
</div>
<el-text v-else type="info">.json</el-text>
</div>
<el-card class="mt-3 w-full" shadow="never" v-loading="loading">
<template #header>
<div class="import-preview-header">
<span>{{ $t('commons.button.preview') }}</span>
<el-tag v-if="rules.length" type="info" effect="plain">
{{ $t('commons.table.total', [rules.length]) }}
</el-tag>
</div>
</template>
<ComplexTable v-model:selects="selects" :data="rules" :height="300">
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.protocol')" prop="protocol" min-width="90" />
<el-table-column :label="$t('firewall.sourceIP')" min-width="150">
@@ -30,7 +45,9 @@
<el-table-column :label="$t('firewall.destPort')" min-width="110">
<template #default="{ row }">{{ row.destinationPort || $t('firewall.allPorts') }}</template>
</el-table-column>
<el-table-column :label="$t('firewall.action')" prop="action" min-width="90" />
<el-table-column :label="$t('firewall.action')" prop="action" min-width="90">
<template #default="{ row }">{{ actionLabel(row.action) }}</template>
</el-table-column>
<el-table-column :label="$t('commons.table.description')" prop="description" min-width="150" />
</ComplexTable>
</el-card>
@@ -49,6 +66,7 @@ import { checkFirewallRules, createFirewallRules } from '@/api/modules/firewall'
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { formatHostAddress, inferAddressFamily } from '@/views/host/firewall/utils/validation';
import { Document } from '@element-plus/icons-vue';
import { genFileId, type UploadFile, type UploadFiles, type UploadProps, type UploadRawFile } from 'element-plus';
import { ref } from 'vue';
@@ -68,6 +86,12 @@ const displayAddress = (rule: Firewall.Rule, address?: string) => {
return `${wildcard}${i18n.global.t('firewall.anyWhere')}`;
};
const actionLabel = (action: Firewall.Action) => {
if (action === 'accept') return i18n.global.t('firewall.accept');
if (action === 'reject') return i18n.global.t('firewall.reject');
return i18n.global.t('firewall.drop');
};
const isRule = (value: unknown): value is Firewall.Rule => {
if (!value || typeof value !== 'object') return false;
const rule = value as Partial<Firewall.Rule>;
@@ -142,6 +166,8 @@ const normalizeImportedRule = (rule: Firewall.Rule): Firewall.Rule[] => {
const fileOnChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
if (!uploadFile.raw) return;
loading.value = true;
rules.value = [];
selects.value = [];
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
reader.onload = (event) => {
@@ -248,12 +274,44 @@ const onImport = async () => {
};
const acceptParams = (value: Firewall.Provider) => {
loading.value = false;
provider.value = value;
rules.value = [];
selects.value = [];
uploaderFiles.value = [];
uploadRef.value?.clearFiles();
visible.value = true;
};
defineExpose({ acceptParams });
</script>
<style scoped lang="scss">
.import-file-bar {
display: flex;
min-height: 32px;
align-items: center;
gap: 12px;
}
.import-file-info {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: var(--el-text-color-regular);
}
.import-file-name {
overflow: hidden;
max-width: 420px;
text-overflow: ellipsis;
white-space: nowrap;
}
.import-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
@@ -372,6 +372,7 @@
<DockerRestart
ref="dockerRestartRef"
v-model:withDockerRestart="withDockerRestart"
:title="$t('firewall.cleanupAction')"
@submit="submitResetRules"
/>
</div>
@@ -395,6 +396,7 @@ import i18n from '@/lang';
import { getCurrentDateFormatted } from '@/utils/date';
import { downloadWithContent } from '@/utils/file';
import { MsgError, MsgSuccess } from '@/utils/message';
import { dockerGuardManagementTarget } from '@/views/host/firewall/docker/model';
import { formatHostAddress } from '@/views/host/firewall/utils/validation';
import RuleImport from '@/views/host/firewall/rule/import/index.vue';
import RuleOperate from '@/views/host/firewall/rule/operate/index.vue';
@@ -419,7 +421,7 @@ interface UsageEntry {
owner: string;
pid?: number;
docker?: boolean;
dockerTrafficPath?: Firewall.DockerGuardEndpoint['trafficPath'];
dockerManagementTarget?: Firewall.DockerGuardEndpoint['managementTarget'];
}
interface DisplayNotice {
@@ -772,7 +774,7 @@ const ruleUsageEntries = (row: RuleRow): UsageEntry[] => {
ports: [endpoint.hostPort],
owner: `Docker: ${endpoint.containerName || endpoint.containerID?.slice(0, 12) || '-'}`,
docker: true,
dockerTrafficPath: endpoint.trafficPath,
dockerManagementTarget: dockerGuardManagementTarget(endpoint),
}));
return [...processes, ...docker];
};
@@ -780,9 +782,11 @@ const usageEntryPortText = (entry: UsageEntry) => entry.ports.join(', ') || '-';
const usageEntryLabel = (entry: UsageEntry) =>
entry.docker
? `${entry.owner} (${usageEntryPortText(entry)}) — ${i18n.global.t(
entry.dockerTrafficPath === 'input'
entry.dockerManagementTarget === 'host_firewall'
? 'firewall.dockerInputUseHostFirewall'
: 'firewall.dockerInputNotProtected',
: entry.dockerManagementTarget === 'container_guard'
? 'firewall.dockerInputNotProtected'
: 'firewall.dockerTrafficPathUnknown',
)}`
: `${entry.owner} (${usageEntryPortText(entry)})`;
const openUsageDetail = (entry: UsageEntry) => {
@@ -931,7 +935,7 @@ const scopeNoticeText = (notice: Firewall.ScopeNotice) => {
case 'unmanaged_active_scopes':
return i18n.global.t('firewall.scopeUnmanagedActive', [value]);
case 'runtime_permanent_mismatch':
return i18n.global.t('firewall.scopeRuntimeMismatch', [value]);
return i18n.global.t('firewall.scopeRuntimeMismatch');
}
};