Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[alias]
xtask = "run --package xtask --"
ubench = "bench --package opte-bench --bench userland --profile release-lto --"
mbench = "bench --package opte-bench --bench multicast --profile release-lto --"
kbench = "bench --package opte-bench --bench xde --"

[env]
Expand Down
1 change: 1 addition & 0 deletions .github/buildomat/jobs/bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pfexec add_drv xde
banner "bench"
cargo kbench local
cargo ubench
cargo mbench

cp -r target/criterion $OUT_DIR
cp -r target/xde-bench $OUT_DIR
Expand Down
3 changes: 3 additions & 0 deletions .github/buildomat/jobs/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ pfexec /input/xde/work/test/multicast_multi_sub --nocapture --test-threads=1
pfexec chmod +x /input/xde/work/test/multicast_validation
pfexec /input/xde/work/test/multicast_validation --nocapture --test-threads=1

pfexec chmod +x /input/xde/work/test/multicast_source_filter
pfexec /input/xde/work/test/multicast_source_filter --nocapture --test-threads=1

banner "teardown"
# Ensure full driver teardown is exercised after tests complete
pfexec rem_drv xde
7 changes: 7 additions & 0 deletions .github/buildomat/jobs/xde.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#: "=/work/test/multicast_rx",
#: "=/work/test/multicast_multi_sub",
#: "=/work/test/multicast_validation",
#: "=/work/test/multicast_source_filter",
#: "=/work/xde.conf",
#: ]
#:
Expand Down Expand Up @@ -134,8 +135,14 @@ multicast_validation_test=$(
cargo build -q --test multicast_validation --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
cargo build --test multicast_source_filter
multicast_source_filter_test=$(
cargo build -q --test multicast_source_filter --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
mkdir -p /work/test
cp $loopback_test /work/test/loopback
cp $multicast_rx_test /work/test/multicast_rx
cp $multicast_multi_sub_test /work/test/multicast_multi_sub
cp $multicast_validation_test /work/test/multicast_validation
cp $multicast_source_filter_test /work/test/multicast_source_filter
4 changes: 4 additions & 0 deletions bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ harness = false
[[bench]]
name = "xde"
harness = false

[[bench]]
name = "multicast"
harness = false
134 changes: 134 additions & 0 deletions bench/benches/multicast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Copyright 2026 Oxide Computer Company

//! Multicast microbenchmarks.

use criterion::BenchmarkId;
use criterion::Criterion;
use criterion::Throughput;
use criterion::criterion_group;
use criterion::criterion_main;
use oxide_vpc::api::FilterMode;
use oxide_vpc::api::IpAddr;
use oxide_vpc::api::Ipv4Addr;
use oxide_vpc::api::Ipv6Addr;
use oxide_vpc::api::SourceFilter;
use std::collections::BTreeSet;
use std::hint::black_box;

/// Generate a source IP address for filter testing (10.0.0.x).
/// These are unicast source addresses, not multicast group destinations.
fn make_src_v4(i: u32) -> IpAddr {
IpAddr::Ip4(Ipv4Addr::from(0x0a000000u32 + i))
}

/// Generate a source IP address for filter testing (fd00::x).
/// These are unicast source addresses, not multicast group destinations.
fn make_src_v6(i: u32) -> IpAddr {
let mut bytes = [0u8; 16];
bytes[0..4].copy_from_slice(&[0xfd, 0x00, 0x00, 0x00]);
bytes[12..16].copy_from_slice(&i.to_be_bytes());
IpAddr::Ip6(Ipv6Addr::from(bytes))
}

/// Benchmark [`SourceFilter::allows`] for various filter configurations.
fn source_filter_allows(c: &mut Criterion) {
let mut group = c.benchmark_group("source_filter/allows");
group.throughput(Throughput::Elements(1));

let src_v4 = make_src_v4(100); // Not in any source list
let src_v6 = make_src_v6(100);

// Fast path: EXCLUDE() with empty sources (*, G)
let filter_any = SourceFilter::default();
group.bench_function("exclude_empty_v4", |b| {
b.iter(|| black_box(filter_any.allows(black_box(src_v4))))
});
group.bench_function("exclude_empty_v6", |b| {
b.iter(|| black_box(filter_any.allows(black_box(src_v6))))
});

// EXCLUDE with sources: "Miss" case where source is not in exclusion list
for size in [1, 5, 10, 50, 100] {
let sources_v4: BTreeSet<_> = (0..size).map(make_src_v4).collect();
let filter_v4 =
SourceFilter { mode: FilterMode::Exclude, sources: sources_v4 };
group.bench_with_input(
BenchmarkId::new("exclude_miss_v4", size),
&filter_v4,
|b, f| b.iter(|| black_box(f.allows(black_box(src_v4)))),
);
let src_in_list_v4 = make_src_v4(0);
group.bench_with_input(
BenchmarkId::new("exclude_hit_v4", size),
&filter_v4,
|b, f| b.iter(|| black_box(f.allows(black_box(src_in_list_v4)))),
);

let sources_v6: BTreeSet<_> = (0..size).map(make_src_v6).collect();
let filter_v6 =
SourceFilter { mode: FilterMode::Exclude, sources: sources_v6 };
group.bench_with_input(
BenchmarkId::new("exclude_miss_v6", size),
&filter_v6,
|b, f| b.iter(|| black_box(f.allows(black_box(src_v6)))),
);
let src_in_list_v6 = make_src_v6(0);
group.bench_with_input(
BenchmarkId::new("exclude_hit_v6", size),
&filter_v6,
|b, f| b.iter(|| black_box(f.allows(black_box(src_in_list_v6)))),
);
}

// INCLUDE with sources: "Hit" case where source is in inclusion list
for size in [1, 5, 10, 50, 100] {
let sources_v4: BTreeSet<_> = (0..size).map(make_src_v4).collect();
let filter_v4 =
SourceFilter { mode: FilterMode::Include, sources: sources_v4 };
let src_in_list_v4 = make_src_v4(0);
group.bench_with_input(
BenchmarkId::new("include_hit_v4", size),
&filter_v4,
|b, f| b.iter(|| black_box(f.allows(black_box(src_in_list_v4)))),
);
group.bench_with_input(
BenchmarkId::new("include_miss_v4", size),
&filter_v4,
|b, f| b.iter(|| black_box(f.allows(black_box(src_v4)))),
);

let sources_v6: BTreeSet<_> = (0..size).map(make_src_v6).collect();
let filter_v6 =
SourceFilter { mode: FilterMode::Include, sources: sources_v6 };
let src_in_list_v6 = make_src_v6(0);
group.bench_with_input(
BenchmarkId::new("include_hit_v6", size),
&filter_v6,
|b, f| b.iter(|| black_box(f.allows(black_box(src_in_list_v6)))),
);
group.bench_with_input(
BenchmarkId::new("include_miss_v6", size),
&filter_v6,
|b, f| b.iter(|| black_box(f.allows(black_box(src_v6)))),
);
}

// INCLUDE() empty, rejecting all
let filter_none =
SourceFilter { mode: FilterMode::Include, sources: BTreeSet::new() };
group.bench_function("include_empty_v4", |b| {
b.iter(|| black_box(filter_none.allows(black_box(src_v4))))
});
group.bench_function("include_empty_v6", |b| {
b.iter(|| black_box(filter_none.allows(black_box(src_v6))))
});

group.finish();
}

criterion_group!(benches, source_filter_allows);
criterion_main!(benches);
14 changes: 12 additions & 2 deletions bin/opteadm/src/bin/opteadm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use oxide_vpc::api::FirewallRule;
use oxide_vpc::api::IpCfg;
use oxide_vpc::api::Ipv4Cfg;
use oxide_vpc::api::Ipv6Cfg;
use oxide_vpc::api::McastForwardingNextHop;
use oxide_vpc::api::McastSubscribeReq;
use oxide_vpc::api::McastUnsubscribeAllReq;
use oxide_vpc::api::McastUnsubscribeReq;
Expand All @@ -64,6 +65,7 @@ use oxide_vpc::api::SetMcast2PhysReq;
use oxide_vpc::api::SetMcastForwardingReq;
use oxide_vpc::api::SetVirt2BoundaryReq;
use oxide_vpc::api::SetVirt2PhysReq;
use oxide_vpc::api::SourceFilter;
use oxide_vpc::api::TunnelEndpoint;
use oxide_vpc::api::VpcCfg;
use oxide_vpc::print::print_mcast_fwd;
Expand Down Expand Up @@ -926,7 +928,11 @@ fn main() -> anyhow::Result<()> {
let next_hop_addr = NextHopV6::new(next_hop, next_hop_vni);
let req = SetMcastForwardingReq {
underlay,
next_hops: vec![(next_hop_addr, replication)],
next_hops: vec![McastForwardingNextHop {
next_hop: next_hop_addr,
replication,
source_filter: SourceFilter::default(),
}],
};
hdl.set_mcast_fwd(&req)?;
}
Expand All @@ -945,7 +951,11 @@ fn main() -> anyhow::Result<()> {
}

Command::McastSubscribe { port, group } => {
let req = McastSubscribeReq { port_name: port, group };
let req = McastSubscribeReq {
port_name: port,
group,
filter: SourceFilter::default(),
};
hdl.mcast_subscribe(&req)?;
}

Expand Down
75 changes: 74 additions & 1 deletion crates/opte-api/src/ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,46 @@ pub enum IpAddr {
}

impl IpAddr {
/// Returns true if this is a multicast address.
pub const fn is_multicast(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_multicast(),
IpAddr::Ip6(v6) => v6.is_multicast(),
}
}

/// Returns true if this is the unspecified address.
pub const fn is_unspecified(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_unspecified(),
IpAddr::Ip6(v6) => v6.is_unspecified(),
}
}

/// Returns true if this is a loopback address.
pub const fn is_loopback(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_loopback(),
IpAddr::Ip6(v6) => v6.is_loopback(),
}
}

