diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 1e4576baa..9ef84232e 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -26,6 +26,7 @@ type FirewallSubsystemStatus struct { } type FirewallLifecycleOperation struct { + BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"` Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"` WithDockerRestart bool `json:"withDockerRestart"` } diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 04de2134c..73d20a84a 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -131,6 +131,9 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) { response := dto.FirewallLifecycleOperationResponse{} + if request.BackupFile != "" && request.Operation != string(lifecycle.OperationStart) { + return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw") + } if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" { return response, s.OperateFirewall(request) } @@ -155,6 +158,19 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp if err != nil { return response, err } + var restoreRequest dto.FirewallRuleCreate + if request.BackupFile != "" { + if client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW { + return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw") + } + restoreRequest, err = prepareFirewallRuleCreate(dto.FirewallRuleCreate{BackupFile: request.BackupFile, Initialize: true}) + if err != nil { + return response, err + } + if err := validateFirewallCreateBatch(restoreRequest, filter.Provider(client.Name())); err != nil { + return response, err + } + } if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) || (request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) && request.Operation != string(lifecycle.OperationRestart)) { @@ -173,7 +189,26 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp return response, err } taskItem.AddSubTaskWithOps(name, func(t *task.Task) error { - return s.runFirewallLifecycleTask(t, client, request, true) + if request.BackupFile != "" { + if err := s.checkSelectedProvider(t.TaskCtx, filter.Provider(client.Name())); err != nil { + return err + } + } + prepareStart := func(lifecycle.Client) error { + if request.BackupFile != "" { + return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallCreateRulesStep"), func() error { + return s.runFirewallCreateRulesTask(t, restoreRequest, filter.Provider(client.Name())) + }) + } + return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { + err := s.SyncPortWhitelist(t.TaskCtx) + if err == nil && client.Name() == lifecycle.ProviderFirewalld { + err = lifecycleproviders.RemoveFirewalldSSHService() + } + return err + }) + } + return s.runFirewallLifecycleTask(t, client, request, prepareStart) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { closeUnstartedFirewallTask(taskItem) @@ -554,29 +589,9 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request } func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) { - if request.BackupFile != "" { - if len(request.Items) != 0 { - return dto.FirewallRuleCreateResponse{}, filter.ErrInvalidRule - } - items, err := readFirewallBackup(request.BackupFile) - if err != nil { - return dto.FirewallRuleCreateResponse{}, err - } - for _, item := range items { - request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported}) - } - } - - for index, item := range request.Items { - if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" { - continue - } - observed := filterufw.ParseRules(item.Rule.Scope, item.Raw) - if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported { - observed[0].Rule.Description = item.Rule.Description - request.Items[index].Rule = observed[0].Rule - request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, "" - } + request, err := prepareFirewallRuleCreate(request) + if err != nil { + return dto.FirewallRuleCreateResponse{}, err } provider, err := s.selectedProvider(ctx) @@ -609,30 +624,11 @@ func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCr if err != nil { return err } - return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, false) + return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, nil) }, nil) } - ruleState := make(map[string]filter.RuleSet) taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error { - firewallRuleMutationMu.Lock() - if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil { - firewallRuleMutationMu.Unlock() - return err - } - _, err := s.createRules(t.TaskCtx, request, t, ruleState) - firewallRuleMutationMu.Unlock() - if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return err - } - if err != nil { - ruleState = nil - } - whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState) - if whitelistErr == nil && provider == filter.ProviderFirewalld { - whitelistErr = lifecycleproviders.RemoveFirewalldSSHService() - } - t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) - return errors.Join(err, whitelistErr) + return s.runFirewallCreateRulesTask(t, request, provider) }, nil, 0, 0) if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil { taskItem.LogFailedWithErr(taskItem.Name, err) @@ -777,7 +773,11 @@ func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) err return nil } } - recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx)) + whitelistErr := s.SyncPortWhitelist(ctx) + if whitelistErr == nil && provider == filter.ProviderFirewalld { + whitelistErr = lifecycleproviders.RemoveFirewalldSSHService() + } + recordFailure("restore whitelist allowances", whitelistErr) return errors.Join(recoveryErrors...) } @@ -805,17 +805,9 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context, return restoreErr } -func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, syncWhitelist bool) error { +func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, prepareStart func(lifecycle.Client) error) error { ctx := t.TaskCtx provider := filter.Provider(client.Name()) - var prepareStart func(lifecycle.Client) error - if syncWhitelist { - prepareStart = func(lifecycle.Client) error { - return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error { - return s.SyncPortWhitelist(ctx) - }) - } - } operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t) if request.Operation == string(lifecycle.OperationStop) { return operationErr diff --git a/agent/app/service/firewall_utils.go b/agent/app/service/firewall_utils.go index 7e64ea09a..3cd3227f8 100644 --- a/agent/app/service/firewall_utils.go +++ b/agent/app/service/firewall_utils.go @@ -554,6 +554,58 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f return nil } +func prepareFirewallRuleCreate(request dto.FirewallRuleCreate) (dto.FirewallRuleCreate, error) { + if request.BackupFile != "" { + if len(request.Items) != 0 { + return request, filter.ErrInvalidRule + } + items, err := readFirewallBackup(request.BackupFile) + if err != nil { + return request, err + } + for _, item := range items { + request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported}) + } + } + + for index, item := range request.Items { + if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" { + continue + } + observed := filterufw.ParseRules(item.Rule.Scope, item.Raw) + if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported { + observed[0].Rule.Description = item.Rule.Description + request.Items[index].Rule = observed[0].Rule + request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, "" + } + } + + return request, nil +} + +func (s *FirewallService) runFirewallCreateRulesTask(t *task.Task, request dto.FirewallRuleCreate, provider filter.Provider) error { + ruleState := make(map[string]filter.RuleSet) + firewallRuleMutationMu.Lock() + if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil { + firewallRuleMutationMu.Unlock() + return err + } + _, err := s.createRules(t.TaskCtx, request, t, ruleState) + firewallRuleMutationMu.Unlock() + if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + if err != nil { + ruleState = nil + } + whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState) + if whitelistErr == nil && provider == filter.ProviderFirewalld { + whitelistErr = lifecycleproviders.RemoveFirewalldSSHService() + } + t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr) + return errors.Join(err, whitelistErr) +} + func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) { var firstFailure error defer func() { @@ -1734,9 +1786,6 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc var recoveryErrors []error if prepareStart != nil { err := prepareStart(client) - if err == nil && client.Name() == constant.FirewallProviderFirewalld { - err = lifecycleproviders.RemoveFirewalldSSHService() - } if err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err)) } diff --git a/frontend/src/api/modules/firewall.ts b/frontend/src/api/modules/firewall.ts index 106393c42..aa8938f8b 100644 --- a/frontend/src/api/modules/firewall.ts +++ b/frontend/src/api/modules/firewall.ts @@ -21,10 +21,10 @@ export const loadForwardBaseInfo = () => export const searchForwardRule = (request: Firewall.ForwardRuleSearch) => http.post>('/hosts/firewall/forward/search', request, TimeoutEnum.T_40S); -export const operateFire = (operation: string, withDockerRestart: boolean) => +export const operateFire = (operation: string, withDockerRestart: boolean, backupFile?: string) => http.post( '/hosts/firewall/operate', - { operation, withDockerRestart }, + { operation, withDockerRestart, backupFile }, TimeoutEnum.T_10M, ); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index bdbb65b31..c3d2d98df 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -4299,9 +4299,14 @@ const message = { familyIncomplete: '{0} chains are incomplete', resetWithBackup: 'Back up rules and reset', resetOnly: 'Reset only', - resetWithBackupHelper: 'Save the current module rules as JSON for restoration during initialization.', + resetWithBackupHelper: + 'Save the current module rules as JSON for restoration during initialization or startup.', resetOnlyHelper: 'Clear current rules without creating a new backup. Existing backups are kept.', backupDirectoryHelper: 'Valid rule backups in {0} are listed below. The newest file is selected by default.', + startOnly: 'Start only', + startAndRestore: 'Start and restore from backup', + startWithBackupHelper: + 'Start the firewall normally, or choose an existing JSON backup to start and restore rules.', restoreAllRules: 'Initialize and restore rules', importRuleBackup: 'Import from backup', initializeOnly: 'Initialize only', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 42bf64f21..de9981b85 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -4347,10 +4347,14 @@ const message = { resetWithBackup: 'Guardar reglas y restablecer', resetOnly: 'Solo restablecer', resetWithBackupHelper: - 'Guarda las reglas del módulo actual en JSON para restaurarlas durante la inicialización.', + 'Guarde las reglas del módulo actual en JSON para restaurarlas al inicializar o iniciar el cortafuegos.', resetOnlyHelper: 'Borrar las reglas actuales sin crear una copia nueva. Se conservan las copias existentes.', backupDirectoryHelper: 'Se muestran las copias válidas de reglas en {0}. Se selecciona la más reciente por defecto.', + startOnly: 'Solo iniciar', + startAndRestore: 'Iniciar y restaurar desde una copia de seguridad', + startWithBackupHelper: + 'Inicie el cortafuegos normalmente o elija una copia JSON existente para iniciar y restaurar reglas.', restoreAllRules: 'Inicializar y restaurar reglas', importRuleBackup: 'Importar desde copia de seguridad', initializeOnly: 'Solo inicializar', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index f544bcd2c..824f6517c 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -4259,10 +4259,15 @@ const message = { familyIncomplete: 'زنجیره‌های {0} ناقص هستند', resetWithBackup: 'پشتیبان‌گیری قوانین و بازنشانی', resetOnly: 'فقط بازنشانی', - resetWithBackupHelper: 'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه بازیابی شوند.', + resetWithBackupHelper: + 'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه یا راه‌اندازی بازیابی شوند.', resetOnlyHelper: 'قوانین فعلی بدون ایجاد نسخه پشتیبان جدید پاک می‌شوند. نسخه‌های پشتیبان قبلی حفظ می‌شوند.', backupDirectoryHelper: 'نسخه‌های پشتیبان معتبر قوانین در {0} نمایش داده می‌شوند. جدیدترین فایل به‌طور پیش‌فرض انتخاب می‌شود.', + startOnly: 'فقط راه\u200cاندازی', + startAndRestore: 'راه\u200cاندازی و بازیابی از نسخه پشتیبان', + startWithBackupHelper: + 'فایروال را به‌طور معمول راه‌اندازی کنید یا یک نسخه پشتیبان JSON موجود را برای راه‌اندازی و بازیابی قوانین انتخاب کنید.', restoreAllRules: 'مقداردهی اولیه و بازیابی قوانین', importRuleBackup: 'ورود از نسخه پشتیبان', initializeOnly: 'فقط راه‌اندازی', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index ff9f6583c..962a78342 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -4282,9 +4282,12 @@ const message = { familyIncomplete: '{0} のチェーンが不完全です', resetWithBackup: 'ルールをバックアップしてリセット', resetOnly: 'リセットのみ', - resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時に復元できます。', + resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時または起動時に復元できます。', resetOnlyHelper: '新しいバックアップを作成せずに現在のルールを消去します。既存のバックアップは保持されます。', backupDirectoryHelper: '{0} 内の有効なルールバックアップを表示します。既定では最新のファイルが選択されます。', + startOnly: '起動のみ', + startAndRestore: '起動してバックアップから復元', + startWithBackupHelper: '通常起動するか、既存の JSON バックアップを選んで起動とルールの復元を行います。', restoreAllRules: '初期化してルールを復元', importRuleBackup: 'バックアップからインポート', initializeOnly: '初期化のみ', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index d28177101..ddbc5891a 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -4204,9 +4204,12 @@ const message = { familyIncomplete: '{0} 체인이 불완전합니다', resetWithBackup: '규칙 백업 후 초기화', resetOnly: '초기화만 수행', - resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 시 복원할 수 있습니다.', + resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 또는 시작 시 복원할 수 있습니다.', resetOnlyHelper: '새 백업 없이 현재 규칙을 삭제합니다. 기존 백업은 유지됩니다.', backupDirectoryHelper: '{0}의 유효한 규칙 백업을 표시합니다. 기본적으로 최신 파일이 선택됩니다.', + startOnly: '시작만', + startAndRestore: '시작 및 백업에서 복원', + startWithBackupHelper: '방화벽을 일반적으로 시작하거나 기존 JSON 백업을 선택하여 시작하고 규칙을 복원합니다.', restoreAllRules: '초기화 및 규칙 복원', importRuleBackup: '백업에서 가져오기', initializeOnly: '초기화만 수행', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 92848cc4a..56d52e1f7 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -4173,9 +4173,12 @@ const message = { familyIncomplete: 'ຊຸດກົດ {0} ບໍ່ຄົບຖ້ວນ', resetWithBackup: 'ສຳຮອງກົດແລ້ວຣີເຊັດ', resetOnly: 'ຣີເຊັດເທົ່ານັ້ນ', - resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ.', + resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ ຫຼື ເປີດໃຊ້ໄຟວໍ.', resetOnlyHelper: 'ລຶບກົດປັດຈຸບັນໂດຍບໍ່ສ້າງໄຟລ໌ສຳຮອງໃໝ່. ໄຟລ໌ສຳຮອງເກົ່າຍັງຄົງຢູ່.', backupDirectoryHelper: 'ສະແດງໄຟລ໌ສຳຮອງກົດທີ່ໃຊ້ໄດ້ໃນ {0}. ເລືອກໄຟລ໌ຫຼ້າສຸດເປັນຄ່າເລີ່ມຕົ້ນ.', + startOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ', + startAndRestore: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ', + startWithBackupHelper: 'ເປີດໄຟວໍຕາມປົກກະຕິ ຫຼື ເລືອກຂໍ້ມູນສຳຮອງ JSON ທີ່ມີຢູ່ເພື່ອເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ.', restoreAllRules: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ', importRuleBackup: 'ນຳເຂົ້າຈາກຂໍ້ມູນສຳຮອງ', initializeOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6f21fc0a6..17a12c69e 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -4362,10 +4362,15 @@ const message = { familyIncomplete: 'Rantaian {0} tidak lengkap', resetWithBackup: 'Sandarkan peraturan dan tetapkan semula', resetOnly: 'Tetapkan semula sahaja', - resetWithBackupHelper: 'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan.', + resetWithBackupHelper: + 'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan atau semasa tembok api dimulakan.', resetOnlyHelper: 'Padam peraturan semasa tanpa sandaran baharu. Sandaran sedia ada dikekalkan.', backupDirectoryHelper: 'Sandaran peraturan yang sah dalam {0} disenaraikan di bawah. Fail terkini dipilih secara lalai.', + startOnly: 'Mula sahaja', + startAndRestore: 'Mula dan pulihkan daripada sandaran', + startWithBackupHelper: + 'Mulakan tembok api seperti biasa atau pilih sandaran JSON sedia ada untuk memulakan dan memulihkan peraturan.', restoreAllRules: 'Mulakan dan pulihkan peraturan', importRuleBackup: 'Import daripada sandaran', initializeOnly: 'Mulakan sahaja', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 6f56b8511..0d6792b74 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -4384,10 +4384,15 @@ const message = { familyIncomplete: 'As cadeias de {0} estão incompletas', resetWithBackup: 'Salvar regras e redefinir', resetOnly: 'Apenas redefinir', - resetWithBackupHelper: 'Salve as regras do módulo atual em JSON para restaurá-las durante a inicialização.', + resetWithBackupHelper: + 'Salve as regras do módulo atual em JSON para restaurá-las ao inicializar ou iniciar o firewall.', resetOnlyHelper: 'Limpar as regras atuais sem criar um novo backup. Os backups existentes são mantidos.', backupDirectoryHelper: 'Os backups válidos de regras em {0} são listados abaixo. O mais recente é selecionado por padrão.', + startOnly: 'Apenas iniciar', + startAndRestore: 'Iniciar e restaurar do backup', + startWithBackupHelper: + 'Inicie o firewall normalmente ou escolha um backup JSON existente para iniciar e restaurar regras.', restoreAllRules: 'Inicializar e restaurar regras', importRuleBackup: 'Importar do backup', initializeOnly: 'Apenas inicializar', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f99d2baa5..030858330 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -4350,10 +4350,15 @@ const message = { familyIncomplete: 'Цепочки {0} неполны', resetWithBackup: 'Сохранить правила и сбросить', resetOnly: 'Только сбросить', - resetWithBackupHelper: 'Сохраните правила текущего модуля в JSON для восстановления при инициализации.', + resetWithBackupHelper: + 'Сохраните правила текущего модуля в JSON для восстановления при инициализации или запуске.', resetOnlyHelper: 'Удалить текущие правила без создания новой копии. Существующие копии сохраняются.', backupDirectoryHelper: 'Ниже перечислены действительные резервные копии правил из {0}. По умолчанию выбрана самая новая.', + startOnly: 'Только запуск', + startAndRestore: 'Запустить и восстановить из резервной копии', + startWithBackupHelper: + 'Запустите брандмауэр как обычно или выберите существующую копию JSON для запуска и восстановления правил.', restoreAllRules: 'Инициализировать и восстановить правила', importRuleBackup: 'Импортировать из резервной копии', initializeOnly: 'Только инициализировать', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index fd11a002a..0299c505d 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -4368,10 +4368,15 @@ const message = { familyIncomplete: '{0} zincirleri eksik', resetWithBackup: 'Kuralları yedekle ve sıfırla', resetOnly: 'Yalnızca sıfırla', - resetWithBackupHelper: 'Geçerli modül kurallarını başlatma sırasında geri yüklemek için JSON olarak kaydedin.', + resetWithBackupHelper: + 'Geçerli modül kurallarını ilk kurulumda veya güvenlik duvarı başlatılırken geri yüklemek için JSON olarak kaydedin.', resetOnlyHelper: 'Yeni yedek oluşturmadan mevcut kuralları temizleyin. Önceki yedekler korunur.', backupDirectoryHelper: '{0} içindeki geçerli kural yedekleri listelenir. Varsayılan olarak en yeni dosya seçilir.', + startOnly: 'Yalnızca başlat', + startAndRestore: 'Başlat ve yedekten geri yükle', + startWithBackupHelper: + 'Güvenlik duvarını normal başlatın veya başlatıp kuralları geri yüklemek için mevcut bir JSON yedeği seçin.', restoreAllRules: 'Başlat ve kuralları geri yükle', importRuleBackup: 'Yedekten içe aktar', initializeOnly: 'Yalnızca başlat', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 51134e822..0e89b4273 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -4010,9 +4010,12 @@ const message = { familyIncomplete: '{0} 鏈不完整', resetWithBackup: '備份規則後重置', resetOnly: '僅重置', - resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化時選擇備份還原。', + resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化或啟動時選擇備份還原。', resetOnlyHelper: '不建立新備份,直接清理目前規則;保留既有歷史備份。', backupDirectoryHelper: '自動讀取 {0} 下的有效規則備份,預設選擇最新檔案。', + startOnly: '僅啟動', + startAndRestore: '啟動並從備份還原', + startWithBackupHelper: '可正常啟動防火牆,或選擇現有的 JSON 備份啟動並還原規則。', restoreAllRules: '初始化並還原規則', importRuleBackup: '從備份匯入', initializeOnly: '僅初始化', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 05f6d5627..8af92e703 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -4055,9 +4055,12 @@ const message = { familyIncomplete: '{0} 链不完整', resetWithBackup: '备份规则后重置', resetOnly: '仅重置', - resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化时选择备份恢复。', + resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化或启动时选择备份恢复。', resetOnlyHelper: '不生成新备份,直接清理当前规则;已有历史备份保留。', backupDirectoryHelper: '自动读取 {0} 下的有效规则备份,默认选择最新文件。', + startOnly: '仅启动', + startAndRestore: '启动并从备份恢复', + startWithBackupHelper: '可正常启动防火墙,或选择已有的 JSON 备份启动并恢复规则。', restoreAllRules: '初始化并恢复规则', importRuleBackup: '从备份导入', initializeOnly: '仅初始化', diff --git a/frontend/src/views/host/firewall/components/rule-dialog.scss b/frontend/src/views/host/firewall/components/rule-dialog.scss index f05f8d227..75f8f81ca 100644 --- a/frontend/src/views/host/firewall/components/rule-dialog.scss +++ b/frontend/src/views/host/firewall/components/rule-dialog.scss @@ -149,3 +149,97 @@ } } } + +.firewall-initialize-dialog.el-dialog, +.firewall-lifecycle-dialog.el-dialog { + display: flex; + flex-direction: column; + width: min(680px, calc(100vw - 32px)); + max-height: calc(92dvh - 24px); + margin-top: max(24px, 8vh); + margin-bottom: 24px; + + .el-dialog__body { + min-height: 0; + overflow-y: auto; + } + + .firewall-rule-dialog__content { + gap: 16px; + } + + .firewall-initialize-dialog__description { + margin: 0; + color: var(--el-text-color-secondary); + font-size: 13px; + line-height: 1.7; + } + + .firewall-rule-dialog__docker { + padding: 14px 16px; + border-radius: 8px; + background: var(--el-fill-color-light); + + .el-form-item { + margin-bottom: 0; + } + + .el-form-item__label { + height: auto; + margin-bottom: 8px; + font-weight: 500; + line-height: 1.6; + } + + .el-form-item__content { + display: block; + line-height: 1.6; + } + + .el-radio-group { + gap: 4px 24px; + } + + .el-radio { + height: auto; + min-height: 28px; + margin-right: 0; + white-space: normal; + } + } + + .firewall-rule-dialog__docker-helper { + margin: 6px 0 0; + color: var(--el-text-color-secondary); + font-size: 12px; + line-height: 1.7; + } + + .firewall-rule-dialog__confirmation { + padding: 14px 0 16px; + color: var(--el-text-color-regular); + text-align: left; + font-size: 13px; + line-height: 1.7; + } + + .firewall-rule-dialog__files { + max-height: min(240px, 30vh); + } + + .firewall-rule-dialog__modes { + align-items: stretch; + } + + .firewall-initialize-dialog__backups { + display: flex; + flex-direction: column; + gap: 12px; + border-top: 1px solid var(--el-border-color-lighter); + padding-top: 16px; + } + + .firewall-rule-dialog__file-name > .min-w-0 { + flex: 1; + } +} diff --git a/frontend/src/views/host/firewall/components/rule-initialize.vue b/frontend/src/views/host/firewall/components/rule-initialize.vue index fd73ce445..db02ed29a 100644 --- a/frontend/src/views/host/firewall/components/rule-initialize.vue +++ b/frontend/src/views/host/firewall/components/rule-initialize.vue @@ -1,12 +1,22 @@