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:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-13 16:25:34 +02:00
1 parent 2de21b2b6b
commit 8e0f1283c6
4 files changed
+344 -17

No files matched your search

+58 -10
View File
@@ -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)