mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
fix(firewall): recover rules after upgrade (#13680)
This commit is contained in:
+17
-15
@@ -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 {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -4049,7 +4049,7 @@ const message = {
|
||||
scopeInactive: 'محدودهٔ مدیریتشده فعال نیست. قوانین جدید ممکن است بر ترافیک فعلی اثر نگذارند.',
|
||||
scopeMissing: 'محدودهٔ مدیریتشدهٔ {0} وجود ندارد و هنگام اعمال نخستین قانون بهصورت امن ایجاد میشود.',
|
||||
scopeUnmanagedActive: 'محدودههای فعال دیگری یافت شد: {0}. 1Panel قوانین آنها را تغییر نمیدهد.',
|
||||
scopeRuntimeMismatch: 'پیکربندی فعال و دائمی در این موارد متفاوت است: {0}.',
|
||||
scopeRuntimeMismatch: 'پیکربندیهای فعال و دائمی firewalld متفاوتاند. دیواره آتش را مجدداً راهاندازی کنید.',
|
||||
dockerRestart: 'عملیات دیواره آتش نیاز به راهاندازی مجدد سرویس Docker دارد',
|
||||
firewallHelper: 'دیواره آتش سیستم {0}',
|
||||
firewallNotStart: 'دیواره آتش سیستم در حال حاضر فعال نیست. ابتدا آن را فعال کنید.',
|
||||
|
||||
@@ -4075,7 +4075,8 @@ const message = {
|
||||
scopeInactive: '管理対象の範囲が有効ではありません。新しいルールが現在の通信に適用されない場合があります。',
|
||||
scopeMissing: '管理対象スコープ {0} がありません。最初のルール適用時に安全に作成されます。',
|
||||
scopeUnmanagedActive: '他の有効な範囲が見つかりました:{0}。1Panel はそのルールを変更しません。',
|
||||
scopeRuntimeMismatch: '実行中と永続設定で次の項目が一致しません:{0}。',
|
||||
scopeRuntimeMismatch:
|
||||
'firewalld の実行中の設定と永続設定が一致しません。ファイアウォールを再起動してください。',
|
||||
dockerRestart: 'ファイアウォール操作にはDockerサービスの再起動が必要です',
|
||||
firewallHelper: '{0}システムファイアウォール',
|
||||
firewallNotStart: '現在、システムファイアウォールは有効になっていません。最初に有効にします。',
|
||||
|
||||
@@ -4002,7 +4002,7 @@ const message = {
|
||||
scopeInactive: '관리 범위가 비활성 상태입니다. 새 규칙이 현재 트래픽에 적용되지 않을 수 있습니다.',
|
||||
scopeMissing: '관리 범위 {0}이(가) 없으며 첫 규칙을 적용할 때 안전하게 생성됩니다.',
|
||||
scopeUnmanagedActive: '다른 활성 범위가 감지되었습니다: {0}. 1Panel은 해당 규칙을 변경하지 않습니다.',
|
||||
scopeRuntimeMismatch: '실행 중 구성과 영구 구성의 다음 항목이 다릅니다: {0}.',
|
||||
scopeRuntimeMismatch: 'firewalld의 실행 중 구성과 영구 구성이 다릅니다. 방화벽을 재시작하세요.',
|
||||
dockerRestart: '방화벽 작업에는 Docker 서비스 재시작이 필요합니다',
|
||||
firewallHelper: '{0} 시스템 방화벽',
|
||||
firewallNotStart: '현재 시스템 방화벽이 활성화되지 않았습니다. 먼저 활성화하세요.',
|
||||
|
||||
@@ -3971,7 +3971,7 @@ const message = {
|
||||
scopeInactive: 'ຂອບເຂດທີ່ຈັດການບໍ່ໄດ້ເປີດໃຊ້. ກົດໃໝ່ອາດບໍ່ມີຜົນຕໍ່ການຈະລາຈອນປັດຈຸບັນ.',
|
||||
scopeMissing: 'ບໍ່ພົບຂອບເຂດຈັດການ {0}; ລະບົບຈະສ້າງຢ່າງປອດໄພເມື່ອນຳໃຊ້ກົດທຳອິດ.',
|
||||
scopeUnmanagedActive: 'ພົບຂອບເຂດອື່ນທີ່ໃຊ້ງານ: {0}. 1Panel ຈະບໍ່ແກ້ໄຂກົດເຫຼົ່ານັ້ນ.',
|
||||
scopeRuntimeMismatch: 'ຄ່າທີ່ໃຊ້ງານແລະຄ່າຖາວອນບໍ່ກົງກັນໃນ: {0}.',
|
||||
scopeRuntimeMismatch: 'ການຕັ້ງຄ່າ firewalld ທີ່ກຳລັງໃຊ້ງານ ແລະ ແບບຖາວອນບໍ່ກົງກັນ. ກະລຸນາເລີ່ມຕົ້ນໄຟວໍໃໝ່.',
|
||||
dockerRestart: 'ການຈັດການໄຟວໍຕ້ອງມີການເລີ່ມຕົ້ນບໍລິການ Docker ໃໝ່',
|
||||
firewallHelper: 'ໄຟວໍລະບົບ {0}',
|
||||
firewallNotStart: 'ໄຟວໍລະບົບຍັງບໍ່ໄດ້ເປີດໃຊ້. ກະລຸນາເປີດກ່ອນ.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -4145,7 +4145,7 @@ const message = {
|
||||
scopeInactive: 'Управляемая область не активна. Новые правила могут не влиять на текущий трафик.',
|
||||
scopeMissing: 'Управляемая область {0} отсутствует и будет безопасно создана при применении первого правила.',
|
||||
scopeUnmanagedActive: 'Обнаружены другие активные области: {0}. 1Panel не будет изменять их правила.',
|
||||
scopeRuntimeMismatch: 'Активная и постоянная конфигурации различаются в следующих элементах: {0}.',
|
||||
scopeRuntimeMismatch: 'Текущая и постоянная конфигурации firewalld не совпадают. Перезапустите брандмауэр.',
|
||||
dockerRestart: 'Операции с брандмауэром требуют перезапуска службы Docker',
|
||||
firewallHelper: '{0} межсетевой экран',
|
||||
firewallNotStart: 'Межсетевой экран в настоящее время не включен. Сначала включите его.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -3821,7 +3821,7 @@ const message = {
|
||||
scopeInactive: '目前託管範圍未啟用,新增規則可能不會作用於現有流量。',
|
||||
scopeMissing: '託管範圍 {0} 尚未建立,套用第一條規則時將以安全方式建立。',
|
||||
scopeUnmanagedActive: '偵測到其他使用中範圍:{0},1Panel 不會修改其中的規則。',
|
||||
scopeRuntimeMismatch: '執行設定與永久設定在下列項目不一致:{0}。',
|
||||
scopeRuntimeMismatch: 'firewalld 執行中設定與永久設定不一致,請重新啟動防火牆。',
|
||||
dockerRestart: '防火牆操作需要重新啟動 Docker 服務',
|
||||
firewallHelper: '{0}系統防火牆',
|
||||
firewallNotStart: '尚未啟用系統防火牆,請先啟用',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user