Files
kor-elf-shield/internal/daemon/firewall/blocking/blocking.go
T

273 lines
6.8 KiB
Go

package blocking
import (
"fmt"
"net"
"strings"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
nftFirewall "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft/block"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type API interface {
NftReload(nft nftFirewall.NFT, blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error
BlockIP(block BlockIP) (bool, error)
BlockIPWithPorts(block BlockIPWithPorts) (bool, error)
UnblockAllIPs() error
UnblockIP(ip net.IP) error
ClearDBData() error
}
type blocking struct {
blockingRepository repository.BlockingRepository
blockListIP block.ListIP
blockListIPWithPort block.ListIPWithPort
logger log.Logger
mu sync.Mutex
}
type BlockIP struct {
IP net.IP
TimeSeconds uint32
Reason string
}
type BlockIPWithPorts struct {
IP net.IP
TimeSeconds uint32
Reason string
Ports []types.L4Port
}
func New(blockingRepository repository.BlockingRepository, logger log.Logger) API {
return &blocking{
blockingRepository: blockingRepository,
logger: logger,
mu: sync.Mutex{},
}
}
func (b *blocking) NftReload(nft nftFirewall.NFT, blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error {
b.mu.Lock()
b.blockListIP = blockListIP
b.blockListIPWithPort = blockListIPWithPort
b.mu.Unlock()
batchBuilder, err := nft.NewBuildBatch()
if err != nil {
return err
}
defer func() {
if err := batchBuilder.Close(); err != nil {
b.logger.Warn(err.Error())
}
}()
isExpiredEntries := false
nowUnix := time.Now().Unix()
err = b.blockingRepository.List(func(e entity.Blocking) error {
ip := net.ParseIP(e.IP)
if ip == nil {
b.logger.Error(fmt.Sprintf("Failed to parse IP address: %s", e.IP))
return nil
}
blockSeconds := uint32(0)
if e.ExpireAtUnix > 0 {
if e.ExpireAtUnix < nowUnix {
isExpiredEntries = true
return nil
}
blockSeconds = uint32(e.ExpireAtUnix - nowUnix)
}
if e.IsPorts() {
l4Ports, err := e.ToL4Ports()
if err != nil {
b.logger.Error(fmt.Sprintf("Failed to parse ports: %s", err))
return nil
}
if err := b.blockListIPWithPort.AddBatchIP(batchBuilder, ip, l4Ports, blockSeconds); err != nil {
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
}
return nil
}
if err := b.blockListIP.AddBatchIP(batchBuilder, ip, blockSeconds); err != nil {
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
return nil
}
return nil
})
if isExpiredEntries {
go func() {
deleteCount, err := b.blockingRepository.DeleteExpired(100)
if err != nil {
b.logger.Error(fmt.Sprintf("Failed to delete expired entries from database: %s", err))
}
b.logger.Debug(fmt.Sprintf("Deleted %d expired entries from database", deleteCount))
}()
}
if err != nil {
return err
}
return nft.RunBatch(batchBuilder)
}
func (b *blocking) BlockIP(block BlockIP) (bool, error) {
if block.IP.IsLoopback() {
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
}
if err := b.blockListIP.AddIP(block.IP, block.TimeSeconds); err != nil {
return false, err
}
expireAtUnix := int64(0)
if block.TimeSeconds > 0 {
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
expireAtUnix = expire.Unix()
}
data := entity.Blocking{
IP: block.IP.String(),
ExpireAtUnix: expireAtUnix,
Reason: block.Reason,
}
if err := b.blockingRepository.Add(data); err != nil {
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
}
return true, nil
}
func (b *blocking) BlockIPWithPorts(block BlockIPWithPorts) (bool, error) {
if block.IP.IsLoopback() {
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
}
if err := b.blockListIPWithPort.AddIP(block.IP, block.Ports, block.TimeSeconds); err != nil {
return false, err
}
var l4Ports []entity.BlockingPort
for _, port := range block.Ports {
l4Ports = append(l4Ports, entity.BlockingPort{
Number: port.Number(),
Protocol: port.ProtocolString(),
})
}
expireAtUnix := int64(0)
if block.TimeSeconds > 0 {
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
expireAtUnix = expire.Unix()
}
data := entity.Blocking{
IP: block.IP.String(),
ExpireAtUnix: expireAtUnix,
Reason: block.Reason,
Ports: l4Ports,
}
if err := b.blockingRepository.Add(data); err != nil {
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
}
return true, nil
}
func (b *blocking) UnblockIP(ip net.IP) error {
err := b.blockingRepository.DeleteByIP(ip, func(e entity.Blocking) error {
if e.IsPorts() {
l4Ports, err := e.ToL4Ports()
if err != nil {
return err
}
return b.removeIPWithPorts(ip, l4Ports)
}
if err := b.blockListIP.DeleteIP(ip); err != nil {
if strings.Contains(err.Error(), "element does not exist") {
return nil
}
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (b *blocking) UnblockAllIPs() error {
err := b.blockingRepository.List(func(e entity.Blocking) error {
ip := net.ParseIP(e.IP)
if ip == nil {
return fmt.Errorf("failed to parse IP address: %s", e.IP)
}
if e.IsPorts() {
l4Ports, err := e.ToL4Ports()
if err != nil {
return err
}
for _, port := range l4Ports {
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
if strings.Contains(err.Error(), "element does not exist") ||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
continue
}
return err
}
}
}
if err := b.blockListIP.DeleteIP(ip); err != nil {
if strings.Contains(err.Error(), "element does not exist") {
return nil
}
return err
}
return nil
})
if err != nil {
_ = b.blockingRepository.Clear()
return err
}
return b.blockingRepository.Clear()
}
func (b *blocking) ClearDBData() error {
return b.blockingRepository.Clear()
}
func (b *blocking) removeIPWithPorts(ip net.IP, l4Ports []types.L4Port) error {
for _, port := range l4Ports {
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
if strings.Contains(err.Error(), "element does not exist") ||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
continue
}
return err
}
}
return nil
}