Skip to content

feat: add rate limiter#1750

Open
drahnr wants to merge 17 commits intonextfrom
bernhard-feat-add-rate-limiter
Open

feat: add rate limiter#1750
drahnr wants to merge 17 commits intonextfrom
bernhard-feat-add-rate-limiter

Conversation

@drahnr
Copy link
Contributor

@drahnr drahnr commented Mar 4, 2026

Adds a rate-limiter with quotas using tower_governor and tower::limit::GlobalConcurrencyLimitLayer.

Done in scope of our recent fd consumption, generally a useful thing to do.

Makes all parameters configurable and group them into GrpcOptions and moves them to miden-node-utils to avoid duplication, similarly for limiting layer setup helpers.

@drahnr drahnr force-pushed the bernhard-feat-add-rate-limiter branch from 3ea5ce1 to 558e109 Compare March 5, 2026 16:25
@drahnr drahnr requested a review from Mirko-von-Leipzig March 5, 2026 16:36
@drahnr drahnr marked this pull request as ready for review March 5, 2026 16:36
) -> runtime::Runtime {
let genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 1, random_secret_key());
fs::create_dir_all(data_directory.join(ACCOUNT_TREE_STORAGE_DIR))
.expect("account tree directory should be created");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this diff related and intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it's required, let me remove it

// TODO double check how to address proxy rate limiting based on i.e. `X-Real-IP`.
request
.metadata_mut()
.insert("forwarded", format!("for={addr}").try_into().unwrap());
Copy link
Collaborator

@sergerad sergerad Mar 5, 2026

Choose a reason for hiding this comment

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

we shouldn't panic here right? this is a callback for every request?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a callback per request, unless someone manages to inject a broken/malicious IP in the Linux Kernel and manage to pop it up in our end, it's virtually impossible to break this invariant.

Since we're using SmartIPKeyExtractor* we might get away without this. It's here for ensuring we have a fallback

}
});
Ok(tower_governor::GovernorLayer::new(config))
}
Copy link
Collaborator

@sergerad sergerad Mar 5, 2026

Choose a reason for hiding this comment

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

Just checking this only actually gets called once (rate_limit_per_ip())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I get 7 hits in the diff? It creates a layer so this is fine, unless I am missing smth?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we should be applying these to all servers? Only the RPC server or no?

The rest are internal; and we should be bounding the calls we make. e.g. we don't want to disconnect our own gRPC connections after some time..

let addr = request
.remote_addr()
.ok_or_else(|| Status::failed_precondition("Expected TCP connection"))?;
// TODO double check how to address proxy rate limiting based on i.e. `X-Real-IP`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Was this addressed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is addressed with the SmartKeyExtractor* which tests for 3 distinct metadatadata entries before using the raw incoming IP

> {
let config = GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.per_second(grpc_options.replenish_per_sec)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we're using this invertedly. .per_second specifies the number of seconds to wait until a single permit is replenished. And not the number of permits to replenish each second.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct.

let config = GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.per_second(grpc_options.replenish_per_sec)
.burst_size(grpc_options.burst_size as u32)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we make burst_size a u32 then?

.burst_size(grpc_options.burst_size as u32)
.use_headers()
.finish()
.context("config parameters are inconsistent, i.e. burst < per second")?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think burst < per second is an error condition. Only that they're non-zero. Which we can enforce with types.

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.

3 participants