wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
166
spn/terminal/session.go
Normal file
166
spn/terminal/session.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
const (
|
||||
rateLimitMinOps = 250
|
||||
rateLimitMaxOpsPerSecond = 5
|
||||
|
||||
rateLimitMinSuspicion = 25
|
||||
rateLimitMinPermaSuspicion = rateLimitMinSuspicion * 100
|
||||
rateLimitMaxSuspicionPerSecond = 1
|
||||
|
||||
// Make this big enough to trigger suspicion limit in first blast.
|
||||
concurrencyPoolSize = 30
|
||||
)
|
||||
|
||||
// Session holds terminal metadata for operations.
|
||||
type Session struct {
|
||||
sync.RWMutex
|
||||
|
||||
// Rate Limiting.
|
||||
|
||||
// started holds the unix timestamp in seconds when the session was started.
|
||||
// It is set when the Session is created and may be treated as a constant.
|
||||
started int64
|
||||
|
||||
// opCount is the amount of operations started (and not rate limited by suspicion).
|
||||
opCount atomic.Int64
|
||||
|
||||
// suspicionScore holds a score of suspicious activity.
|
||||
// Every suspicious operations is counted as at least 1.
|
||||
// Rate limited operations because of suspicion are also counted as 1.
|
||||
suspicionScore atomic.Int64
|
||||
|
||||
concurrencyPool chan struct{}
|
||||
}
|
||||
|
||||
// SessionTerminal is an interface for terminals that support authorization.
|
||||
type SessionTerminal interface {
|
||||
GetSession() *Session
|
||||
}
|
||||
|
||||
// SessionAddOn can be inherited by terminals to add support for sessions.
|
||||
type SessionAddOn struct {
|
||||
lock sync.Mutex
|
||||
|
||||
// session holds the terminal session.
|
||||
session *Session
|
||||
}
|
||||
|
||||
// GetSession returns the terminal's session.
|
||||
func (t *SessionAddOn) GetSession() *Session {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
// Create session if it does not exist.
|
||||
if t.session == nil {
|
||||
t.session = NewSession()
|
||||
}
|
||||
|
||||
return t.session
|
||||
}
|
||||
|
||||
// NewSession returns a new session.
|
||||
func NewSession() *Session {
|
||||
return &Session{
|
||||
started: time.Now().Unix() - 1, // Ensure a 1 second difference to current time.
|
||||
concurrencyPool: make(chan struct{}, concurrencyPoolSize),
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimitInfo returns some basic information about the status of the rate limiter.
|
||||
func (s *Session) RateLimitInfo() string {
|
||||
secondsActive := time.Now().Unix() - s.started
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%do/s %ds/s %ds",
|
||||
s.opCount.Load()/secondsActive,
|
||||
s.suspicionScore.Load()/secondsActive,
|
||||
secondsActive,
|
||||
)
|
||||
}
|
||||
|
||||
// RateLimit enforces a rate and suspicion limit.
|
||||
func (s *Session) RateLimit() *Error {
|
||||
secondsActive := time.Now().Unix() - s.started
|
||||
|
||||
// Check the suspicion limit.
|
||||
score := s.suspicionScore.Load()
|
||||
if score > rateLimitMinSuspicion {
|
||||
scorePerSecond := score / secondsActive
|
||||
if scorePerSecond >= rateLimitMaxSuspicionPerSecond {
|
||||
// Add current try to suspicion score.
|
||||
s.suspicionScore.Add(1)
|
||||
|
||||
return ErrRateLimited
|
||||
}
|
||||
|
||||
// Permanently rate limit if suspicion goes over the perma min limit and
|
||||
// the suspicion score is greater than 80% of the operation count.
|
||||
if score > rateLimitMinPermaSuspicion &&
|
||||
score*5 > s.opCount.Load()*4 { // Think: 80*5 == 100*4
|
||||
return ErrRateLimited
|
||||
}
|
||||
}
|
||||
|
||||
// Check the rate limit.
|
||||
count := s.opCount.Add(1)
|
||||
if count > rateLimitMinOps {
|
||||
opsPerSecond := count / secondsActive
|
||||
if opsPerSecond >= rateLimitMaxOpsPerSecond {
|
||||
return ErrRateLimited
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Suspicion Factors.
|
||||
const (
|
||||
SusFactorCommon = 1
|
||||
SusFactorWeirdButOK = 5
|
||||
SusFactorQuiteUnusual = 10
|
||||
SusFactorMustBeMalicious = 100
|
||||
)
|
||||
|
||||
// ReportSuspiciousActivity reports suspicious activity of the terminal.
|
||||
func (s *Session) ReportSuspiciousActivity(factor int64) {
|
||||
s.suspicionScore.Add(factor)
|
||||
}
|
||||
|
||||
// LimitConcurrency limits concurrent executions.
|
||||
// If over the limit, waiting goroutines are selected randomly.
|
||||
// It returns the context error if it was canceled.
|
||||
func (s *Session) LimitConcurrency(ctx context.Context, f func()) error {
|
||||
// Wait for place in pool.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case s.concurrencyPool <- struct{}{}:
|
||||
// We added our entry to the pool, continue with execution.
|
||||
}
|
||||
|
||||
// Drain own spot if pool after execution.
|
||||
defer func() {
|
||||
select {
|
||||
case <-s.concurrencyPool:
|
||||
// Own entry drained.
|
||||
default:
|
||||
// This should never happen, but let's play safe and not deadlock when pool is empty.
|
||||
log.Warningf("spn/session: failed to drain own entry from concurrency pool")
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute and return.
|
||||
f()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user