Security: Add per-IP connection and login rate limiting
The TCP device port is exposed directly to the public internet, so a single host must not be able to exhaust the global connection budget, flood new connection attempts, or brute-force the device authorization passcode. - server: cap concurrent connections and the accept rate per source IP (defaults 256 concurrent / 120 per minute, overridable via YAMA_PERIP_MAX_CONNS and YAMA_PERIP_CONN_RATE). Rejections happen at accept time, before any buffer or goroutine is allocated, and a rejected device simply retries on its reconnect loop. - cmd: throttle TOKEN_AUTH per source IP (120/min) and answer over-limit attempts with a normal failed response so clients get a definitive reply. - web: periodically sweep the login rate-limit maps so the username-keyed one (which is attacker-controlled) cannot grow without bound. Per-IP defaults are generous enough that a NAT office whose devices share one source IP is unaffected. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
2de21b2b6b
commit
8e0f1283c6
4 files changed
+344
-17
No files matched your search
+58
-10
@@ -26,11 +26,12 @@ import (
|
||||
|
||||
// MyHandler implements the server.Handler interface
|
||||
type MyHandler struct {
|
||||
log *logger.Logger
|
||||
auth *auth.Authenticator
|
||||
srv *server.Server
|
||||
hub *hub.Hub
|
||||
signer licensing.Signer // CMD_MASTERSETTING signer (local / remote / noop)
|
||||
log *logger.Logger
|
||||
auth *auth.Authenticator
|
||||
srv *server.Server
|
||||
hub *hub.Hub
|
||||
signer licensing.Signer // CMD_MASTERSETTING signer (local / remote / noop)
|
||||
loginLimit *wsauth.RateLimiter // per-IP TOKEN_AUTH throttle
|
||||
}
|
||||
|
||||
// OnConnect is called when a client connects
|
||||
@@ -497,6 +498,20 @@ func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, client
|
||||
|
||||
// handleAuth handles authorization request (TOKEN_AUTH = 100)
|
||||
func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) {
|
||||
// Per-IP throttle on TOKEN_AUTH: rate-limit online passcode-guessing
|
||||
// attempts against the device authorization protocol. On exceed we answer
|
||||
// with a normal "failed" response (resp[0]=0) so the client gets a
|
||||
// definitive answer instead of hanging, while a brute-forcer is capped.
|
||||
ip := ctx.GetPeerIP()
|
||||
if !h.loginLimit.Allow(ip) {
|
||||
h.log.Warn("TOKEN_AUTH rate-limited: ip=%s conn=%d", ip, ctx.ID)
|
||||
resp := h.auth.BuildResponse(&auth.AuthResult{Valid: false, Message: "尝试过于频繁,请稍后再试"})
|
||||
if err := h.srv.Send(ctx, resp); err != nil {
|
||||
h.log.Error("Failed to send rate-limit auth response to client %d: %v", ctx.ID, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
result := h.auth.Authenticate(data)
|
||||
info := ctx.GetInfo()
|
||||
|
||||
@@ -674,6 +689,19 @@ func normalizeBasePath(s string) string {
|
||||
return "/" + s
|
||||
}
|
||||
|
||||
// envInt parses an integer env var, returning def when unset or malformed.
|
||||
func envInt(name string, def int) int {
|
||||
v := strings.TrimSpace(os.Getenv(name))
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
|
||||
// signed with the operator's RSA private key and prints it to stdout.
|
||||
//
|
||||
@@ -875,23 +903,43 @@ func main() {
|
||||
log.Warn("Failed to load %s: %v (continuing with admin only)", usersFile, err)
|
||||
}
|
||||
|
||||
// TCP authorization (TOKEN_AUTH) brute-force throttle. Keyed by source IP
|
||||
// (the real TCP peer — there is no X-Forwarded-For on the device port).
|
||||
// Generous enough that a NAT office whose devices all re-auth at once is
|
||||
// unaffected, tight enough to make online passcode guessing impractical.
|
||||
authLoginLimit := wsauth.NewRateLimiter(120, time.Minute)
|
||||
|
||||
// Bounded-key hygiene: prune aged-out entries so the per-IP map cannot grow
|
||||
// without bound. Runs for the process lifetime (a daemon goroutine; the
|
||||
// process exits via the SIGINT/SIGTERM handler at the bottom of main).
|
||||
go func() {
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
authLoginLimit.Sweep()
|
||||
}
|
||||
}()
|
||||
|
||||
// Create servers for each port
|
||||
var servers []*server.Server
|
||||
for _, port := range ports {
|
||||
config := server.DefaultConfig()
|
||||
config.Port = port
|
||||
config.MaxConnections = 9999
|
||||
config.PerIPMaxConnections = envInt("YAMA_PERIP_MAX_CONNS", config.PerIPMaxConnections)
|
||||
config.PerIPConnRateLimit = envInt("YAMA_PERIP_CONN_RATE", config.PerIPConnRateLimit)
|
||||
|
||||
srv := server.New(config)
|
||||
srv.SetLogger(log.WithPrefix(fmt.Sprintf("Server:%d", port)))
|
||||
|
||||
// Create handler for this server
|
||||
handler := &MyHandler{
|
||||
log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)),
|
||||
auth: authenticator,
|
||||
srv: srv,
|
||||
hub: deviceHub,
|
||||
signer: signer,
|
||||
log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)),
|
||||
auth: authenticator,
|
||||
srv: srv,
|
||||
hub: deviceHub,
|
||||
signer: signer,
|
||||
loginLimit: authLoginLimit,
|
||||
}
|
||||
srv.SetHandler(handler)
|
||||
|
||||
|
||||
Reference in new issue
Block a user