mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
284 lines
6.4 KiB
Go
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)
|
|
}
|