on master help sign in
Make X-Forwarded-For header optional; skip IP rate-limiting when absent
b77814aiko
server.knot+21−18
@@ -158,9 +158,6 @@ federationEnabled : Bool
158federationEnabled = True {}
158federationEnabled = True {}
159federationUrl : Text -> Text
159federationUrl : Text -> Text
160federationUrl = \server -> federationScheme ++ server
160federationUrl = \server -> federationScheme ++ server
161−-- When True, reject requests that arrive without a client-IP header (for IP-based rate limits).
162−requireClientIp : Bool
163−requireClientIp = True {}
164
161
165-- ============================================
162-- ============================================
166-- Types
163-- Types
@@ -307,12 +304,12 @@ type RateEntry = {key: Text, windowStart: Timestamp, count: Int}
307-- `{"error": "<code>"}`.
304-- `{"error": "<code>"}`.
308
305
309route AuthApi where
306route AuthApi where
310− POST {pubkey: PubkeyHex} /auth/challenge headers {clientIp: Text} -> {challenge: Text, expiresAt: Timestamp} = ReqChallenge
307+ POST {pubkey: PubkeyHex} /auth/challenge headers {xForwardedFor: Maybe Text} -> {challenge: Text, expiresAt: Timestamp} = ReqChallenge
311− POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {clientIp: Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
308+ POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {xForwardedFor: Maybe Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
312
309
313route ClientApi where
310route ClientApi where
314− POST {cursor: Timestamp} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp} = Poll
311+ POST {cursor: Timestamp} /poll headers {authorization: Text, xForwardedFor: Maybe Text} -> {events: [PollEvent], cursor: Timestamp} = Poll
315− POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {} = SendMessages
312+ POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, xForwardedFor: Maybe Text} -> {} = SendMessages
316
313
317route FederationApi where
314route FederationApi where
318 POST {events: [GossipEventType], fromServer: ServerName} /federation/gossip -> {} = RecvGossip
315 POST {events: [GossipEventType], fromServer: ServerName} /federation/gossip -> {} = RecvGossip
@@ -485,6 +482,14 @@ isBadServerName = \sn -> sn == "" || sn == "localhost" || not all (chars sn) (\c
485matchIp : Text -> Text -> Bool
482matchIp : Text -> Text -> Bool
486matchIp = \clientIp sessionIp -> sessionIp == "" || clientIp == sessionIp
483matchIp = \clientIp sessionIp -> sessionIp == "" || clientIp == sessionIp
487
484
485+-- Unwrap an X-Forwarded-For header value to a plain IP string. Empty when the
486+-- header is absent (no reverse proxy in front), which downstream gates treat
487+-- as "skip IP-based rate limiting".
488+clientIpOf : Maybe Text -> Text
489+clientIpOf = \h -> case h of
490+ Just {value: ip} -> ip
491+ Nothing {} -> ""
492+
488-- Pull the token out of an `Authorization: Bearer <token>` header. Returns
493-- Pull the token out of an `Authorization: Bearer <token>` header. Returns
489-- Nothing if the prefix is missing or the token is empty.
494-- Nothing if the prefix is missing or the token is empty.
490extractToken : Text -> Maybe Text
495extractToken : Text -> Maybe Text
@@ -567,7 +572,9 @@ checkAuthGlobalRate = \t -> atomic (do
567 *authGlobalRateCount = c + 1
572 *authGlobalRateCount = c + 1
568 yield (False {}))
573 yield (False {}))
569
574
570−-- Per-source-IP rate gate (skipped when the IP is unknown — see `requireClientIp`).
575+-- Per-source-IP rate gate. When the IP is unknown (no X-Forwarded-For from a
576+-- reverse proxy), the gate is skipped — direct deployments without a proxy
577+-- still rely on global rate limits.
571checkIpRateGate : Text -> Int<Ms> -> IO {rw *ipRates} Bool
578checkIpRateGate : Text -> Int<Ms> -> IO {rw *ipRates} Bool
572checkIpRateGate = \ip t -> if ip == ""
579checkIpRateGate = \ip t -> if ip == ""
573then yield (False {})
580then yield (False {})
@@ -816,13 +823,9 @@ refreshLocalPresence = \t -> do
816handleChallenge : PubkeyHex -> Text -> IO {rw *authGlobalRateCount, rw *authGlobalRateWindowStart, rw *challengeRates, rw *challenges, rw *globalRateCount, rw *globalRateWindowStart, rw *ipRates, clock, random} (Result HttpError {challenge: Text, expiresAt: Int})
823handleChallenge : PubkeyHex -> Text -> IO {rw *authGlobalRateCount, rw *authGlobalRateWindowStart, rw *challengeRates, rw *challenges, rw *globalRateCount, rw *globalRateWindowStart, rw *ipRates, clock, random} (Result HttpError {challenge: Text, expiresAt: Int})
817handleChallenge = \pubkey clientIp -> do
824handleChallenge = \pubkey clientIp -> do
818 t <- now
825 t <- now
819− preErr <- if requireClientIp && clientIp == ""
820− then yield "client_ip_required"
821− else authPreCheck clientIp t
826+ preErr <- authPreCheck clientIp t
822 if preErr != ""
827 if preErr != ""
823− then yield (Err {
824− error: {status: if preErr == "client_ip_required" then 400 else 429, message: preErr}
828+ then yield (Err {error: {status: 429, message: preErr}})
825− })
826 else do
829 else do
827 challenge <- genToken
830 challenge <- genToken
828 result <- atomic (do
831 result <- atomic (do
@@ -1191,10 +1194,10 @@ handleFederationBatchForward = \forwards _ -> do
1191-- its `handleXxx` function. Every route entry above must have a clause here.
1194-- its `handleXxx` function. Every route entry above must have a clause here.
1192api : Server Api _
1195api : Server Api _
1193api = serve Api where
1196api = serve Api where
1194− ReqChallenge = \{pubkey, clientIp} -> handleChallenge pubkey clientIp
1197+ ReqChallenge = \{pubkey, xForwardedFor} -> handleChallenge pubkey (clientIpOf xForwardedFor)
1195− VerifyAuth = \{pubkey, challenge, signature, revokeOthers, clientIp} -> handleVerifyAuth pubkey challenge signature revokeOthers clientIp
1198+ VerifyAuth = \{pubkey, challenge, signature, revokeOthers, xForwardedFor} -> handleVerifyAuth pubkey challenge signature revokeOthers (clientIpOf xForwardedFor)
1196− Poll = \{cursor, authorization, clientIp} -> handlePoll cursor authorization clientIp
1199+ Poll = \{cursor, authorization, xForwardedFor} -> handlePoll cursor authorization (clientIpOf xForwardedFor)
1197− SendMessages = \{messages, authorization, clientIp} -> handleSendMessages messages authorization clientIp
1200+ SendMessages = \{messages, authorization, xForwardedFor} -> handleSendMessages messages authorization (clientIpOf xForwardedFor)
1198 RecvGossip = \{events, fromServer} -> handleRecvGossip events fromServer
1201 RecvGossip = \{events, fromServer} -> handleRecvGossip events fromServer
1199 ForwardMessage = \{toKey, encryptedBlob, fromServer} -> handleForwardMessage toKey encryptedBlob fromServer
1202 ForwardMessage = \{toKey, encryptedBlob, fromServer} -> handleForwardMessage toKey encryptedBlob fromServer
1200 FederationBatchForward = \{forwards, fromServer} -> handleFederationBatchForward forwards fromServer
1203 FederationBatchForward = \{forwards, fromServer} -> handleFederationBatchForward forwards fromServer