package lifecycle import ( "errors" "fmt" "os" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers" ) const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall" type Operation string const ( OperationStart Operation = "start" OperationStop Operation = "stop" OperationRestart Operation = "restart" ) type Operator struct { client Client RunAction func(operation, name string, action func() error) error } // DockerRestartError reports that the requested firewall operation completed, // but rebuilding Docker's firewall rules failed. type DockerRestartError struct { Err error } func (e *DockerRestartError) Error() string { return fmt.Sprintf("failed to restart Docker: %v", e.Err) } func (e *DockerRestartError) Unwrap() error { return e.Err } type CompletedOperationError struct { Operation Operation Err error } func (e *CompletedOperationError) Error() string { return fmt.Sprintf("firewall %s completed with recovery errors: %v", e.Operation, e.Err) } func (e *CompletedOperationError) Unwrap() error { return e.Err } func NewOperator(client Client) *Operator { return &Operator{client: client} } func (o *Operator) runAction(operation, name string, action func() error) error { if o.RunAction != nil { return o.RunAction(operation, name, action) } return action() } func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error { var recoveryErrors []error switch operation { case OperationStart: if err := o.runAction("Start", o.client.Name(), o.client.Start); err != nil { return err } if prepareStart != nil { if err := o.prepareAfterStart(prepareStart); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after start: %w", err)) } } case OperationStop: return o.StopWithPrepare(withDockerRestart, nil) case OperationRestart: if err := o.runAction("TaskRestart", o.client.Name(), o.client.Restart); err != nil { return err } if prepareStart != nil { if err := o.prepareAfterStart(prepareStart); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after restart: %w", err)) } } default: return fmt.Errorf("not supported operation: %s", operation) } if withDockerRestart { if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil { recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err}) } } if o.client.Name() == ProviderFirewalld && operation == OperationStart { if err := o.runAction("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil { recoveryErrors = append(recoveryErrors, err) } } if err := errors.Join(recoveryErrors...); err != nil { return &CompletedOperationError{Operation: operation, Err: err} } return nil } func (o *Operator) prepareAfterStart(prepare func(Client) error) error { if err := prepare(o.client); err != nil { return err } if o.client.Name() == ProviderFirewalld { return providers.RemoveFirewalldSSHService() } return nil } // StopWithPrepare records dependent service state, runs preparation, stops the // firewall, and optionally restarts Docker in that order. func (o *Operator) StopWithPrepare(withDockerRestart bool, prepareStop func() error) error { if o.client.Name() == ProviderFirewalld { if err := rememberFail2BanBeforeFirewallStop(); err != nil { return err } } if prepareStop != nil { if err := prepareStop(); err != nil { return err } } if err := o.runAction("Stop", o.client.Name(), o.client.Stop); err != nil { return err } if withDockerRestart { if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil { return &DockerRestartError{Err: err} } } return nil } func rememberFail2BanBeforeFirewallStop() error { exists, err := controller.CheckExist("fail2ban.service") if err != nil { global.LOG.Warnf("check fail2ban.service installation before stopping the firewall failed: %v", err) } if !exists { return nil } active, err := controller.CheckActive("fail2ban.service") if err != nil { global.LOG.Warnf("check fail2ban.service status before stopping the firewall failed: %v", err) } if !active { return nil } if err := os.WriteFile(fail2BanRestoreWithFirewallMarker, nil, 0600); err != nil { return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err) } return nil } func restoreFail2BanAfterFirewallStart() error { if _, err := os.Stat(fail2BanRestoreWithFirewallMarker); err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err) } if err := controller.HandleStart("fail2ban.service"); err != nil { return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err) } if err := os.Remove(fail2BanRestoreWithFirewallMarker); err != nil && !os.IsNotExist(err) { return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err) } return nil }