1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
package game
import (
"sync"
"time"
)
// Interactive login throttling. The admin HTTP /login route has its own
// limiter; the telnet/TLS/websocket login is the primary credential surface
// and needs one too. The policy is an escalating delay keyed on
// (account, client IP): a hard lockout is deliberately avoided because
// account existence is discoverable and a lockout would be a targeted
// griefing tool. The delay runs on the connection goroutine, never the
// owner, so a brute-force attempt stalls only its own connection.
const (
// loginThrottleMaxDelay caps the escalation so a legitimate player who
// fat-fingers a password a few times is inconvenienced, not locked out.
loginThrottleMaxDelay = 4 * time.Second
// loginThrottleMaxKeys bounds the tracked keys so a source that cycles
// account names or addresses cannot grow server memory without bound.
loginThrottleMaxKeys = 4096
// loginThrottleEntryTTL is how long an untouched entry is retained.
loginThrottleEntryTTL = 10 * time.Minute
)
type loginThrottleEntry struct {
failures int
lastSeen time.Time
}
type loginThrottle struct {
mu sync.Mutex
entries map[string]loginThrottleEntry
}
func newLoginThrottle() *loginThrottle {
return &loginThrottle{entries: make(map[string]loginThrottleEntry)}
}
// ensureLoginThrottle returns the game's login throttle, creating it on first
// use. The connection goroutine calls this; sync.Once makes the lazy init
// race-free for Game literals that did not go through New.
func (g *Game) ensureLoginThrottle() *loginThrottle {
g.loginThrottleOnce.Do(func() {
g.loginThrottle = newLoginThrottle()
})
return g.loginThrottle
}
// loginThrottleKey combines the attempted account and the client address.
// Either may be empty (synthetic connections).
func loginThrottleKey(account, ip string) string {
return account + "\x00" + ip
}
// delayFor records a failed attempt and returns the escalating delay the
// caller should wait before responding: 0, 250ms, 500ms, 1s, 2s, 4s, 4s...
func (t *loginThrottle) delayFor(key string) time.Duration {
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
if len(t.entries) >= loginThrottleMaxKeys {
t.evictLocked(now)
}
e := t.entries[key]
e.failures++
e.lastSeen = now
t.entries[key] = e
if e.failures < 2 {
// The first wrong password is free: a single typo must not cost a
// legitimate player any time.
return 0
}
shift := e.failures - 2
if shift > 4 { // 250ms << 4 == loginThrottleMaxDelay
shift = 4
}
d := time.Duration(250*int64(1<<uint(shift))) * time.Millisecond
if d > loginThrottleMaxDelay {
d = loginThrottleMaxDelay
}
return d
}
// clear forgets a key after a successful login.
func (t *loginThrottle) clear(key string) {
t.mu.Lock()
delete(t.entries, key)
t.mu.Unlock()
}
// evictLocked drops expired entries, then the oldest entry if still at the
// cap. Caller holds mu.
func (t *loginThrottle) evictLocked(now time.Time) {
for k, e := range t.entries {
if now.Sub(e.lastSeen) > loginThrottleEntryTTL {
delete(t.entries, k)
}
}
if len(t.entries) < loginThrottleMaxKeys {
return
}
oldestKey := ""
var oldest time.Time
for k, e := range t.entries {
if oldestKey == "" || e.lastSeen.Before(oldest) {
oldestKey, oldest = k, e.lastSeen
}
}
if oldestKey != "" {
delete(t.entries, oldestKey)
}
}
|