mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
fix: improve firewall rule management (#13670)
This commit is contained in:
@@ -80,8 +80,11 @@ type FirewallSystemPort struct {
|
||||
}
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
ManagedTotal int64 `json:"managedTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
@@ -106,7 +109,15 @@ type FirewallRuleCheckResult struct {
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
}
|
||||
|
||||
type FirewallNativeDetail struct {
|
||||
|
||||
@@ -280,8 +280,19 @@ func (s *FirewallService) loadProtectedPorts() ([]firewall.PortWhitelist, error)
|
||||
}
|
||||
|
||||
func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRuleInventory) (dto.FirewallRuleInventoryResponse, error) {
|
||||
scope := request.Scope.Normalize()
|
||||
if isCombinedUFWInventoryScope(scope) {
|
||||
requestedScopes := request.Scopes
|
||||
if len(requestedScopes) == 0 && request.Scope.Provider != "" {
|
||||
requestedScopes = []filter.Scope{request.Scope}
|
||||
}
|
||||
if len(requestedScopes) == 0 {
|
||||
return dto.FirewallRuleInventoryResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
scopes := make([]filter.Scope, len(requestedScopes))
|
||||
for index, requested := range requestedScopes {
|
||||
scopes[index] = requested.Normalize()
|
||||
}
|
||||
if len(scopes) == 1 && isCombinedUFWInventoryScope(scopes[0]) {
|
||||
scope := scopes[0]
|
||||
if err := s.checkSelectedProvider(ctx, scope.Provider); err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
@@ -289,19 +300,27 @@ func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRul
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
return s.combinedUFWInventory(ctx, runtime, scope)
|
||||
response, err := s.combinedUFWInventory(ctx, runtime, scope)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
return finalizeFirewallInventory(response, request), nil
|
||||
}
|
||||
if err := scope.ValidateMVP(); err != nil {
|
||||
provider := scopes[0].Provider
|
||||
for _, scope := range scopes {
|
||||
if err := scope.ValidateMVP(); err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
if scope.Provider != provider {
|
||||
return dto.FirewallRuleInventoryResponse{}, fmt.Errorf(
|
||||
"%w: inventory scopes must use the same provider", filter.ErrInvalidScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err := s.checkSelectedProvider(ctx, provider); err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
if err := s.checkSelectedProvider(ctx, scope.Provider); err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
runtime, err := s.adapters.Resolve(scope.Provider)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
snapshot, err := runtime.Observe(ctx, scope)
|
||||
runtime, err := s.adapters.Resolve(provider)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
@@ -309,15 +328,158 @@ func (s *FirewallService) Inventory(ctx context.Context, request dto.FirewallRul
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
desired, err := s.desiredFirewallRulesForScope(ctx, stored, scope)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
response := dto.FirewallRuleInventoryResponse{}
|
||||
for _, scope := range scopes {
|
||||
snapshot, err := runtime.Observe(ctx, scope)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
desired, err := s.desiredFirewallRulesForScope(ctx, stored, scope)
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
}
|
||||
response.Items = append(response.Items, items...)
|
||||
response.Notices = append(response.Notices, snapshot.Notices...)
|
||||
}
|
||||
items, err := filter.MergeInventory(filter.InventoryMergeInput{Observed: snapshot.Rules, Desired: desired})
|
||||
if err != nil {
|
||||
return dto.FirewallRuleInventoryResponse{}, err
|
||||
return finalizeFirewallInventory(response, request), nil
|
||||
}
|
||||
|
||||
func finalizeFirewallInventory(
|
||||
response dto.FirewallRuleInventoryResponse,
|
||||
request dto.FirewallRuleInventory,
|
||||
) dto.FirewallRuleInventoryResponse {
|
||||
response.AllTotal = int64(len(response.Items))
|
||||
for _, item := range response.Items {
|
||||
if isDeletableManagedInventoryItem(item) {
|
||||
response.ManagedTotal++
|
||||
}
|
||||
}
|
||||
return dto.FirewallRuleInventoryResponse{Items: items, Notices: snapshot.Notices}, nil
|
||||
filtered := make([]filter.InventoryItem, 0, len(response.Items))
|
||||
for _, item := range response.Items {
|
||||
if matchesFirewallInventoryRequest(item, request) {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
response.Total = int64(len(filtered))
|
||||
if request.All {
|
||||
response.Items = filtered
|
||||
return response
|
||||
}
|
||||
page, pageSize := max(1, request.Page), max(1, request.PageSize)
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(filtered) {
|
||||
response.Items = make([]filter.InventoryItem, 0)
|
||||
return response
|
||||
}
|
||||
end := min(start+pageSize, len(filtered))
|
||||
response.Items = filtered[start:end]
|
||||
return response
|
||||
}
|
||||
|
||||
func matchesFirewallInventoryRequest(item filter.InventoryItem, request dto.FirewallRuleInventory) bool {
|
||||
if slicesContains(request.ExcludeChains, item.Rule.Scope.Chain) {
|
||||
return false
|
||||
}
|
||||
if len(request.Families) > 0 && !matchesFirewallInventoryFamily(item.Rule, request.Families) {
|
||||
return false
|
||||
}
|
||||
if len(request.Actions) > 0 && !matchesFirewallInventoryAction(item.Rule.Action, request.Actions) {
|
||||
return false
|
||||
}
|
||||
if len(request.States) > 0 && !slicesContains(request.States, item.State) {
|
||||
return false
|
||||
}
|
||||
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
||||
if keyword == "" {
|
||||
return true
|
||||
}
|
||||
rule := item.Rule
|
||||
values := []string{
|
||||
firewallInventoryProtocol(rule), rule.SourceAddress, rule.SourcePort, rule.DestinationAddress,
|
||||
rule.DestinationPort, rule.Description, string(rule.Action), string(item.State),
|
||||
}
|
||||
if item.Observed != nil {
|
||||
values = append(values, item.Observed.Rule.Description)
|
||||
}
|
||||
if item.Desired != nil {
|
||||
values = append(values, item.Desired.Rule.Description)
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesFirewallInventoryFamily(rule filter.FirewallRule, families []filter.Family) bool {
|
||||
for _, family := range families {
|
||||
if rule.Scope.Family != filter.FamilyInet && rule.Scope.Family == family {
|
||||
return true
|
||||
}
|
||||
if rule.Scope.Family == filter.FamilyInet &&
|
||||
(rule.SourceAddress == "" || (family == filter.FamilyIPv6) == strings.Contains(rule.SourceAddress, ":")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesFirewallInventoryAction(action filter.Action, actions []string) bool {
|
||||
for _, requested := range actions {
|
||||
if requested == "accept" && action == filter.ActionAccept {
|
||||
return true
|
||||
}
|
||||
if requested == "deny" && action != filter.ActionAccept {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firewallInventoryProtocol(rule filter.FirewallRule) string {
|
||||
if rule.NativeKind == filter.NativeKindZoneService {
|
||||
return "service"
|
||||
}
|
||||
if rule.NativeKind == filter.NativeKindUFWApplication && rule.Protocol == "" {
|
||||
return "app"
|
||||
}
|
||||
if rule.Scope.Provider == filter.ProviderUFW && rule.Protocol == "all" && rule.DestinationPort != "" {
|
||||
return "tcp/udp"
|
||||
}
|
||||
return rule.Protocol
|
||||
}
|
||||
|
||||
func slicesContains[T comparable](values []T, target T) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDeletableManagedInventoryItem(item filter.InventoryItem) bool {
|
||||
if item.Desired == nil || item.Desired.Protected || item.State == filter.InventoryStateProtected {
|
||||
return false
|
||||
}
|
||||
if item.Desired.Origin != filter.RuleOriginCreated && item.Desired.Origin != filter.RuleOriginAdopted {
|
||||
return false
|
||||
}
|
||||
if isIptablesSystemPresetInventoryScope(item.Rule.Scope) {
|
||||
return false
|
||||
}
|
||||
return item.State != filter.InventoryStateDrifted ||
|
||||
(item.Match == filter.InventoryMatchMissing && item.Observed == nil)
|
||||
}
|
||||
|
||||
func isIptablesSystemPresetInventoryScope(scope filter.Scope) bool {
|
||||
return (scope.Provider == filter.ProviderIptables || scope.Provider == filter.ProviderNftables) &&
|
||||
(scope.Chain == filter.BasicBeforeChain || scope.Chain == filter.BasicAfterChain)
|
||||
}
|
||||
|
||||
func isCombinedUFWInventoryScope(scope filter.Scope) bool {
|
||||
@@ -2419,26 +2581,7 @@ func loadRequiredFirewallPortWhiteList() ([]firewall.PortWhitelist, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
func SyncFirewallPortWhitelistAfterUpdate(oldValue string) error {
|
||||
client, err := selectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := lifecycle.LoadState(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables {
|
||||
isInit, _, err := loadDirectFirewallInitStatus(state.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isInit {
|
||||
return nil
|
||||
}
|
||||
} else if !state.IsActive {
|
||||
return nil
|
||||
}
|
||||
func ReleaseFirewallPortWhitelistAfterUpdate(oldValue string) error {
|
||||
ports, err := loadConfiguredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2451,14 +2594,50 @@ func SyncFirewallPortWhitelistAfterUpdate(oldValue string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.Name == constant.FirewallProviderIptables || state.Name == constant.FirewallProviderNftables {
|
||||
ports = excludeFirewallPorts(ports, required)
|
||||
oldPorts = excludeFirewallPorts(oldPorts, required)
|
||||
} else {
|
||||
ports = firewall.NormalizePortWhitelist(append(ports, required...))
|
||||
oldPorts = firewall.NormalizePortWhitelist(append(oldPorts, required...))
|
||||
removed := excludeFirewallPorts(oldPorts, ports)
|
||||
removed = excludeFirewallPorts(removed, required)
|
||||
return newFirewallService().releaseSystemPorts(context.Background(), systemPorts(removed))
|
||||
}
|
||||
|
||||
// releaseSystemPorts only removes whitelist ownership from persisted rules. It
|
||||
// deliberately leaves the active native rules unchanged so users can delete
|
||||
// them explicitly from the firewall rule list.
|
||||
func (s *FirewallService) releaseSystemPorts(ctx context.Context, ports []dto.FirewallSystemPort) error {
|
||||
portSet, err := normalizeSystemPorts(ports)
|
||||
if err != nil || len(portSet) == 0 {
|
||||
return err
|
||||
}
|
||||
return syncManagedAcceptedPorts(oldPorts, ports)
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
|
||||
owners := make(map[string]struct{}, len(portSet)*2)
|
||||
for _, port := range portSet {
|
||||
owners[model.FirewallRuleOwner(
|
||||
constant.FirewallRuleSourceSecurity,
|
||||
constant.FirewallSystemAcceptedPortSourcePrefix+systemPortKey(port),
|
||||
)] = struct{}{}
|
||||
if port.Family == constant.FirewallFamilyIPv4 {
|
||||
owners[model.FirewallRuleOwner(
|
||||
constant.FirewallRuleSourceSecurity,
|
||||
constant.FirewallSystemAcceptedPortSourcePrefix+legacySystemPortKey(port),
|
||||
)] = struct{}{}
|
||||
}
|
||||
}
|
||||
records, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, record := range records {
|
||||
if _, exists := owners[record.Owner]; !exists {
|
||||
continue
|
||||
}
|
||||
if err := s.rules.UpdateWithRevision(ctx, record.UUID, record.Revision, map[string]interface{}{
|
||||
"owner": constant.FirewallRuleSourceUser,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("release accepted firewall port rule %q: %w", record.UUID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newIptablesHelperManager() *iptables_helper.Manager {
|
||||
|
||||
@@ -981,6 +981,16 @@ type firewallScopeReconciler struct {
|
||||
}
|
||||
|
||||
func (r *firewallScopeReconciler) reconcile() {
|
||||
if r.runtime.Provider() == filter.ProviderUFW {
|
||||
snapshot, err := r.runtime.ObserveMutation(r.ctx, r.snapshot.Scope)
|
||||
if err != nil {
|
||||
for _, entry := range r.entries {
|
||||
entry.fail(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
r.snapshot = snapshot
|
||||
}
|
||||
removes := make([]*firewallRuleSyncEntry, 0)
|
||||
updates := make([]*firewallRuleSyncEntry, 0)
|
||||
creates := make([]*firewallRuleSyncEntry, 0)
|
||||
@@ -1217,6 +1227,10 @@ func firewallRuleSyncChange(
|
||||
before := firewallsync.ObservedRule(*item.Observed)
|
||||
locator := item.Observed.Locator
|
||||
change.Operation = filter.ChangeUpdate
|
||||
if requiresUFWMarkerAdoption(entry.desired, *item.Observed) {
|
||||
change.Operation = filter.ChangeAdopt
|
||||
change.PreviousMarker = item.Observed.Marker
|
||||
}
|
||||
change.Before = &before
|
||||
change.Locator = &locator
|
||||
default:
|
||||
@@ -1225,6 +1239,17 @@ func firewallRuleSyncChange(
|
||||
return change, true, nil
|
||||
}
|
||||
|
||||
func requiresUFWMarkerAdoption(desired filter.DesiredRule, observed filter.ObservedRule) bool {
|
||||
if desired.Rule.Scope.Provider != filter.ProviderUFW || observed.Marker == desired.Marker {
|
||||
return false
|
||||
}
|
||||
marker := strings.TrimSpace(observed.Marker)
|
||||
if marker == "" {
|
||||
return desired.Origin == filter.RuleOriginAdopted
|
||||
}
|
||||
return marker == "1panel-rule:"+strings.TrimSpace(desired.UUID)
|
||||
}
|
||||
|
||||
func firewallRuleSyncInsertionPosition(
|
||||
snapshot filter.Snapshot,
|
||||
entries []*firewallRuleSyncEntry,
|
||||
|
||||
@@ -138,7 +138,7 @@ func (u *SettingService) Update(key, value string) error {
|
||||
return err
|
||||
}
|
||||
if key == constant.FirewallPortWhiteList {
|
||||
return SyncFirewallPortWhitelistAfterUpdate(oldValue)
|
||||
return ReleaseFirewallPortWhitelistAfterUpdate(oldValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ const (
|
||||
)
|
||||
|
||||
type DesiredChange struct {
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
Before *FirewallRule `json:"before,omitempty"`
|
||||
After *FirewallRule `json:"after,omitempty"`
|
||||
Locator *Locator `json:"locator,omitempty"`
|
||||
Append bool `json:"append,omitempty"`
|
||||
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
|
||||
Operation ChangeOperation `json:"operation"`
|
||||
Before *FirewallRule `json:"before,omitempty"`
|
||||
After *FirewallRule `json:"after,omitempty"`
|
||||
Locator *Locator `json:"locator,omitempty"`
|
||||
PreviousMarker string `json:"previousMarker,omitempty"`
|
||||
Append bool `json:"append,omitempty"`
|
||||
RestoreAtEnd bool `json:"restoreAtEnd,omitempty"`
|
||||
}
|
||||
|
||||
type NativeCommand struct {
|
||||
|
||||
@@ -214,6 +214,28 @@ func findObservedInventoryMatch(
|
||||
!ObservedRuleMatchesExpected(candidates[match].rule, desired.Rule) {
|
||||
return match, InventoryMatchChanged
|
||||
}
|
||||
if status != InventoryMatchMissing {
|
||||
return match, status
|
||||
}
|
||||
if desired.Origin == RuleOriginAdopted {
|
||||
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, "", candidates)
|
||||
if status != InventoryMatchMissing {
|
||||
if match >= 0 {
|
||||
return match, InventoryMatchChanged
|
||||
}
|
||||
return match, status
|
||||
}
|
||||
}
|
||||
legacyMarker := "1panel-rule:" + strings.TrimSpace(desired.UUID)
|
||||
if legacyMarker != "1panel-rule:" && legacyMarker != marker {
|
||||
match, status = uniqueUnclaimedSemanticCandidate(desired.Rule, legacyMarker, candidates)
|
||||
if status != InventoryMatchMissing {
|
||||
if match >= 0 {
|
||||
return match, InventoryMatchChanged
|
||||
}
|
||||
return match, status
|
||||
}
|
||||
}
|
||||
return match, status
|
||||
}
|
||||
if desired.ObservedInstanceKey != "" {
|
||||
@@ -222,6 +244,32 @@ func findObservedInventoryMatch(
|
||||
return uniqueUnclaimedCandidate(byRuleKey[desired.RuleKey], candidates)
|
||||
}
|
||||
|
||||
func uniqueUnclaimedSemanticCandidate(
|
||||
expected FirewallRule,
|
||||
marker string,
|
||||
candidates []observedInventoryCandidate,
|
||||
) (int, InventoryMatch) {
|
||||
match := -1
|
||||
count := 0
|
||||
for index := range candidates {
|
||||
candidate := candidates[index]
|
||||
if candidate.claimed || strings.TrimSpace(candidate.rule.Marker) != marker ||
|
||||
!ObservedRuleMatchesExpected(candidate.rule, expected) {
|
||||
continue
|
||||
}
|
||||
match = index
|
||||
count++
|
||||
}
|
||||
switch count {
|
||||
case 0:
|
||||
return -1, InventoryMatchMissing
|
||||
case 1:
|
||||
return match, InventoryMatchExact
|
||||
default:
|
||||
return -1, InventoryMatchAmbiguous
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueUnclaimedCandidate(indices []int, candidates []observedInventoryCandidate) (int, InventoryMatch) {
|
||||
match := -1
|
||||
count := 0
|
||||
|
||||
@@ -173,7 +173,16 @@ func (a *Adapter) Apply(ctx context.Context, plan filter.BackendPlan) (filter.Ap
|
||||
return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, fmt.Errorf("verify UFW rule: %w", err))
|
||||
}
|
||||
if !verification.Matched {
|
||||
return filter.ApplyResult{}, a.compensate(ctx, plan.Rules[0], executed, errors.New("ufw write verification failed"))
|
||||
return filter.ApplyResult{}, a.compensate(
|
||||
ctx,
|
||||
plan.Rules[0],
|
||||
executed,
|
||||
fmt.Errorf(
|
||||
"ufw write verification failed for marker %q in scope %s",
|
||||
plan.Rules[0].Expected.Marker,
|
||||
plan.Scope.Key(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return filter.ApplyResult{
|
||||
Applied: []filter.ObservedRule{plan.Rules[0].Expected},
|
||||
@@ -298,8 +307,14 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
|
||||
position = *target.Locator.Position
|
||||
plan.Previous = &target
|
||||
plan.Expected = observedForRule(normalized, marker, position)
|
||||
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 {
|
||||
@@ -406,15 +421,23 @@ func validateMutationTarget(snapshot filter.Snapshot, change filter.DesiredChang
|
||||
if target.Protected {
|
||||
return filter.ObservedRule{}, filter.ErrProtectedRule
|
||||
}
|
||||
previousMarker := strings.TrimSpace(change.PreviousMarker)
|
||||
if requireOwned {
|
||||
if target.Marker != marker {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw rule is not owned by this rule UUID", filter.ErrInvalidRule)
|
||||
}
|
||||
} else if target.Marker != "" {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw adoption target is already marked", filter.ErrInvalidRule)
|
||||
} else if target.Marker != previousMarker {
|
||||
if target.Marker != "" {
|
||||
return filter.ObservedRule{}, fmt.Errorf("%w: ufw adoption target marker changed", filter.ErrRuleStale)
|
||||
}
|
||||
return filter.ObservedRule{}, filter.ErrRuleStale
|
||||
}
|
||||
if target.Marker == marker && target.ParseStatus != filter.ParseStatusSupported &&
|
||||
(target.ParseStatus == filter.ParseStatusOpaque || filter.ObservedRuleMatchesExpected(target, desired)) {
|
||||
semanticMatch := filter.ObservedRuleMatchesExpected(target, desired)
|
||||
canHydrateOwned := target.Marker == marker &&
|
||||
(target.ParseStatus == filter.ParseStatusOpaque || semanticMatch)
|
||||
canHydrateAdopted := !requireOwned && target.Marker == previousMarker &&
|
||||
target.ParseStatus != filter.ParseStatusOpaque && semanticMatch
|
||||
if target.ParseStatus != filter.ParseStatusSupported && (canHydrateOwned || canHydrateAdopted) {
|
||||
orderIndex := target.Rule.OrderIndex
|
||||
target.Rule = desired
|
||||
target.Rule.OrderIndex = orderIndex
|
||||
@@ -544,19 +567,20 @@ func observedComment(observed filter.ObservedRule) string {
|
||||
}
|
||||
|
||||
func (a *Adapter) verify(ctx context.Context, plan filter.BackendPlan) (filter.VerifyResult, error) {
|
||||
snapshot, err := a.Observe(ctx, plan.Scope)
|
||||
scopes := append([]filter.Scope{plan.Scope}, relatedScopes(plan.Scope)...)
|
||||
snapshots, err := a.ObserveScopes(ctx, scopes)
|
||||
if err != nil {
|
||||
return filter.VerifyResult{}, err
|
||||
}
|
||||
if len(snapshots) != len(scopes) {
|
||||
return filter.VerifyResult{}, errors.New("ufw observation returned an incomplete scope set")
|
||||
}
|
||||
snapshot := snapshots[0]
|
||||
rulePlan := plan.Rules[0]
|
||||
marker := rulePlan.Expected.Marker
|
||||
count := countMarker(snapshot, marker)
|
||||
for _, scope := range relatedScopes(plan.Scope) {
|
||||
other, err := a.Observe(ctx, scope)
|
||||
if err != nil {
|
||||
return filter.VerifyResult{}, err
|
||||
}
|
||||
count += countMarker(other, marker)
|
||||
count := 0
|
||||
for _, observedSnapshot := range snapshots {
|
||||
count += countMarker(observedSnapshot, marker)
|
||||
}
|
||||
if rulePlan.Operation == filter.ChangeDelete {
|
||||
return filter.VerifyResult{Snapshot: snapshot, Matched: count == 0}, nil
|
||||
@@ -819,12 +843,24 @@ func stripRuleAnnotations(value string) (string, bool) {
|
||||
func applicationProfile(value string) (string, string, bool) {
|
||||
profile, iface, ok := splitInterface(value)
|
||||
profile = strings.TrimSpace(profile)
|
||||
if !ok || !validApplicationProfileName(profile) {
|
||||
if !ok || looksLikeAddressedService(profile) || !validApplicationProfileName(profile) {
|
||||
return "", "", false
|
||||
}
|
||||
return profile, iface, true
|
||||
}
|
||||
|
||||
func looksLikeAddressedService(value string) bool {
|
||||
tokens := strings.Fields(value)
|
||||
if len(tokens) < 2 {
|
||||
return false
|
||||
}
|
||||
if isAnywhere(tokens[0]) {
|
||||
return true
|
||||
}
|
||||
_, ok := parseAddress(tokens[0])
|
||||
return ok
|
||||
}
|
||||
|
||||
func validApplicationProfileName(profile string) bool {
|
||||
profile = strings.TrimSpace(profile)
|
||||
if profile == "" || len(profile) > 255 || strings.ContainsAny(profile, "/\\\r\n\x00") {
|
||||
|
||||
@@ -129,8 +129,7 @@ func (e *Engine) CompileDesired(
|
||||
return nil, err
|
||||
}
|
||||
result := make([]filter.DesiredRule, 0, len(rules))
|
||||
scopeOrdinals := make(map[string]int)
|
||||
for _, rule := range rules {
|
||||
for ordinal, rule := range rules {
|
||||
prepared, err := e.Prepare(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -142,9 +141,6 @@ func (e *Engine) CompileDesired(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scopeKey := prepared.Scope.Key()
|
||||
ordinal := scopeOrdinals[scopeKey]
|
||||
scopeOrdinals[scopeKey] = ordinal + 1
|
||||
prepared.UUID = compiledRuleUUID(policyUUID, ruleKey, ordinal)
|
||||
desired := filter.DesiredRule{UUID: policyUUID, Rule: prepared, RuleKey: ruleKey, Origin: origin}
|
||||
if capabilities.Marker {
|
||||
|
||||
@@ -36254,28 +36254,101 @@ const docTemplate = `{
|
||||
},
|
||||
"dto.FirewallRuleInventory": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"accept",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"all": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeChains": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"1PANEL_BASIC_BEFORE",
|
||||
"1PANEL_BASIC",
|
||||
"1PANEL_BASIC_AFTER"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"families": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"ipv4",
|
||||
"ipv6"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"info": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/filter.Scope"
|
||||
},
|
||||
"scopes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.Scope"
|
||||
},
|
||||
"maxItems": 16,
|
||||
"type": "array"
|
||||
},
|
||||
"states": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"managed",
|
||||
"adopted",
|
||||
"external",
|
||||
"drifted",
|
||||
"protected"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scope"
|
||||
"page",
|
||||
"pageSize"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.FirewallRuleInventoryResponse": {
|
||||
"properties": {
|
||||
"allTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.InventoryItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"managedTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"notices": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.ScopeNotice"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -48917,4 +48990,4 @@ var SwaggerInfo = &swag.Spec{
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36250,28 +36250,101 @@
|
||||
},
|
||||
"dto.FirewallRuleInventory": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"accept",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"all": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeChains": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"1PANEL_BASIC_BEFORE",
|
||||
"1PANEL_BASIC",
|
||||
"1PANEL_BASIC_AFTER"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"families": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"ipv4",
|
||||
"ipv6"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"info": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/filter.Scope"
|
||||
},
|
||||
"scopes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.Scope"
|
||||
},
|
||||
"maxItems": 16,
|
||||
"type": "array"
|
||||
},
|
||||
"states": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"managed",
|
||||
"adopted",
|
||||
"external",
|
||||
"drifted",
|
||||
"protected"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scope"
|
||||
"page",
|
||||
"pageSize"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.FirewallRuleInventoryResponse": {
|
||||
"properties": {
|
||||
"allTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.InventoryItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"managedTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"notices": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/filter.ScopeNotice"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -48896,4 +48969,4 @@
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,9 @@ export namespace Firewall {
|
||||
}
|
||||
|
||||
export interface Inventory {
|
||||
total: number;
|
||||
allTotal: number;
|
||||
managedTotal: number;
|
||||
items: InventoryItem[];
|
||||
notices?: ScopeNotice[];
|
||||
}
|
||||
@@ -214,8 +217,14 @@ export namespace Firewall {
|
||||
checkFlag: string;
|
||||
}
|
||||
|
||||
export interface InventoryRequest {
|
||||
scope: Scope;
|
||||
export interface InventoryRequest extends ReqPage {
|
||||
scopes: Scope[];
|
||||
all?: boolean;
|
||||
info: string;
|
||||
families?: Array<'ipv4' | 'ipv6'>;
|
||||
actions?: Array<'accept' | 'deny'>;
|
||||
states?: InventoryState[];
|
||||
excludeChains?: string[];
|
||||
}
|
||||
|
||||
export interface NativeDetailRequest {
|
||||
|
||||
@@ -4270,7 +4270,7 @@ const message = {
|
||||
'Unbind - When unbound, all added firewall rules will become invalid. Proceed with caution. Confirm?',
|
||||
portWhiteList: 'Port allowlist',
|
||||
portWhiteListAlter:
|
||||
'Ports in the allowlist are opened automatically when the firewall is initialized or started.',
|
||||
'Saving does not change the current firewall rules immediately. New ports are opened the next time the firewall is initialized or started. To close a removed port, delete its existing rule manually from the rule list.',
|
||||
portWhiteListHelper: 'Supports IPv4/IPv6, TCP/UDP, single ports, and port ranges such as 8000-8100.',
|
||||
chain: 'Chain',
|
||||
sourceIP: 'Source IP',
|
||||
|
||||
@@ -4320,7 +4320,7 @@ const message = {
|
||||
'Desvincular: al desvincular, todas las reglas de firewall agregadas se volverán inválidas. Proceda con precaución. ¿Confirmar?',
|
||||
portWhiteList: 'Lista blanca de puertos',
|
||||
portWhiteListAlter:
|
||||
'Los puertos de la lista blanca se abrirán automáticamente al inicializar o iniciar el firewall.',
|
||||
'Guardar no cambia inmediatamente las reglas actuales del firewall. Los puertos nuevos se abrirán la próxima vez que se inicialice o inicie el firewall. Para cerrar un puerto eliminado, borre manualmente su regla existente en la lista de reglas.',
|
||||
portWhiteListHelper: 'Admite IPv4/IPv6, TCP/UDP, puertos individuales y rangos como 8000-8100.',
|
||||
chain: 'Cadena',
|
||||
sourceIP: 'IP de Origen',
|
||||
|
||||
@@ -4224,7 +4224,7 @@ const message = {
|
||||
'قطع اتصال - در صورت قطع اتصال، همه قوانین دیواره آتش اضافه شده بیاثر میشوند. با احتیاط ادامه دهید. تأیید میکنید؟',
|
||||
portWhiteList: 'لیست سفید پورت',
|
||||
portWhiteListAlter:
|
||||
'پورتهای موجود در لیست سفید هنگام راهاندازی یا شروع دیواره آتش به طور خودکار باز میشوند.',
|
||||
'ذخیرهسازی قوانین فعلی دیواره آتش را فوراً تغییر نمیدهد. پورتهای جدید هنگام مقداردهی اولیه یا راهاندازی بعدی باز میشوند. برای بستن پورتی که حذف کردهاید، قانون موجود آن را بهصورت دستی از فهرست قوانین حذف کنید.',
|
||||
portWhiteListHelper: 'از IPv4/IPv6، TCP/UDP، پورت تکی و بازههایی مانند 8000-8100 پشتیبانی میکند.',
|
||||
chain: 'زنجیره',
|
||||
sourceIP: 'IP مبدأ',
|
||||
|
||||
@@ -4250,7 +4250,8 @@ const message = {
|
||||
unbindHelper:
|
||||
'アンバインド - アンバインドすると、追加されたすべてのファイアウォールルールが無効になります。注意して操作してください。確認しますか?',
|
||||
portWhiteList: 'ポートホワイトリスト',
|
||||
portWhiteListAlter: 'ファイアウォールの初期化または起動時に、ホワイトリスト内のポートは自動的に許可されます。',
|
||||
portWhiteListAlter:
|
||||
'保存しても現在のファイアウォールルールはすぐに変更されません。新しいポートは、次回の初期化または起動時に許可されます。削除したポートを閉じるには、ルール一覧から既存のルールを手動で削除してください。',
|
||||
portWhiteListHelper: 'IPv4/IPv6、TCP/UDP、単一ポート、8000-8100 のようなポート範囲に対応します。',
|
||||
chain: 'チェーン',
|
||||
sourceIP: '送信元 IP',
|
||||
|
||||
@@ -4170,7 +4170,8 @@ const message = {
|
||||
unbindHelper:
|
||||
'바인딩 해제 - 바인딩 해제 시 추가된 모든 방화벽 규칙이 무효화됩니다. 주의하여 진행하세요. 확인하시겠습니까?',
|
||||
portWhiteList: '포트 화이트리스트',
|
||||
portWhiteListAlter: '방화벽을 초기화하거나 시작할 때 화이트리스트의 포트가 자동으로 허용됩니다.',
|
||||
portWhiteListAlter:
|
||||
'저장해도 현재 방화벽 규칙은 즉시 변경되지 않습니다. 새 포트는 다음 초기화 또는 시작 시 허용됩니다. 제거한 포트를 닫으려면 규칙 목록에서 기존 규칙을 수동으로 삭제하세요.',
|
||||
portWhiteListHelper: 'IPv4/IPv6, TCP/UDP, 단일 포트 및 8000-8100과 같은 포트 범위를 지원합니다.',
|
||||
chain: '체인',
|
||||
sourceIP: '소스 IP',
|
||||
|
||||
@@ -4137,7 +4137,8 @@ const message = {
|
||||
unbindHelper:
|
||||
'ຍົກເລີກການຜູກມັດ - ເມື່ອຍົກເລີກ, ກົດລະບຽບໄຟວໍທັງໝົດຈະບໍ່ມີຜົນ. ກະລຸນາເຮັດດ້ວຍຄວາມລະມັດລະວັງ. ຢືນຢັນ?',
|
||||
portWhiteList: 'ພອດທີ່ອະນຸຍາດ',
|
||||
portWhiteListAlter: 'ພອດໃນລາຍການອະນຸຍາດຈະຖືກເປີດໂດຍອັດຕະໂນມັດເມື່ອເລີ່ມຕົ້ນໄຟວໍ.',
|
||||
portWhiteListAlter:
|
||||
'ການບັນທຶກຈະບໍ່ປ່ຽນກົດໄຟວໍປັດຈຸບັນທັນທີ. ພອດໃໝ່ຈະຖືກເປີດໃນການເລີ່ມຕົ້ນຄັ້ງຕໍ່ໄປ. ເພື່ອປິດພອດທີ່ລຶບອອກ ໃຫ້ລຶບກົດທີ່ມີຢູ່ຈາກລາຍການກົດດ້ວຍຕົນເອງ.',
|
||||
portWhiteListHelper: 'ຮອງຮັບ IPv4/IPv6, TCP/UDP, ພອດດຽວ ແລະ ຊ່ວງພອດເຊັ່ນ 8000-8100.',
|
||||
chain: 'Chain',
|
||||
sourceIP: 'IP ຕົ້ນທາງ',
|
||||
|
||||
@@ -4335,7 +4335,7 @@ const message = {
|
||||
'Nyahikat - Apabila tidak terikat, semua peraturan firewall yang ditambah akan menjadi tidak sah. Teruskan dengan berhati-hati. Sahkan?',
|
||||
portWhiteList: 'Senarai putih port',
|
||||
portWhiteListAlter:
|
||||
'Port dalam senarai putih akan dibuka secara automatik apabila firewall dimulakan atau dihidupkan.',
|
||||
'Menyimpan tidak mengubah peraturan firewall semasa dengan serta-merta. Port baharu akan dibuka pada pemulaan atau pengaktifan seterusnya. Untuk menutup port yang dialih keluar, padam peraturan sedia ada secara manual daripada senarai peraturan.',
|
||||
portWhiteListHelper: 'Menyokong IPv4/IPv6, TCP/UDP, port tunggal dan julat seperti 8000-8100.',
|
||||
chain: 'Rantai',
|
||||
sourceIP: 'IP Sumber',
|
||||
|
||||
@@ -4358,7 +4358,7 @@ const message = {
|
||||
'Desvincular - Quando desvinculado, todas as regras de firewall adicionadas se tornarão inválidas. Prossiga com cautela. Confirmar?',
|
||||
portWhiteList: 'Lista branca de portas',
|
||||
portWhiteListAlter:
|
||||
'As portas na lista branca serão abertas automaticamente ao inicializar ou iniciar o firewall.',
|
||||
'Salvar não altera imediatamente as regras atuais do firewall. Novas portas serão abertas na próxima inicialização ou ativação. Para fechar uma porta removida, exclua manualmente a regra existente na lista de regras.',
|
||||
portWhiteListHelper: 'Suporta IPv4/IPv6, TCP/UDP, portas únicas e intervalos como 8000-8100.',
|
||||
chain: 'Cadeia',
|
||||
sourceIP: 'IP de Origem',
|
||||
|
||||
@@ -4323,7 +4323,7 @@ const message = {
|
||||
'Отвязать - При отвязке все добавленные правила брандмауэра станут недействительными. Действуйте осторожно. Подтвердить?',
|
||||
portWhiteList: 'Белый список портов',
|
||||
portWhiteListAlter:
|
||||
'Порты из белого списка будут автоматически открыты при инициализации или запуске брандмауэра.',
|
||||
'Сохранение не изменяет текущие правила брандмауэра сразу. Новые порты будут открыты при следующей инициализации или запуске. Чтобы закрыть удалённый из списка порт, вручную удалите существующее правило в списке правил.',
|
||||
portWhiteListHelper: 'Поддерживает IPv4/IPv6, TCP/UDP, отдельные порты и диапазоны, например 8000-8100.',
|
||||
chain: 'Цепочка',
|
||||
sourceIP: 'Исходный IP',
|
||||
|
||||
@@ -4335,7 +4335,7 @@ const message = {
|
||||
'Bağlantıyı Kaldır - Bağlantı kaldırıldığında, eklenen tüm güvenlik duvarı kuralları geçersiz olacaktır. Dikkatli ilerleyin. Onaylıyor musunuz?',
|
||||
portWhiteList: 'Port beyaz listesi',
|
||||
portWhiteListAlter:
|
||||
'Beyaz listedeki portlar, güvenlik duvarı başlatılırken veya çalıştırılırken otomatik olarak açılır.',
|
||||
'Kaydetmek mevcut güvenlik duvarı kurallarını hemen değiştirmez. Yeni portlar bir sonraki başlatma veya etkinleştirmede açılır. Listeden kaldırılan bir portu kapatmak için mevcut kuralı kurallar listesinden elle silin.',
|
||||
portWhiteListHelper: 'IPv4/IPv6, TCP/UDP, tek portlar ve 8000-8100 gibi port aralıklarını destekler.',
|
||||
chain: 'Zincir',
|
||||
sourceIP: 'Kaynak IP',
|
||||
|
||||
@@ -3980,7 +3980,8 @@ const message = {
|
||||
bindHelper: '綁定 僅當狀態為綁定時,防火牆規則才能生效,是否確認?',
|
||||
unbindHelper: '解除綁定 解除綁定時,已新增的所有防火牆規則將失效,請謹慎操作,是否確認?',
|
||||
portWhiteList: '埠白名單',
|
||||
portWhiteListAlter: '防火牆初始化或啟動時,將自動放行白名單埠',
|
||||
portWhiteListAlter:
|
||||
'儲存後不會立即變更目前的防火牆規則。新增的埠將在下次初始化或啟動防火牆時自動放行;若要關閉已移出的埠,請前往規則清單手動刪除現有規則。',
|
||||
portWhiteListHelper: '支援 IPv4/IPv6、TCP/UDP、單一連接埠以及 8000-8100 格式的連接埠範圍。',
|
||||
chain: '鏈',
|
||||
sourceIP: '來源 IP',
|
||||
|
||||
@@ -4026,7 +4026,8 @@ const message = {
|
||||
bindHelper: '绑定 仅当状态为绑定时,防火墙规则才能生效,是否确认?',
|
||||
unbindHelper: '解绑 解除绑定时,已添加的所有防火墙规则将失效,请谨慎操作,是否确认?',
|
||||
portWhiteList: '端口白名单',
|
||||
portWhiteListAlter: '防火墙初始化或启动时,将自动放行白名单端口',
|
||||
portWhiteListAlter:
|
||||
'保存后不会立即更改当前防火墙规则。新增端口将在下次初始化或启动防火墙时自动放行;如需关闭已移出的端口,请前往规则列表手动删除现有规则。',
|
||||
portWhiteListHelper: '支持 IPv4/IPv6、TCP/UDP、单端口以及 8000-8100 格式的端口范围。',
|
||||
chain: '链',
|
||||
sourceIP: '源 IP',
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="tableRows"
|
||||
:heightDiff="370"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" sortable>
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
:heightDiff="370"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" sortable min-width="90">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
:heightDiff="370"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" sortable>
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<TableRefresh @search="search" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable v-model:selects="selects" :data="containerRows" :heightDiff="370" row-key="key">
|
||||
<ComplexTable v-model:selects="selects" :data="containerRows" :heightDiff="320" row-key="key">
|
||||
<el-table-column type="selection" :selectable="hasProtectedEndpoint" width="48" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" min-width="180">
|
||||
<template #default="{ row }">{{ row.name }}</template>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
:data="data"
|
||||
:heightDiff="370"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column label="IP" :min-width="60" prop="family">
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission
|
||||
:disabled="loading || allManagedRules.length === 0"
|
||||
:disabled="loading || managedTotal === 0"
|
||||
@click="exportRulesBySelection"
|
||||
>
|
||||
{{ $t('commons.button.export') }}
|
||||
@@ -173,7 +173,7 @@
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</div>
|
||||
<TableSearch v-model:searchName="searchName" @search="resetPagination" />
|
||||
<TableSearch v-model:searchName="searchName" @search="searchWithReset" />
|
||||
<TableRefresh @search="search" />
|
||||
<TableSetting title="firewall-rule-refresh" @search="search" />
|
||||
</template>
|
||||
@@ -182,9 +182,10 @@
|
||||
<ComplexTable
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="pagedItems"
|
||||
:heightDiff="370"
|
||||
:data="allRows"
|
||||
:heightDiff="320"
|
||||
row-key="rowKey"
|
||||
@search="search"
|
||||
>
|
||||
<el-table-column type="selection" :selectable="isDeletableManagedRule" width="48" fix />
|
||||
<el-table-column :label="$t('firewall.action')" width="76">
|
||||
@@ -397,7 +398,7 @@ import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import FireStatus from '@/views/host/firewall/status/index.vue';
|
||||
import ProcessDetail from '@/views/host/process/process/detail/index.vue';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { Expand, Filter, Lock, WarningFilled } from '@element-plus/icons-vue';
|
||||
|
||||
@@ -455,6 +456,8 @@ const iptablesChains = ['1PANEL_BASIC_BEFORE', '1PANEL_BASIC', '1PANEL_BASIC_AFT
|
||||
const visibleIptablesChains = ref<string[]>([...iptablesChains]);
|
||||
const searchName = ref('');
|
||||
const inventoryItems = ref<Firewall.InventoryItem[]>([]);
|
||||
const inventoryTotal = ref(0);
|
||||
const managedTotal = ref(0);
|
||||
const listeningProcesses = ref<Process.ListeningProcess[]>([]);
|
||||
const dockerEndpoints = ref<Firewall.DockerGuardEndpoint[]>([]);
|
||||
const selects = ref<RuleRow[]>([]);
|
||||
@@ -511,11 +514,37 @@ const providerScopes = (): Firewall.Scope[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const inventoryFilters = () => ({
|
||||
families: selectedRuleFilters.value
|
||||
.filter((filter) => filter.startsWith('family:'))
|
||||
.map((filter) => filter.slice('family:'.length) as 'ipv4' | 'ipv6'),
|
||||
actions: selectedRuleFilters.value
|
||||
.filter((filter) => filter.startsWith('action:'))
|
||||
.map((filter) => filter.slice('action:'.length) as 'accept' | 'deny'),
|
||||
states: selectedRuleFilters.value
|
||||
.filter((filter) => filter.startsWith('state:'))
|
||||
.map((filter) => filter.slice('state:'.length) as Firewall.InventoryState),
|
||||
excludeChains: isDirectBackend.value
|
||||
? iptablesChains.filter((chain) => !visibleIptablesChains.value.includes(chain))
|
||||
: [],
|
||||
});
|
||||
|
||||
const inventoryRequest = (page = paginationConfig.currentPage, pageSize = paginationConfig.pageSize) => ({
|
||||
scopes: providerScopes(),
|
||||
info: searchName.value,
|
||||
page,
|
||||
pageSize,
|
||||
...inventoryFilters(),
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
if (!isFirewallReady.value) {
|
||||
loading.value = false;
|
||||
inventoryItems.value = [];
|
||||
scopeNotices.value = [];
|
||||
paginationConfig.total = 0;
|
||||
inventoryTotal.value = 0;
|
||||
managedTotal.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -524,18 +553,31 @@ const search = async () => {
|
||||
loading.value = false;
|
||||
inventoryItems.value = [];
|
||||
scopeNotices.value = [];
|
||||
paginationConfig.total = 0;
|
||||
inventoryTotal.value = 0;
|
||||
managedTotal.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const [responses] = await Promise.all([
|
||||
Promise.all(scopes.map((scope) => searchFirewallRules({ scope }))),
|
||||
const [response] = await Promise.all([
|
||||
searchFirewallRules(inventoryRequest()),
|
||||
loadListeningProcesses(),
|
||||
loadDockerEndpoints(),
|
||||
]);
|
||||
inventoryItems.value = responses.flatMap((response) => response.data.items || []);
|
||||
scopeNotices.value = responses.flatMap((response) => response.data.notices || []);
|
||||
const total = response.data.total || 0;
|
||||
const lastPage = Math.max(1, Math.ceil(total / paginationConfig.pageSize));
|
||||
if (paginationConfig.currentPage > lastPage) {
|
||||
paginationConfig.currentPage = lastPage;
|
||||
await search();
|
||||
return;
|
||||
}
|
||||
inventoryItems.value = response.data.items || [];
|
||||
scopeNotices.value = response.data.notices || [];
|
||||
paginationConfig.total = total;
|
||||
inventoryTotal.value = response.data.allTotal || 0;
|
||||
managedTotal.value = response.data.managedTotal || 0;
|
||||
selects.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -714,20 +756,18 @@ const sameIptablesPositionScope = (rule: Firewall.Rule, family: Firewall.Family,
|
||||
|
||||
const priorityPositionRanges = (
|
||||
item?: Firewall.InventoryItem,
|
||||
sourceItems: Firewall.InventoryItem[] = inventoryItems.value,
|
||||
): Partial<Record<Firewall.Family, PriorityPositionRange>> => {
|
||||
if (provider.value === 'firewalld') return {};
|
||||
const extraPosition = item ? 0 : 1;
|
||||
if (provider.value === 'ufw') {
|
||||
if (!item) {
|
||||
const maxPosition = inventoryItems.value.reduce(
|
||||
(max, row) => Math.max(max, row.observed?.locator.position || 0),
|
||||
0,
|
||||
);
|
||||
const maxPosition = sourceItems.reduce((max, row) => Math.max(max, row.observed?.locator.position || 0), 0);
|
||||
const range = { min: 1, max: Math.max(1, maxPosition + 1) };
|
||||
return { ipv4: range, ipv6: range };
|
||||
}
|
||||
const family = item.rule.scope.family;
|
||||
const positions = inventoryItems.value
|
||||
const positions = sourceItems
|
||||
.filter((row) => row.rule.scope.family === family && row.observed?.locator.position)
|
||||
.map((row) => row.observed!.locator.position!);
|
||||
const currentPosition = item.observed?.locator.position || 1;
|
||||
@@ -741,7 +781,7 @@ const priorityPositionRanges = (
|
||||
const chain = item?.rule.scope.chain || '1PANEL_BASIC';
|
||||
return Object.fromEntries(
|
||||
(['ipv4', 'ipv6'] as Firewall.Family[]).map((family) => {
|
||||
const scopeRows = inventoryItems.value
|
||||
const scopeRows = sourceItems
|
||||
.filter((row) => sameIptablesPositionScope(row.rule, family, chain))
|
||||
.sort(
|
||||
(left, right) => (left.observed?.locator.position || 0) - (right.observed?.locator.position || 0),
|
||||
@@ -771,8 +811,8 @@ const priorityPositionRanges = (
|
||||
);
|
||||
};
|
||||
|
||||
const allRows = computed<RuleRow[]>(() =>
|
||||
inventoryItems.value.map((item, index) => {
|
||||
const toRuleRows = (items: Firewall.InventoryItem[]): RuleRow[] =>
|
||||
items.map((item, index) => {
|
||||
const nativeGroup = item.rule.orderBucket || item.rule.nativeKind || 'default';
|
||||
return {
|
||||
...item,
|
||||
@@ -782,103 +822,43 @@ const allRows = computed<RuleRow[]>(() =>
|
||||
item.observed?.marker ||
|
||||
`${scopeIdentity(item.rule)}:${nativeGroup}:${item.observed?.locator.position ?? index}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const allManagedRules = computed(() => allRows.value.filter((row) => isDeletableManagedRule(row)));
|
||||
|
||||
const matchesRuleFamily = (rule: Firewall.Rule, family: Firewall.Family) => {
|
||||
if (rule.scope.family !== 'inet') return rule.scope.family === family;
|
||||
|
||||
const address = rule.sourceAddress;
|
||||
if (!address) return true;
|
||||
return family === 'ipv6' ? address.includes(':') : !address.includes(':');
|
||||
};
|
||||
|
||||
const matchesRuleFilters = (item: Firewall.InventoryItem) => {
|
||||
const familyFilters = selectedRuleFilters.value.filter((filter) => filter.startsWith('family:'));
|
||||
if (
|
||||
familyFilters.length > 0 &&
|
||||
!familyFilters.some((filter) => matchesRuleFamily(item.rule, filter.slice('family:'.length) as Firewall.Family))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actionFilters = selectedRuleFilters.value.filter((filter) => filter.startsWith('action:'));
|
||||
if (
|
||||
actionFilters.length > 0 &&
|
||||
!actionFilters.some((filter) =>
|
||||
filter === 'action:accept' ? item.rule.action === 'accept' : item.rule.action !== 'accept',
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateFilters = selectedRuleFilters.value.filter((filter) => filter.startsWith('state:'));
|
||||
return stateFilters.length === 0 || stateFilters.some((filter) => item.state === filter.slice('state:'.length));
|
||||
};
|
||||
|
||||
const filteredItems = computed<RuleRow[]>(() => {
|
||||
const keyword = searchName.value.trim().toLowerCase();
|
||||
return allRows.value.filter((item) => {
|
||||
if (
|
||||
(item.rule.scope.provider === 'iptables' || item.rule.scope.provider === 'nftables') &&
|
||||
!visibleIptablesChains.value.includes(item.rule.scope.chain || '')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesRuleFilters(item)) {
|
||||
return false;
|
||||
}
|
||||
if (!keyword) return true;
|
||||
return [
|
||||
displayProtocol(item),
|
||||
displayAddress(item),
|
||||
displayPort(item),
|
||||
item.rule.sourceAddress,
|
||||
item.rule.sourcePort,
|
||||
item.rule.destinationAddress,
|
||||
item.rule.description,
|
||||
item.observed?.rule.description,
|
||||
item.desired?.rule.description,
|
||||
item.observed?.raw,
|
||||
item.rule.action,
|
||||
item.state,
|
||||
].some((value) => value?.toLowerCase().includes(keyword));
|
||||
});
|
||||
});
|
||||
|
||||
const pagedItems = computed(() => {
|
||||
const start = (paginationConfig.currentPage - 1) * paginationConfig.pageSize;
|
||||
return filteredItems.value.slice(start, start + paginationConfig.pageSize);
|
||||
});
|
||||
const allRows = computed<RuleRow[]>(() => toRuleRows(inventoryItems.value));
|
||||
|
||||
const loadAllInventoryItems = async () => {
|
||||
if (inventoryTotal.value === 0) return [];
|
||||
const response = await searchFirewallRules({
|
||||
...inventoryRequest(1, paginationConfig.pageSize),
|
||||
all: true,
|
||||
info: '',
|
||||
families: [],
|
||||
actions: [],
|
||||
states: [],
|
||||
excludeChains: [],
|
||||
});
|
||||
return response.data.items || [];
|
||||
};
|
||||
|
||||
const resetPagination = () => {
|
||||
paginationConfig.currentPage = 1;
|
||||
};
|
||||
|
||||
const searchWithReset = () => {
|
||||
resetPagination();
|
||||
return search();
|
||||
};
|
||||
|
||||
const changeIptablesChainFilter = () => {
|
||||
selects.value = [];
|
||||
resetPagination();
|
||||
return searchWithReset();
|
||||
};
|
||||
|
||||
const changeRuleFilter = () => {
|
||||
selects.value = [];
|
||||
resetPagination();
|
||||
return searchWithReset();
|
||||
};
|
||||
|
||||
watch(
|
||||
filteredItems,
|
||||
(items) => {
|
||||
paginationConfig.total = items.length;
|
||||
const lastPage = Math.max(1, Math.ceil(items.length / paginationConfig.pageSize));
|
||||
if (paginationConfig.currentPage > lastPage) {
|
||||
paginationConfig.currentPage = lastPage;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const notices = computed<DisplayNotice[]>(() => {
|
||||
const unique = new Map<string, DisplayNotice>();
|
||||
scopeNotices.value.forEach((notice) => {
|
||||
@@ -967,10 +947,11 @@ const openCreate = async () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const sourceItems = await loadAllInventoryItems();
|
||||
ruleOperateRef.value?.acceptParams(
|
||||
provider.value as Firewall.Provider,
|
||||
undefined,
|
||||
priorityPositionRanges(),
|
||||
priorityPositionRanges(undefined, sourceItems),
|
||||
supportsFirewalldPriority.value,
|
||||
);
|
||||
};
|
||||
@@ -1016,9 +997,11 @@ const exportRules = async (rows: RuleRow[]) => {
|
||||
downloadWithContent(JSON.stringify(exported, null, 2), `1panel-firewall-rules-${getCurrentDateFormatted()}.json`);
|
||||
};
|
||||
|
||||
const exportRulesBySelection = () => {
|
||||
const exportRulesBySelection = async () => {
|
||||
const selected = selects.value.filter((row) => isDeletableManagedRule(row));
|
||||
return exportRules(selected.length > 0 ? selected : allManagedRules.value);
|
||||
if (selected.length > 0) return exportRules(selected);
|
||||
const allManagedRules = toRuleRows(await loadAllInventoryItems()).filter((row) => isDeletableManagedRule(row));
|
||||
return exportRules(allManagedRules);
|
||||
};
|
||||
|
||||
const isWildcardDestinationPort = (rule: Firewall.Rule) => {
|
||||
@@ -1228,12 +1211,13 @@ const displayRulePriority = (row: Firewall.InventoryItem) => {
|
||||
return row.observed?.locator.position ?? '-';
|
||||
};
|
||||
|
||||
const openEdit = (row: RuleRow) => {
|
||||
const openEdit = async (row: RuleRow) => {
|
||||
if (!isEditableManagedRule(row)) return;
|
||||
const sourceItems = await loadAllInventoryItems();
|
||||
ruleOperateRef.value?.acceptParams(
|
||||
provider.value as Firewall.Provider,
|
||||
row,
|
||||
priorityPositionRanges(row),
|
||||
priorityPositionRanges(row, sourceItems),
|
||||
supportsFirewalldPriority.value,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
<span class="protocol-option-description">{{ $t('firewall.allProtocolHelper') }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
<el-option v-if="provider !== 'ufw'" label="ICMP" value="icmp" />
|
||||
<el-option v-if="provider !== 'ufw'" label="ICMPV6" value="icmpv6" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP" prop="sourceAddresses">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@search="search"
|
||||
:heightDiff="370"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" />
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<TableRefresh @search="resetAndLoadLogs" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :data="logs" :pagination-config="paginationConfig" @row-click="openDetail" :heightDiff="370">
|
||||
<ComplexTable :data="logs" :pagination-config="paginationConfig" @row-click="openDetail" :heightDiff="320">
|
||||
<el-table-column prop="time" :label="$t('commons.table.date')" width="220" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('logs.priority')" width="120">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<TableSetting title="login-log-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="370">
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="330">
|
||||
<el-table-column :label="$t('logs.loginIP')" prop="ip" />
|
||||
<el-table-column v-if="isEnterprise" :label="$t('commons.login.username')" prop="user" />
|
||||
<el-table-column :label="$t('logs.loginAddress')" prop="address" />
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<TableSetting title="operation-log-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="370">
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="330">
|
||||
<el-table-column :label="$t('logs.resource')" prop="group" fix>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.source && row.source.indexOf('-') === -1">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<TableSetting title="task-log-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="370">
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="330">
|
||||
<el-table-column :label="$t('logs.taskName')" prop="name" min-width="180px">
|
||||
<template #default="{ row }">
|
||||
{{ translateTaskText(row.name) }}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template #main>
|
||||
<MainDiv :heightDiff="370">
|
||||
<MainDiv :heightDiff="330">
|
||||
<LogFile
|
||||
ref="logRef"
|
||||
:config="logConfig"
|
||||
|
||||
Reference in New Issue
Block a user