Files
1Panel/agent/utils/firewall/lifecycle/providers/firewalld.go
ssongliu 782bc1e67c fix(firewall): manage SSH access and queue stop operations (#13831)
* fix(firewall): manage SSH access and queue stop operations

* fix(firewall): reconcile whitelist rules and sync differences
2026-09-16 16:45:33 +08:00

215 lines
5.7 KiB
Go

package providers
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
type Firewalld struct{}
const firewalldConfigDir = "/etc/firewalld"
var firewalldConfigSubdirectories = []string{
"helpers",
"icmptypes",
"ipsets",
"policies",
"services",
"zones",
}
func NewFirewalld() (*Firewalld, error) {
return &Firewalld{}, nil
}
func (f *Firewalld) Name() string {
return "firewalld"
}
func (f *Firewalld) Status() (bool, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--state")
if err != nil {
if firewalldStopped(stdout, err) {
return false, nil
}
return false, fmt.Errorf("load firewall status failed: %w", err)
}
return strings.TrimSpace(stdout) == "running", nil
}
func firewalldStopped(stdout string, err error) bool {
message := strings.ToLower(strings.TrimSpace(stdout))
if err != nil {
message += " " + strings.ToLower(err.Error())
}
return strings.Contains(message, "not running")
}
func (f *Firewalld) Version() (string, error) {
stdout, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", "--version")
if err != nil {
return "", fmt.Errorf("load the firewall version failed, %v", err)
}
return strings.ReplaceAll(stdout, "\n ", ""), nil
}
func (f *Firewalld) Start() error {
if err := controller.HandleStart("firewalld"); err != nil {
return fmt.Errorf("enable the firewall failed, err: %v", err)
}
return nil
}
func RemoveFirewalldSSHService() error {
for _, permanent := range []bool{true, false} {
args := []string{"--zone=" + filter.FirewalldInputZone, "--remove-service=ssh"}
configuration := "runtime"
if permanent {
args = append(args, "--permanent")
configuration = "permanent"
}
if _, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", args...); err != nil {
return fmt.Errorf("remove firewalld SSH service from %s configuration: %w", configuration, err)
}
}
return nil
}
func (f *Firewalld) Stop() error {
if err := controller.HandleStop("firewalld"); err != nil {
return fmt.Errorf("stop the firewall failed, err: %v", err)
}
return nil
}
func (f *Firewalld) Reset() error {
if err := f.ResetBeforeStop(); err != nil {
return err
}
if err := f.Stop(); err != nil {
return fmt.Errorf("stop firewalld after reset failed: %w", err)
}
return nil
}
func (f *Firewalld) ResetBeforeStop() error {
backupDir := fmt.Sprintf("%s.1panel-backup-%d", firewalldConfigDir, time.Now().UnixNano())
rollback, err := replaceFirewalldConfig(
firewalldConfigDir,
backupDir,
restoreFirewalldConfigContext,
validateFirewalldConfig,
)
if err != nil {
return fmt.Errorf("reset firewalld to defaults failed: %w", err)
}
if err := controller.Handle("disable", "firewalld"); err != nil {
return fmt.Errorf(
"reset firewalld to defaults failed: %w",
errors.Join(err, rollback()),
)
}
return nil
}
func replaceFirewalldConfig(
configDir string,
backupDir string,
prepare func(string) error,
validate func() error,
) (func() error, error) {
info, err := os.Lstat(configDir)
hadConfig := err == nil
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("inspect firewalld configuration: %w", err)
}
if hadConfig && !info.IsDir() {
return nil, fmt.Errorf("firewalld configuration path %s is not a directory", configDir)
}
if hadConfig {
if _, err := os.Lstat(backupDir); err == nil {
return nil, fmt.Errorf("firewalld backup path already exists: %s", backupDir)
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("inspect firewalld backup path: %w", err)
}
if err := os.Rename(configDir, backupDir); err != nil {
return nil, fmt.Errorf("back up firewalld configuration: %w", err)
}
}
rollback := func() error {
removeErr := os.RemoveAll(configDir)
if !hadConfig {
return removeErr
}
restoreErr := os.Rename(backupDir, configDir)
return errors.Join(removeErr, restoreErr)
}
fail := func(cause error) (func() error, error) {
if rollbackErr := rollback(); rollbackErr != nil {
return nil, errors.Join(cause, fmt.Errorf("rollback firewalld configuration: %w", rollbackErr))
}
return nil, cause
}
mode := os.FileMode(0755)
if hadConfig {
mode = info.Mode().Perm()
}
if err := os.Mkdir(configDir, mode); err != nil {
return fail(fmt.Errorf("create firewalld configuration directory: %w", err))
}
for _, directory := range firewalldConfigSubdirectories {
if err := os.Mkdir(filepath.Join(configDir, directory), 0755); err != nil {
return fail(fmt.Errorf("create firewalld %s directory: %w", directory, err))
}
}
if prepare != nil {
if err := prepare(configDir); err != nil {
return fail(fmt.Errorf("prepare firewalld configuration directory: %w", err))
}
}
if validate != nil {
if err := validate(); err != nil {
return fail(fmt.Errorf("validate default firewalld configuration: %w", err))
}
}
return rollback, nil
}
func restoreFirewalldConfigContext(configDir string) error {
if _, err := exec.LookPath("restorecon"); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return nil
}
return err
}
_, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithOptionalSudoAndStdout(
"restorecon", "-RF", configDir,
)
return err
}
func validateFirewalldConfig() error {
_, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithOptionalSudoAndStdout(
"firewall-offline-cmd", "--get-zones",
)
return err
}
func (f *Firewalld) Restart() error {
if err := controller.HandleRestart("firewalld"); err != nil {
return fmt.Errorf("restart the firewall failed, err: %v", err)
}
return nil
}