Files

284 lines
6.4 KiB
Go

package firewall
import (
"context"
"errors"
"fmt"
"net"
"regexp"
"sync"
"time"
"UAmask/internal/clock"
"github.com/sirupsen/logrus"
)
var validSetName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
var ErrExecutorStopped = errors.New("firewall executor stopped")
type Applier interface {
ApplyBatch(items []BypassTarget) error
}
type completionFunc func(error)
type queuedTarget struct {
target BypassTarget
completion completionFunc
flush chan struct{}
}
type pendingTarget struct {
target BypassTarget
completions []completionFunc
}
type Executor struct {
queue chan queuedTarget
stopChan chan struct{}
stopOnce sync.Once
waitGroup sync.WaitGroup
lifecycleMu sync.RWMutex
stopped bool
log *logrus.Logger
applier Applier
maxBatchSize int
maxBatchWait time.Duration
clock clock.Clock
}
func NewExecutor(log *logrus.Logger, queueSize int, applier Applier) *Executor {
return NewExecutorWithClock(log, queueSize, applier, clock.RealClock{})
}
func NewExecutorWithClock(log *logrus.Logger, queueSize int, applier Applier, runtimeClock clock.Clock) *Executor {
return &Executor{
queue: make(chan queuedTarget, queueSize),
stopChan: make(chan struct{}),
log: log,
applier: applier,
maxBatchSize: 200,
maxBatchWait: 100 * time.Millisecond,
clock: clock.OrReal(runtimeClock),
}
}
func (e *Executor) Start() {
e.waitGroup.Add(1)
go e.worker()
e.log.Info("[FirewallExecutor] worker started")
}
func (e *Executor) Stop() {
e.stopOnce.Do(func() {
e.lifecycleMu.Lock()
e.stopped = true
close(e.stopChan)
e.lifecycleMu.Unlock()
})
e.waitGroup.Wait()
e.log.Info("[FirewallExecutor] worker stopped")
}
func (e *Executor) Enqueue(target BypassTarget) {
_ = e.TryEnqueue(target)
}
func (e *Executor) TryEnqueue(target BypassTarget) bool {
return e.TryEnqueueWithResult(target, nil)
}
func (e *Executor) TryEnqueueWithResult(target BypassTarget, completion func(error)) bool {
if !isValidTarget(target) {
e.log.Warnf("[FirewallExecutor] Invalid bypass target: %+v", target)
return false
}
e.lifecycleMu.RLock()
defer e.lifecycleMu.RUnlock()
if e.stopped {
return false
}
select {
case e.queue <- queuedTarget{target: target, completion: completion}:
return true
case <-e.clock.After(50 * time.Millisecond):
e.log.Warnf("[FirewallExecutor] queue full, dropping target for %s:%d", target.IP, target.Port)
return false
}
}
func (e *Executor) Flush(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
done := make(chan struct{})
e.lifecycleMu.RLock()
if e.stopped {
e.lifecycleMu.RUnlock()
return ErrExecutorStopped
}
select {
case e.queue <- queuedTarget{flush: done}:
e.lifecycleMu.RUnlock()
case <-ctx.Done():
e.lifecycleMu.RUnlock()
return ctx.Err()
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (e *Executor) worker() {
defer e.waitGroup.Done()
batches := make(map[string]map[string]*pendingTarget)
batchTimer := e.clock.NewTimer(e.maxBatchWait)
stopAndDrainTimer(batchTimer)
for {
select {
case queued := <-e.queue:
if queued.flush != nil {
e.executeBatches(batches)
batches = make(map[string]map[string]*pendingTarget)
stopAndDrainTimer(batchTimer)
close(queued.flush)
continue
}
key := e.addToBatch(batches, queued)
if len(batches) == 1 && len(batches[key]) == 1 {
batchTimer.Reset(e.maxBatchWait)
}
if len(batches[key]) >= e.maxBatchSize {
e.executeBatches(batches)
batches = make(map[string]map[string]*pendingTarget)
stopAndDrainTimer(batchTimer)
}
case <-batchTimer.C():
if len(batches) > 0 {
e.executeBatches(batches)
batches = make(map[string]map[string]*pendingTarget)
}
case <-e.stopChan:
batches = e.drainQueue(batches)
e.executeBatches(batches)
stopAndDrainTimer(batchTimer)
return
}
}
}
func (e *Executor) drainQueue(batches map[string]map[string]*pendingTarget) map[string]map[string]*pendingTarget {
for {
select {
case queued := <-e.queue:
if queued.flush != nil {
e.executeBatches(batches)
batches = make(map[string]map[string]*pendingTarget)
close(queued.flush)
continue
}
e.addToBatch(batches, queued)
default:
return batches
}
}
}
func stopAndDrainTimer(timer clock.Timer) {
if timer.Stop() {
return
}
select {
case <-timer.C():
default:
}
}
func (e *Executor) addToBatch(batches map[string]map[string]*pendingTarget, queued queuedTarget) string {
key := batchKey(queued.target)
dedupKey := fmt.Sprintf("%s:%d", queued.target.IP, queued.target.Port)
if _, ok := batches[key]; !ok {
batches[key] = make(map[string]*pendingTarget)
}
pending, ok := batches[key][dedupKey]
if !ok {
pending = &pendingTarget{}
batches[key][dedupKey] = pending
}
pending.target = queued.target
if queued.completion != nil {
pending.completions = append(pending.completions, queued.completion)
}
return key
}
func (e *Executor) executeBatches(batches map[string]map[string]*pendingTarget) {
if len(batches) == 0 {
return
}
for key, itemsMap := range batches {
if len(itemsMap) == 0 {
continue
}
items := make([]BypassTarget, 0, len(itemsMap))
pendingItems := make([]*pendingTarget, 0, len(itemsMap))
for _, pending := range itemsMap {
items = append(items, pending.target)
pendingItems = append(pendingItems, pending)
}
err := e.applier.ApplyBatch(items)
if err != nil {
first := items[0]
e.log.Warnf("[FirewallExecutor] Failed batch %s for set %s (%s): %v", key, first.SetName, first.Backend, err)
} else {
first := items[0]
e.log.Debugf("[FirewallExecutor] Added %d unique targets to set %s (%s)", len(items), first.SetName, first.Backend)
}
for _, pending := range pendingItems {
for _, completion := range pending.completions {
e.notifyCompletion(completion, err)
}
}
}
}
func (e *Executor) notifyCompletion(completion completionFunc, err error) {
defer func() {
if recovered := recover(); recovered != nil {
e.log.Errorf("[FirewallExecutor] completion callback panicked: %v", recovered)
}
}()
completion(err)
}
func batchKey(target BypassTarget) string {
return fmt.Sprintf("%s:%s", target.Backend, target.SetName)
}
func isValidTarget(target BypassTarget) bool {
if target.IP == "" || target.SetName == "" {
return false
}
if net.ParseIP(target.IP) == nil {
return false
}
return validSetName.MatchString(target.SetName)
}