Skip to content

Commit 8d4c8b6

Browse files
daniel-nolandclaude
andcommitted
feat(dataplane): enforce and display rate limits
Enforce limits after ACLs and before NAT. Add show rate-limits and drop rate-limited packets without punting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
1 parent 2c94b9b commit 8d4c8b6

11 files changed

Lines changed: 91 additions & 1 deletion

File tree

‎Cargo.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/bin/cmdtree_dp.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,12 @@ fn cmd_show_link() -> Node {
348348
.action(CliAction::ShowLink)
349349
}
350350

351+
fn cmd_show_rate_limits() -> Node {
352+
Node::new("rate-limits")
353+
.desc("Show rate limits and per-color packet and byte counts")
354+
.action(CliAction::ShowRateLimits)
355+
}
356+
351357
fn cmd_show_capture() -> Node {
352358
Node::new("capture")
353359
.desc("Show the running or most recent packet capture")
@@ -388,6 +394,7 @@ fn cmd_show() -> Node {
388394
root += cmd_show_lldp();
389395
root += cmd_show_transceivers();
390396
root += cmd_show_link();
397+
root += cmd_show_rate_limits();
391398
root += cmd_show_tech();
392399
root
393400
}

‎cli/src/cliproto.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,9 @@ pub enum CliAction {
557557

558558
// Port links (mstlink)
559559
ShowLink,
560+
561+
// Rate limits
562+
ShowRateLimits,
560563
}
561564
impl CliAction {
562565
#[must_use]

‎dataplane/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ shuttle_dfs = ["concurrency/shuttle_dfs", "shuttle"]
1515

1616
[dependencies]
1717
acl-filter = { workspace = true }
18+
rate-limit = { workspace = true }
1819
afpacket = { workspace = true, features = ["async-tokio"] }
1920
args = { workspace = true }
2021
arrayvec = { workspace = true }

‎dataplane/src/packet_processor/mod.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use concurrency::sync::Arc;
1919
use acl_filter::{AclFilter, AclFilterContextReaderFactory, AclFilterContextWriter};
2020
use flow_entry::flow_table::{FlowLookup, FlowTable};
2121
use flow_filter::{FlowFilter, FlowFilterContextReaderFactory, FlowFilterContextWriter};
22+
use rate_limit::{RateLimitReader, RateLimitWriter, RateLimiter, RateLimits, VpcDestination};
23+
use std::num::NonZeroUsize;
2224

2325
use nat::masquerade::{NatAllocatorReaderFactory, NatAllocatorWriter};
2426
use nat::portfw::{PortForwarder, PortFwTableReaderFactory, PortFwTableWriter};
@@ -50,6 +52,7 @@ pub(crate) struct InternalSetup {
5052
pub natallocatorw: NatAllocatorWriter,
5153
pub flow_filter_writer: FlowFilterContextWriter,
5254
pub aclfiltertablesw: AclFilterContextWriter,
55+
pub rate_limits: RateLimitWriter<VpcDestination>,
5356
pub stats: StatsCollector,
5457
pub vpc_stats_store: Arc<VpcStatsStore>,
5558
pub portfw_w: PortFwTableWriter,
@@ -71,6 +74,7 @@ pub(crate) struct PipelineIngredients {
7174
natallocator_factory: NatAllocatorReaderFactory,
7275
flow_filter_reader_factory: FlowFilterContextReaderFactory,
7376
aclfiltertablesr_factory: AclFilterContextReaderFactory,
77+
rate_limits: RateLimitReader<VpcDestination>,
7478
portfw_factory: PortFwTableReaderFactory,
7579
stats_w: PacketStatsWriter,
7680
pkt_stats: Arc<PacketStats>,
@@ -115,6 +119,7 @@ impl PipelineIngredients {
115119
let flow_filter =
116120
FlowFilter::new("flow-filter", self.flow_filter_reader_factory.handle());
117121
let acl_filter = AclFilter::new("acl-filter", self.aclfiltertablesr_factory.handle());
122+
let rate_limiter = RateLimiter::new(self.rate_limits.clone());
118123
let icmp_error_handler = IcmpErrorHandler::new(self.flow_table.clone());
119124
let flow_lookup = FlowLookup::new("flow-lookup", self.flow_table.clone());
120125
let portfw = PortForwarder::new(
@@ -141,6 +146,9 @@ impl PipelineIngredients {
141146
.add_stage(flow_lookup)
142147
.add_stage(flow_filter)
143148
.add_stage(acl_filter)
149+
// After the ACLs, so that denied packets are not charged; before NAT, so that rate
150+
// limits see destinations as the source VPC addresses them.
151+
.add_stage(rate_limiter)
144152
.add_stage(static_nat)
145153
.add_stage(portfw)
146154
.add_stage(masquerade)
@@ -161,6 +169,7 @@ pub(crate) fn start_router(
161169
params: RouterParams,
162170
driver_status: DriverStatusReader,
163171
capture_dir: Option<String>,
172+
workers: NonZeroUsize,
164173
) -> Result<InternalSetup, RouterError> {
165174
let vpcmapw = VpcMapWriter::<VpcMapName>::new();
166175
let vpc_stats_store: Arc<VpcStatsStore> = VpcStatsStore::new();
@@ -176,6 +185,7 @@ pub(crate) fn start_router(
176185
let flow_filter_reader_factory = flow_filter_writer.get_reader_factory();
177186
let aclfiltertablesw = AclFilterContextWriter::new();
178187
let aclfiltertablesr_factory = aclfiltertablesw.get_reader_factory();
188+
let rate_limits = RateLimitWriter::new(RateLimits::empty(), workers);
179189
let nattablesw = NatTablesWriter::new();
180190
let natallocatorw = NatAllocatorWriter::new();
181191
let nattabler_factory = nattablesw.get_reader_factory();
@@ -209,6 +219,7 @@ pub(crate) fn start_router(
209219
transceivers_read,
210220
))),
211221
link: Some(Box::new(crate::phy_agent::PhyCli(phy_read))),
222+
rate_limits: Some(Box::new(rate_limits.reader())),
212223
};
213224

214225
// create router
@@ -226,6 +237,7 @@ pub(crate) fn start_router(
226237
natallocator_factory,
227238
flow_filter_reader_factory,
228239
aclfiltertablesr_factory,
240+
rate_limits: rate_limits.reader(),
229241
portfw_factory,
230242
stats_w,
231243
pkt_stats,
@@ -242,6 +254,7 @@ pub(crate) fn start_router(
242254
natallocatorw,
243255
flow_filter_writer,
244256
aclfiltertablesw,
257+
rate_limits,
245258
stats,
246259
vpc_stats_store,
247260
portfw_w,

‎dataplane/src/runtime.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,11 +784,13 @@ pub fn main() {
784784
};
785785

786786
// start router
787+
let workers = std::num::NonZeroUsize::from(config.driver.num_workers());
787788
let mut setup = start_router(
788789
&shutdown.router,
789790
router_params,
790791
driver_status_reader,
791792
config.cli.capture_dir.clone(),
793+
workers,
792794
)
793795
.expect("failed to start router");
794796

@@ -997,6 +999,7 @@ pub fn main() {
997999
natallocatorw: setup.natallocatorw,
9981000
flow_filter_writer: setup.flow_filter_writer,
9991001
aclfilterw: setup.aclfiltertablesw,
1002+
rate_limits: setup.rate_limits,
10001003
portfw_w: setup.portfw_w,
10011004
vpc_stats_store: setup.vpc_stats_store,
10021005
dp_status_r: dp_status.clone(),

‎mgmt/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bolero = ["dep:bolero", "interface-manager/bolero", "id/bolero", "net/bolero", "
1818
[dependencies]
1919
# internal
2020
acl-filter = { workspace = true }
21+
rate-limit = { workspace = true }
2122
args = { workspace = true }
2223
clock = { workspace = true }
2324
config = { workspace = true }
@@ -59,6 +60,7 @@ tracing = { workspace = true, features = ["attributes"] }
5960
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
6061

6162
[dev-dependencies]
63+
policer = { workspace = true }
6264
# internal
6365
dpdk = { workspace = true, features = ["test"] } # EAL for tests that build the rte_acl-backed ACL filter and flow-filter context
6466
fixin = { workspace = true }

‎mgmt/src/processor/proc.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use acl_filter::AclFilterContextWriter;
88
use concurrency::sync::Arc;
99
use config::external::overlay::ValidatedOverlay;
1010
use flow_entry::flow_table::FlowTable;
11+
use rate_limit::{Backend, RateLimitWriter, VpcDestination};
1112
use std::collections::{HashMap, HashSet};
1213
use tokio::sync::RwLock;
1314
use tokio::sync::mpsc;
@@ -100,6 +101,9 @@ pub struct ConfigProcessorParams {
100101
// writer for ACL filter tables
101102
pub aclfilterw: AclFilterContextWriter,
102103

104+
// writer for rate limits
105+
pub rate_limits: RateLimitWriter<VpcDestination>,
106+
103107
// writer for port forwarding table
104108
pub portfw_w: PortFwTableWriter,
105109

@@ -574,6 +578,15 @@ fn apply_acl_filter_config(
574578
Ok(())
575579
}
576580

581+
fn apply_rate_limit_config(
582+
overlay: &ValidatedOverlay,
583+
rate_limits: &RateLimitWriter<VpcDestination>,
584+
) -> ConfigResult {
585+
rate_limits.apply(overlay, Backend::Dpdk)?;
586+
debug!("Successfully updated the rate limits");
587+
Ok(())
588+
}
589+
577590
/// Update the Nat tables for static NAT
578591
fn apply_static_nat_config(
579592
vpc_table: &ValidatedVpcTable,
@@ -679,6 +692,7 @@ impl ConfigProcessor {
679692
let natallocatorw = &mut self.proc_params.natallocatorw;
680693
let flow_filter_writer = &mut self.proc_params.flow_filter_writer;
681694
let aclfilterw = &mut self.proc_params.aclfilterw;
695+
let rate_limits = &self.proc_params.rate_limits;
682696
let portfw_w = &mut self.proc_params.portfw_w;
683697
let flow_table = &self.proc_params.flow_table;
684698

@@ -725,6 +739,9 @@ impl ConfigProcessor {
725739
/* apply ACL filter config */
726740
apply_acl_filter_config(overlay, aclfilterw)?;
727741

742+
/* apply rate limits */
743+
apply_rate_limit_config(overlay, rate_limits)?;
744+
728745
/* apply static NAT config */
729746
apply_static_nat_config(overlay.vpc_table(), nattablesw)?;
730747

‎mgmt/src/tests/mgmt.rs‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ pub mod test {
5656

5757
use config::external::ExternalConfigBuilder;
5858
use config::external::overlay::Overlay;
59+
use config::external::overlay::ratelimit::{ColorActions, RateLimit, RateLimitTables};
5960
use config::external::overlay::vpc::{Vpc, VpcTable};
6061
use config::external::overlay::vpcpeering::{
6162
VpcExpose, VpcManifest, VpcPeering, VpcPeeringTable,
6263
};
6364
use config::external::underlay::Underlay;
65+
use policer::{Profile, Rate};
6466

6567
use config::ExternalConfig;
6668
use config::internal::device::DeviceConfig;
@@ -202,7 +204,25 @@ pub mod test {
202204
let vpc_table = sample_vpc_table();
203205
let peering_table = sample_vpc_peering_table();
204206
/* Overlay config */
205-
Overlay::new(vpc_table, peering_table)
207+
Overlay::new(vpc_table, peering_table).with_rate_limits(sample_rate_limits())
208+
}
209+
210+
/* A byte limit on what VPC-1 sends VPC-2 */
211+
fn sample_rate_limits() -> RateLimitTables {
212+
let rate = Rate::new(std::num::NonZeroU64::new(125_000_000).expect("not zero"));
213+
let profile = Profile::new(rate, 1_250_000, None, 0).expect("a burst");
214+
let mut tables = RateLimitTables::default();
215+
tables
216+
.bytes
217+
.add(RateLimit {
218+
name: "vpc-1-to-vpc-2".to_string(),
219+
vpc: "VPC-1".to_string(),
220+
destination: Prefix::expect_from(("192.168.128.0", 24)),
221+
profile,
222+
actions: ColorActions::default(),
223+
})
224+
.expect("one rate limit");
225+
tables
206226
}
207227

208228
/* DEVICE configuration */
@@ -492,6 +512,13 @@ pub mod test {
492512
/* create AclFilterContext for ACL filtering */
493513
let aclfilterw = AclFilterContextWriter::new();
494514

515+
/* create the rate limits */
516+
let rate_limits = rate_limit::RateLimitWriter::new(
517+
rate_limit::RateLimits::empty(),
518+
std::num::NonZeroUsize::MIN,
519+
);
520+
let enforced = rate_limits.clone();
521+
495522
/* create port forwarding table */
496523
let portfw_w = PortFwTableWriter::new();
497524

@@ -516,6 +543,7 @@ pub mod test {
516543
natallocatorw,
517544
flow_filter_writer,
518545
aclfilterw,
546+
rate_limits,
519547
portfw_w,
520548
vpc_stats_store,
521549
dp_status_r,
@@ -543,6 +571,17 @@ pub mod test {
543571
Some("test-gw"),
544572
"an applied config tells its watchers the gateway's name"
545573
);
574+
let enforced = enforced.load();
575+
assert_eq!(
576+
enforced
577+
.bytes()
578+
.rules()
579+
.iter()
580+
.map(rate_limit::Rule::name)
581+
.collect::<Vec<_>>(),
582+
["vpc-1-to-vpc-2"],
583+
"an applied config hands its rate limits to the workers"
584+
);
546585

547586
/* stop the router */
548587
debug!("Stopping the router...");

‎routing/src/cli/handler.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ fn do_handle_cli_request(
598598
CliAction::ShowLldpNeighbors => show_provider(request, sources.lldp_neighbors.as_deref()),
599599
CliAction::ShowTransceivers => show_provider(request, sources.transceivers.as_deref()),
600600
CliAction::ShowLink => show_provider(request, sources.link.as_deref()),
601+
CliAction::ShowRateLimits => show_provider(request, sources.rate_limits.as_deref()),
601602

602603
/* prefetching */
603604
CliAction::Prefetch => prefetch(request, gwconfig, db),

0 commit comments

Comments
 (0)