You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1Panel/backend/app/service/fail2ban.go

252 lines
6.2 KiB

package service
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/utils/firewall"
"github.com/1Panel-dev/1Panel/backend/utils/toolbox"
)
const defaultFail2BanPath = "/etc/fail2ban/jail.local"
type Fail2BanService struct{}
type IFail2BanService interface {
LoadBaseInfo() (dto.Fail2BanBaseInfo, error)
Search(search dto.Fail2BanSearch) ([]string, error)
Operate(operation string) error
OperateSSHD(req dto.Fail2BanSet) error
UpdateConf(req dto.Fail2BanUpdate) error
UpdateConfByFile(req dto.UpdateByFile) error
}
func NewIFail2BanService() IFail2BanService {
return &Fail2BanService{}
}
func (u *Fail2BanService) LoadBaseInfo() (dto.Fail2BanBaseInfo, error) {
var baseInfo dto.Fail2BanBaseInfo
client, err := toolbox.NewFail2Ban()
if err != nil {
return baseInfo, err
}
baseInfo.IsEnable, baseInfo.IsActive, baseInfo.IsExist = client.Status()
if !baseInfo.IsActive {
baseInfo.Version = "-"
} else {
baseInfo.Version = client.Version()
}
conf, err := os.ReadFile(defaultFail2BanPath)
if err != nil {
if baseInfo.IsActive {
return baseInfo, fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err)
} else {
return baseInfo, nil
}
}
lines := strings.Split(string(conf), "\n")
block := ""
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "[default]") {
block = "default"
continue
}
if strings.HasPrefix(line, "[sshd]") {
block = "sshd"
continue
}
if strings.HasPrefix(line, "[") {
block = ""
continue
}
if block != "default" && block != "sshd" {
continue
}
loadFailValue(line, &baseInfo)
}
return baseInfo, nil
}
func (u *Fail2BanService) Search(req dto.Fail2BanSearch) ([]string, error) {
var list []string
client, err := toolbox.NewFail2Ban()
if err != nil {
return nil, err
}
if req.Status == "banned" {
list, err = client.ListBanned()
} else {
list, err = client.ListIgnore()
}
if err != nil {
return nil, err
}
return list, nil
}
func (u *Fail2BanService) Operate(operation string) error {
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
return client.Operate(operation)
}
func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error {
if req.Key == "banaction" {
if req.Value == "firewallcmd-ipset" || req.Value == "ufw" {
itemName := "ufw"
if req.Value == "firewallcmd-ipset" {
itemName = "firewalld"
}
client, err := firewall.NewFirewallClient()
if err != nil {
return err
}
if client.Name() != itemName {
return buserr.WithName("ErrBanAction", itemName)
}
status, _ := client.Status()
if status != "running" {
return buserr.WithName("ErrBanAction", itemName)
}
}
}
if req.Key == "logpath" {
if _, err := os.Stat(req.Value); err != nil {
return err
}
}
conf, err := os.ReadFile(defaultFail2BanPath)
if err != nil {
return fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err)
}
lines := strings.Split(string(conf), "\n")
isStart, isEnd, hasKey := false, false, false
newFile := ""
for index, line := range lines {
if !isStart && strings.HasPrefix(line, "[sshd]") {
isStart = true
newFile += fmt.Sprintf("%s\n", line)
continue
}
if !isStart || isEnd {
newFile += fmt.Sprintf("%s\n", line)
continue
}
if strings.HasPrefix(line, req.Key) {
hasKey = true
newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value)
continue
}
if strings.HasPrefix(line, "[") || index == len(lines)-1 {
isEnd = true
if !hasKey {
newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value)
}
}
newFile += line
if index != len(lines)-1 {
newFile += "\n"
}
}
file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(newFile)
write.Flush()
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.Operate("restart"); err != nil {
return err
}
return nil
}
func (u *Fail2BanService) UpdateConfByFile(req dto.UpdateByFile) error {
file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.File)
write.Flush()
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.Operate("restart"); err != nil {
return err
}
return nil
}
func (u *Fail2BanService) OperateSSHD(req dto.Fail2BanSet) error {
if req.Operate == "ignore" {
if err := u.UpdateConf(dto.Fail2BanUpdate{Key: "ignoreip", Value: strings.Join(req.IPs, ",")}); err != nil {
return err
}
return nil
}
client, err := toolbox.NewFail2Ban()
if err != nil {
return err
}
if err := client.ReBanIPs(req.IPs); err != nil {
return err
}
return nil
}
func loadFailValue(line string, baseInfo *dto.Fail2BanBaseInfo) {
if strings.HasPrefix(line, "port") {
itemValue := strings.ReplaceAll(line, "port", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.Port, _ = strconv.Atoi(strings.TrimSpace(itemValue))
}
if strings.HasPrefix(line, "maxretry") {
itemValue := strings.ReplaceAll(line, "maxretry", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.MaxRetry, _ = strconv.Atoi(strings.TrimSpace(itemValue))
}
if strings.HasPrefix(line, "findtime") {
itemValue := strings.ReplaceAll(line, "findtime", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.FindTime = strings.TrimSpace(itemValue)
}
if strings.HasPrefix(line, "bantime") {
itemValue := strings.ReplaceAll(line, "bantime", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.BanTime = strings.TrimSpace(itemValue)
}
if strings.HasPrefix(line, "banaction") {
itemValue := strings.ReplaceAll(line, "banaction", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.BanAction = strings.TrimSpace(itemValue)
}
if strings.HasPrefix(line, "logpath") {
itemValue := strings.ReplaceAll(line, "logpath", "")
itemValue = strings.ReplaceAll(itemValue, "=", "")
baseInfo.LogPath = strings.TrimSpace(itemValue)
}
}