Skip to content

feat(relay): Operation-based rate limiting + Resolve most recent#185

Open
afterburn wants to merge 33 commits intomainfrom
feat/rate-limiting
Open

feat(relay): Operation-based rate limiting + Resolve most recent#185
afterburn wants to merge 33 commits intomainfrom
feat/rate-limiting

Conversation

@afterburn
Copy link
Copy Markdown
Contributor

@afterburn afterburn commented Sep 18, 2025

Summary
This PR replaces current rate limiting with an operation-based configuration that maps directly to the relay's three core operations: publish, resolve, and resolve_most_recent.

Configuration Example

[relay]
[[relay.rate_limits]]
operation = "resolve"
quota = "10r/s"
burst = 20

[[relay.rate_limits]]
operation = "resolve_most_recent"
quota = "2r/s"
burst = 5

[[relay.rate_limits]]
operation = "publish"
quota = "2r/s"
burst = 10
whitelist = ["127.0.0.1", "192.168.1.0/24"]  # optional, both ips and subnets supported

Operations in this toml file automatically map to:

  • publish → PUT requests (publishing records)
  • resolve → GET requests without most_recent=true (cached lookups)
  • resolve_most_recent → GET requests with most_recent=true (DHT queries)

If no config.toml file is present, system will use defaults, so this is not a breaking change.

@afterburn afterburn requested review from SHAcollision and SeverinAlexB and removed request for SHAcollision September 18, 2025 12:56
@afterburn afterburn marked this pull request as ready for review September 22, 2025 09:17
Copy link
Copy Markdown
Contributor

@SeverinAlexB SeverinAlexB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to do a second round later

@afterburn
Copy link
Copy Markdown
Contributor Author

@SeverinAlexB resolved your feedback:

  • Removed kb, mb, gb quota units, now always uses either r/s or r/m as requested
  • Changed default rate limits to something saner
  • Added behind_proxy flag in config toml, which allows users to respect/ignore proxy headers
  • Cleaned up IP extraction, only happens once per request now
  • Added operation 'Index' so users can specify rate limiting for root path

Copy link
Copy Markdown
Contributor

@SeverinAlexB SeverinAlexB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments, otherwise looks good.

In the Pubky Core team, we have a convention on how to format PR title. We stick to the conventional commit format.

So for this PR, the title could be: feat(relay): Operation-based rate limiting + Resolve most recent

CleanShot 2025-09-25 at 10 37 51@2x

@afterburn afterburn changed the title Add operation-based rate limiting feat(relay): Operation-based rate limiting + Resolve most recent Sep 25, 2025
@afterburn
Copy link
Copy Markdown
Contributor Author

@SeverinAlexB

  • If no rate limits are defined in config.toml, rate limiting will be disabled completely
  • Added Index to Operation enum so calling GET http://0.0.0.0:6881/ doesn't trigger the incorrect rate limit
  • Added resolve_most_recent_enabled flag so users have the ability to completely ignore the most_recent=true query parameter (defaults to false for backward compatibility)

Copy link
Copy Markdown
Contributor

@SeverinAlexB SeverinAlexB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One NIT, otherwise good to go

@SeverinAlexB
Copy link
Copy Markdown
Contributor

SeverinAlexB commented Oct 6, 2025

Actually, should this add the most_recent option to the pkarr client too? You are just adding it to the relay in this PR?

@afterburn
Copy link
Copy Markdown
Contributor Author

@SeverinAlexB @SHAcollision Restored DHT rate limiting that was previously removed. Since the DHT can be accessed directly, keeping rate limiting in place is necessary. The implementation is now fully backward compatible. Existing [rate_limiter] configurations continue to work as before. New [dht_rate_limit] and [http_rate_limit] options are opt-in. By default, it falls back to the legacy IP-based limiter unless one of the new configs is defined. Also added a comprehensive test to cover the new behavior and ensure backward compat.

afterburn and others added 8 commits November 4, 2025 12:21
* Consolidate simple-dns dependency in workspace root

* Bump simple-dns to latest 0.11.2

* Adapt calls to simple-dns methods

* pkarr: fix port param lookup

* extra: update Endpoint.params type

Params type was updated to better match what simple-dns exposes.

* Throw error if expected SVCParam type doesn't match

* Simplify ipv4 parsing

Co-authored-by: DZ <dzdidi@users.noreply.github.com>

* Use static lifetine for SVCParam in Endpoint.params

* fixed clippy

* A few code cleanups

* fmt

---------

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Co-authored-by: DZ <dzdidi@users.noreply.github.com>
…most_recent sends ?most_recent=true to relays
@afterburn afterburn force-pushed the feat/rate-limiting branch 3 times, most recently from 2bc52f7 to 0d89091 Compare February 27, 2026 14:33
@afterburn afterburn requested review from 86667 and removed request for SHAcollision March 6, 2026 16:27
Copy link
Copy Markdown

@86667 86667 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments 👍

Also we could have quite a few more unit tests. All behaviour may be tested somewhere but unit tests inline with the code they test are pretty much always worth it these days with how fast AI can write them IMO

/// Whether to respect the most_recent query parameter in resolve requests
///
/// If true, the most_recent query parameter is respected and will bypass cache.
/// If false, the most_recent query parameter is ignored and will always load from cache.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like difficult behaviour to debug. Did we consider returning an error if most_recent is requested but the relay is configured with resolve_most_recent: false?

/// Set the operation-based rate limiter configuration.
///
/// Defaults to None (no operation-based rate limiting).
pub fn rate_limiter_config(&mut self, limits: Option<Vec<OperationLimit>>) -> &mut Self {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may technically be a breaking change. im not sure if we have any other consumers of the module as a lib

[package]
name = "pkarr"
version = "5.0.3"
version = "6.0.0-rc.0"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be merging into v6 branch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if so, maybe we can remove the config backwards compatibility logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

especially concerning is behind_proxy being in two possible places - which takes precedence? If we need to keep backwards compatibility then this should be documented. Quick AI check says that [relay].behind_proxy will infact override [rate_limiter].behind_proxy

};

// Go through all the limits and check if we need to throttle or reject the request.
for limit in limits.clone() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no need to clone

Operation::Resolve => {
req.method() == Method::GET
&& Self::is_operation(req.uri().path())
&& !(resolve_most_recent && Self::has_query_param(req, "most_recent", "true"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the rate limiter need to consider the resolve_most_recent config var here? it cares for rate limiting based on the requests, the process's config is irrelevant

tracing::debug!("Rate limit of {} exceeded for {:?}", limit.limit.quota, key);
return Box::pin(async move {
Ok(
HttpError::new(StatusCode::TOO_MANY_REQUESTS, Some("Rate limit exceeded"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry-After header always nice for 429s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants