mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 15:14:03 +03:00
FILE / ScuroNeko/mtg
mtglib/internal/doppel/ganger.go
Исходный файл и его история в репозитории.
This is a followup for https://github.com/9seconds/mtg/issues/412 it makes sense to manage timers inplace instead of creating for new goroutines: saves memory
268 lines
5.0 KiB
Go
268 lines
5.0 KiB
Go
package doppel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/9seconds/mtg/v2/essentials"
|
|
)
|
|
|
|
const (
|
|
DoppelGangerMaxDurations = 4096
|
|
DoppelGangerScoutRaidEach = 6 * time.Hour
|
|
DoppelGangerScoutRepeats = 10
|
|
|
|
MinCertSizesToCalculate = 3
|
|
)
|
|
|
|
// NoiseParams holds the measured cert chain size for FakeTLS noise calibration.
|
|
// If Mean is 0, the caller should use a legacy fallback.
|
|
type NoiseParams struct {
|
|
Mean int
|
|
Jitter int
|
|
}
|
|
|
|
type scoutRaidResult struct {
|
|
durations []time.Duration
|
|
certSizes []int
|
|
}
|
|
|
|
type gangerConnRequest struct {
|
|
ret chan<- Conn
|
|
payload essentials.Conn
|
|
}
|
|
|
|
type Ganger struct {
|
|
ctx context.Context
|
|
ctxCancel context.CancelFunc
|
|
logger Logger
|
|
wg sync.WaitGroup
|
|
|
|
scout Scout
|
|
scoutRaidEach time.Duration
|
|
scoutRaidRepeats int
|
|
|
|
drs bool
|
|
|
|
stats Stats
|
|
durations []time.Duration
|
|
certSizes []int
|
|
|
|
noiseParams atomic.Pointer[NoiseParams]
|
|
|
|
connRequests chan gangerConnRequest
|
|
}
|
|
|
|
func (g *Ganger) Shutdown() {
|
|
g.ctxCancel()
|
|
g.wg.Wait()
|
|
}
|
|
|
|
func (g *Ganger) Run() {
|
|
g.wg.Go(func() {
|
|
g.run()
|
|
})
|
|
}
|
|
|
|
// NoiseParams returns the current cert-size-based noise parameters.
|
|
// Returns zero-value NoiseParams if not yet measured (caller should use fallback).
|
|
func (g *Ganger) NoiseParams() NoiseParams {
|
|
if p := g.noiseParams.Load(); p != nil {
|
|
return *p
|
|
}
|
|
|
|
return NoiseParams{}
|
|
}
|
|
|
|
func (g *Ganger) NewConn(conn essentials.Conn) (Conn, error) {
|
|
rvChan := make(chan Conn)
|
|
req := gangerConnRequest{
|
|
ret: rvChan,
|
|
payload: conn,
|
|
}
|
|
defer close(req.ret)
|
|
|
|
select {
|
|
case <-g.ctx.Done():
|
|
return Conn{}, context.Cause(g.ctx)
|
|
case g.connRequests <- req:
|
|
}
|
|
|
|
select {
|
|
case <-g.ctx.Done():
|
|
return Conn{}, context.Cause(g.ctx)
|
|
case conn := <-rvChan:
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
func (g *Ganger) run() {
|
|
scoutTicker := time.NewTicker(g.scoutRaidEach)
|
|
defer func() {
|
|
scoutTicker.Stop()
|
|
|
|
select {
|
|
case <-scoutTicker.C:
|
|
default:
|
|
}
|
|
}()
|
|
|
|
scoutCollectedChan := make(chan scoutRaidResult)
|
|
currentScoutCollectedChan := scoutCollectedChan
|
|
|
|
updatedStatsChan := make(chan Stats)
|
|
|
|
g.wg.Go(func() {
|
|
g.runScoutRaid(scoutCollectedChan)
|
|
})
|
|
|
|
for {
|
|
select {
|
|
case <-g.ctx.Done():
|
|
return
|
|
case result := <-currentScoutCollectedChan:
|
|
g.durations = append(g.durations, result.durations...)
|
|
|
|
if len(g.durations) > DoppelGangerMaxDurations {
|
|
copy(g.durations, g.durations[len(g.durations)-DoppelGangerMaxDurations:])
|
|
g.durations = g.durations[:DoppelGangerMaxDurations]
|
|
}
|
|
|
|
// Update cert sizes and recompute noise params.
|
|
g.certSizes = append(g.certSizes, result.certSizes...)
|
|
if len(g.certSizes) > DoppelGangerMaxDurations {
|
|
g.certSizes = g.certSizes[len(g.certSizes)-DoppelGangerMaxDurations:]
|
|
}
|
|
|
|
if len(g.certSizes) >= MinCertSizesToCalculate {
|
|
g.updateNoiseParams()
|
|
}
|
|
|
|
if len(g.durations) < MinDurationsToCalculate {
|
|
continue
|
|
}
|
|
|
|
durations := g.durations
|
|
currentScoutCollectedChan = nil
|
|
g.wg.Go(func() {
|
|
select {
|
|
case <-g.ctx.Done():
|
|
case updatedStatsChan <- NewStats(durations, g.drs):
|
|
}
|
|
})
|
|
case stats := <-updatedStatsChan:
|
|
g.stats = stats
|
|
currentScoutCollectedChan = scoutCollectedChan
|
|
case <-scoutTicker.C:
|
|
g.wg.Go(func() {
|
|
g.runScoutRaid(scoutCollectedChan)
|
|
})
|
|
case req := <-g.connRequests:
|
|
select {
|
|
case <-g.ctx.Done():
|
|
case req.ret <- NewConn(g.ctx, req.payload, g.stats):
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Ganger) updateNoiseParams() {
|
|
if len(g.certSizes) == 0 {
|
|
return
|
|
}
|
|
|
|
sum := 0
|
|
for _, s := range g.certSizes {
|
|
sum += s
|
|
}
|
|
|
|
mean := sum / len(g.certSizes)
|
|
|
|
maxDev := 0
|
|
for _, s := range g.certSizes {
|
|
d := s - mean
|
|
if d < 0 {
|
|
d = -d
|
|
}
|
|
|
|
if d > maxDev {
|
|
maxDev = d
|
|
}
|
|
}
|
|
|
|
if maxDev < 100 {
|
|
maxDev = 100
|
|
}
|
|
|
|
np := &NoiseParams{Mean: mean, Jitter: maxDev}
|
|
g.noiseParams.Store(np)
|
|
|
|
g.logger.Info(fmt.Sprintf(
|
|
"updated noise params: mean=%d jitter=%d samples=%d",
|
|
mean, maxDev, len(g.certSizes),
|
|
))
|
|
}
|
|
|
|
func (g *Ganger) runScoutRaid(rvChan chan<- scoutRaidResult) {
|
|
var result scoutRaidResult
|
|
|
|
for range g.scoutRaidRepeats {
|
|
learned, err := g.scout.Learn(g.ctx)
|
|
if err != nil {
|
|
g.logger.WarningError("cannot learn", err)
|
|
continue
|
|
}
|
|
|
|
result.durations = append(result.durations, learned.Durations...)
|
|
|
|
if learned.CertSize > 0 {
|
|
result.certSizes = append(result.certSizes, learned.CertSize)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-g.ctx.Done():
|
|
return
|
|
case rvChan <- result:
|
|
}
|
|
}
|
|
|
|
func NewGanger(
|
|
ctx context.Context,
|
|
network Network,
|
|
logger Logger,
|
|
scoutEach time.Duration,
|
|
scoutRepeats int,
|
|
urls []string,
|
|
drs bool,
|
|
) *Ganger {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
if scoutEach == 0 {
|
|
scoutEach = DoppelGangerScoutRaidEach
|
|
}
|
|
|
|
if scoutRepeats == 0 {
|
|
scoutRepeats = DoppelGangerScoutRepeats
|
|
}
|
|
|
|
return &Ganger{
|
|
ctx: ctx,
|
|
ctxCancel: cancel,
|
|
logger: logger,
|
|
scoutRaidEach: scoutEach,
|
|
scoutRaidRepeats: scoutRepeats,
|
|
drs: drs,
|
|
stats: Stats{
|
|
k: StatsDefaultK,
|
|
lambda: StatsDefaultLambda,
|
|
drs: drs,
|
|
},
|
|
scout: NewScout(network, urls),
|
|
connRequests: make(chan gangerConnRequest),
|
|
}
|
|
}
|