/// Returns true if this is a link-local address.
pub const fn is_link_local(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_link_local(),
IpAddr::Ip6(v6) => v6.is_link_local(),
}
}

/// Returns true if this is a broadcast address (IPv4 only, IPv6 has no broadcast).
pub const fn is_broadcast(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_broadcast(),
IpAddr::Ip6(_) => false,
}
}

/// Return the multicast MAC address associated with this multicast IP address.
/// If the IP address is not multicast, None will be returned.
///
Expand Down Expand Up @@ -410,6 +443,7 @@ pub struct Ipv4Addr {

impl Ipv4Addr {
pub const ANY_ADDR: Self = Self { inner: [0; 4] };
pub const LOCALHOST: Self = Self { inner: [127, 0, 0, 1] };
pub const LOCAL_BCAST: Self = Self { inner: [255; 4] };

/// Return the bytes of the address.
Expand Down Expand Up @@ -455,10 +489,31 @@ impl Ipv4Addr {
u32::from_be_bytes(self.bytes()).to_be()
}

/// Returns true if this is a multicast address (224.0.0.0/4).
pub const fn is_multicast(&self) -> bool {
matches!(self.inner[0], 224..240)
}

/// Returns true if this is the unspecified address (0.0.0.0).
pub const fn is_unspecified(&self) -> bool {
matches!(self.inner, [0, 0, 0, 0])
}

/// Returns true if this is a loopback address (127.0.0.0/8).
pub const fn is_loopback(&self) -> bool {
self.inner[0] == 127
}

/// Returns true if this is the broadcast address (255.255.255.255).
pub const fn is_broadcast(&self) -> bool {
matches!(self.inner, [255, 255, 255, 255])
}

/// Returns true if this is a link-local address (169.254.0.0/16).
pub const fn is_link_local(&self) -> bool {
self.inner[0] == 169 && self.inner[1] == 254
}

/// Return the multicast MAC address associated with this multicast IPv4
/// address. If the IPv4 address is not multicast, None will be returned.
///
Expand Down Expand Up @@ -621,6 +676,9 @@ impl Ipv6Addr {
/// The unspecified IPv6 address, i.e., `::` or all zeros.
pub const ANY_ADDR: Self = Self { inner: [0; 16] };

/// The loopback address, i.e., `::1`.
pub const LOCALHOST: Self = Self::from_const([0, 0, 0, 0, 0, 0, 0, 1]);

/// The All-Routers multicast address, used in the Neighbor Discovery
/// Protocol.
pub const ALL_ROUTERS: Self =
Expand Down Expand Up @@ -694,11 +752,26 @@ impl Ipv6Addr {
&self.inner[..EXPECTED.len()] == EXPECTED
}

/// Return `true` if this is a multicast IPv6 address, and `false` otherwise
/// Returns true if this is a multicast address (ff00::/8).
pub const fn is_multicast(&self) -> bool {
self.inner[0] == 0xFF
}

/// Returns true if this is the unspecified address (::).
pub const fn is_unspecified(&self) -> bool {
matches!(self.inner, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
}

/// Returns true if this is the loopback address (::1).
pub const fn is_loopback(&self) -> bool {
matches!(self.inner, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
}

/// Returns true if this is a link-local address (fe80::/10).
pub const fn is_link_local(&self) -> bool {
self.inner[0] == 0xfe && (self.inner[1] & 0xc0) == 0x80
}

/// Return `true` if this is a multicast IPv6 address with the ff04::/16 prefix
/// (admin-local scope with flags=0) as used by Omicron for underlay multicast.
///
Expand Down
2 changes: 1 addition & 1 deletion crates/opte-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub use ulp::*;
///
/// We rely on CI and the check-api-version.sh script to verify that
/// this number is incremented anytime the oxide-api code changes.
pub const API_VERSION: u64 = 38;
pub const API_VERSION: u64 = 39;

/// Major version of the OPTE package.
pub const MAJOR_VERSION: u64 = 0;
Expand Down
Loading