Repository navigation
feat: opt-in per-IP limit on pending handshakes - #579
AndreyOsipuk wants to merge 6 commits into
Conversation
A connection holds a worker until it authenticates, goes to the fronting
domain or hits the handshake timeout. One address that opens hundreds of
connections and never completes the handshake can keep the whole
concurrency budget busy and starve everyone else.
[defense.pending-handshakes] max-per-ip limits concurrent unauthenticated
handshakes from one IP. The slot is released as soon as the secret is
verified or the connection is fronted, so authenticated sessions and
bursts of short media connections (authenticated within milliseconds) are
not counted. dry-run observes the limit without enforcing it. Disabled by
default. Event PendingHandshakeLimit, metric pending_handshake_limit{action}.
The allowlist loads asynchronously and rejects every connection until it is ready, which on a slow CI runner looked like a rejection by the limit (0 admitted). Also apply De Morgan's law as staticcheck suggests.
|
@AndreyOsipuk this change also went into dolonet/mtg-multi (#47), and review there produced follow-ups in dolonet#50 that apply here:
Could you port them, so that @9seconds reviews the version that was already reviewed? |
The limit stops silent and slow connections, not every flood: a connection that sends garbage goes to the fronting domain at once, releases its slot and then holds a worker under the idle timeout. IPv6 is counted per /128, so a client with a /64 is effectively not limited. Behind a shared address (CGNAT) one abusive client can take all slots and block its neighbours for up to the handshake timeout, so dry-run comes first. Updated in example.config.toml and the ProxyOpts / limiter comments.
Add the new event to the noop stream/observer cases and an event stream test that checks both observers get it with its stream ID, timestamp and action.
- the retry loop after the pending connection is closed pauses 50ms between attempts, so it no longer can run out before the proxy notices the EOF and releases the slot; - waiting for the allowlist has a 5s deadline instead of looping forever; - the closed pending connection goes to a local fronting server (DomainFrontingHost/Port on 127.0.0.1) instead of the real fronting host, so the test needs no external network. The setup moves to helpers to be reused by the next test. Passes 30 runs with -race, and with the network disabled.
Each connection rejected by the per-IP pending-handshake limit wrote an Info line, which is a lot under the very flood the limit is for. The line is now written at most once per 10 seconds with the number of suppressed ones. The EventPendingHandshakeLimit event is still sent for every rejection: it feeds the pending_handshake_limit metric. The throttle is a small internal package, internal/logthrottle: one line per interval, the rest counted and reported with the next line.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #579 +/- ##
==========================================
+ Coverage 66.65% 67.41% +0.76%
==========================================
Files 110 112 +2
Lines 4000 4103 +103
==========================================
+ Hits 2666 2766 +100
- Misses 1189 1192 +3
Partials 145 145 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Ported the follow-ups from dolonet#50. The head is now
Checks: |
Why
A connection occupies a worker (
concurrency) until it authenticates, goes to the fronting domain or hits the handshake timeout. A single address that opens hundreds of connections and never completes the handshake can keep the whole budget busy, and every other client getsconcurrency limited. We hit exactly this in production on another MTProto proxy (telemt): one client opened ~1100 TCP connections and the proxy stopped serving everyone. mtg has the same exposure: there is no per-IP limit of any kind.Limiting connections per IP at the firewall (
connlimit) is the wrong tool: behind carrier-grade NAT many real users share one address, and Telegram opens a burst of connections when sending media. We tried it and it cut photo uploads for real users.What
dry-runadmits everything and reports what would have been rejected, to pick a threshold from real traffic first.EventPendingHandshakeLimit; Prometheus/StatsD metricpending_handshake_limit{action="rejected"|"observed"}.The same limit is proposed for telemt (telemt/telemt#950) and for mtg-multi (dolonet#47).
Tests
Unit tests for the limiter (disabled, reject and release, dry-run, independent addresses, concurrent acquire/release never exceeds the limit) and an end-to-end proxy test (stalled handshakes from one IP: the extra connection is closed, other IPs are served, the slot is freed when a handshake ends).
go vet ./...,go test -race ./...andGOOS=windows go build ./...pass. Disabled by default, no behaviour change unless configured.