Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6d959de
[multicast] DDM multicast exchange: V4 protocol, MRIB sync, M2P hooks
zeeshanlakhani Apr 1, 2026
86ec97f
[review] address PR review: doc comments, lock! cleanup
zeeshanlakhani Apr 7, 2026
b5e96c8
[review+arch-change] address PR review + revisit M2P/OPTE handling
zeeshanlakhani Apr 8, 2026
bf2a703
[api] add if_name to PeerInfo for sled-to-port mapping
zeeshanlakhani Apr 8, 2026
6bd0e0e
merge upstream branch
zeeshanlakhani Apr 16, 2026
a3df7a0
[multicast|deps] remove mg-common dep from ddm-admin-client
zeeshanlakhani Apr 17, 2026
b92bbba
merge upstream branch
zeeshanlakhani Apr 19, 2026
1002d01
merge zl/mrib
zeeshanlakhani Apr 21, 2026
bc74980
[mrib] sync rpf revalidation on unicast route removal
zeeshanlakhani Apr 21, 2026
8c961bc
merge upstream branch
zeeshanlakhani Apr 22, 2026
66f87ea
merge upstream branch
zeeshanlakhani May 6, 2026
8949e82
[ddmd] add --no-state-machine flag for test fixtures and Linux build
zeeshanlakhani May 7, 2026
fab9beb
[ddm-api] PUT /peer for state-machine-bypassing peer injection
zeeshanlakhani May 8, 2026
f369dd7
[merge] merge upstream
zeeshanlakhani May 20, 2026
ea12d44
[merge + update] merge upstream branch
zeeshanlakhani May 22, 2026
4d77f62
[merge] mrib into ddm-mcast
zeeshanlakhani May 28, 2026
62c627c
merge upstream branch
zeeshanlakhani Jun 2, 2026
60a5a24
mg-common: shared tfport name parser for qsfp and rear ports
zeeshanlakhani Jun 2, 2026
acb26b9
[refactor] mg-lower: extract `port_link_from_ifname` helper from dend…
zeeshanlakhani Jun 2, 2026
e506027
[design-change] ddm: program DPD multicast members in ddmd from impor…
zeeshanlakhani Jun 3, 2026
5aaa7c9
[ddm] negotiate and exchange underlay multicast routes (post-merge)
zeeshanlakhani Jun 9, 2026
c49f755
[ddm] clarify --api-only peer-set docs, bind admin API synchronously
zeeshanlakhani Jun 14, 2026
4cc5698
[merge] Merge upstream: includes MRIB/v12 update
zeeshanlakhani Jun 15, 2026
398b4a7
[merge] merge main: dpd-client to latest multicast-e2e, Db error Conf…
zeeshanlakhani Jul 12, 2026
37a0e2e
[merge] merge in parent
zeeshanlakhani Jul 14, 2026
77ff902
[ddm] run the multicast sweep as a runtime task, add periodic exchang…
zeeshanlakhani Jun 22, 2026
0969755
[ci] helios target
zeeshanlakhani Jul 15, 2026
c7dadcb
[merge] merge in parent
zeeshanlakhani Jul 28, 2026
faec135
[merge] merge in parent
zeeshanlakhani Aug 4, 2026
e4d2432
[deps] bump dendrite mcast pin to 72b200e5
zeeshanlakhani Aug 4, 2026
f74e746
[deps] update dendrite mcast pin to 0d48b5a3
zeeshanlakhani Aug 6, 2026
4507e3a
[merge] merge main w/ mcast pins
zeeshanlakhani Aug 24, 2026
5fedb5d
[ddm] bound V4 multicast exchange bodies
zeeshanlakhani Aug 24, 2026
e9fb532
[merge] merge upstream
zeeshanlakhani Aug 24, 2026
96a2f15
[merge] merge upstream
zeeshanlakhani Aug 24, 2026
f0e5ad9
[client-common] record oxnet consolidation intent for vni and multica…
zeeshanlakhani Aug 28, 2026
6b571a9
[merge] merge zl/mrib
zeeshanlakhani Aug 28, 2026
36e4365
[merge] merge zl/mrib
zeeshanlakhani Aug 31, 2026
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
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dropshot-api-manager-types = "0.7.2"
expectorate = "1.3.0"
schemars = { version = "0.8.22", features = [ "uuid1", "chrono" ] }
tokio = { version = "1.52.1", features = ["full"] }
futures = "0.3"
serde_repr = "0.1"
anyhow = "1.0.104"
port-file = "0.1.0"
Expand Down
4 changes: 4 additions & 0 deletions client-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ edition = "2024"

[dependencies]
oxnet.workspace = true
schemars.workspace = true
serde.workspace = true
thiserror.workspace = true

[dev-dependencies]
omicron-common.workspace = true
serde_json.workspace = true
2 changes: 0 additions & 2 deletions client-common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// 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 addressing constants shared across the routing suite.
//!
//! These mirror the canonical definitions in `omicron_common::address`.
Expand Down
2 changes: 2 additions & 0 deletions client-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

pub mod address;
pub mod multicast;
pub mod vni;

/// Like `println!`, but silently exits on broken pipe (EPIPE) instead of
/// panicking. Other I/O errors still panic.
Expand Down
237 changes: 237 additions & 0 deletions client-common/src/multicast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// 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/.

//! Validated underlay multicast address shared across the routing suite.
//!
//! Lives in the cycle-free leaf crate so the API-types crates consumed by
//! Omicron can share a single definition without depending on
//! `omicron_common`, which would form a dependency cycle.
Comment thread
zeeshanlakhani marked this conversation as resolved.

// TODO: Consolidate these types into `oxnet`, the cycle-free leaf crate that
// maghemite, dendrite, and omicron already share, so the duplication can be
// removed. `dpd_types::mcast::UnderlayMulticastIpv6` in dendrite carries an
// independent copy today.

use crate::address::UNDERLAY_MULTICAST_SUBNET;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::net::{IpAddr, Ipv6Addr};
use std::str::FromStr;
use thiserror::Error;

/// Error constructing an [`UnderlayMulticastIpv6`] address.
#[derive(Debug, Clone, Error)]
pub enum UnderlayMulticastError {
/// The address is not within the underlay multicast subnet (ff04::/64).
#[error(
"underlay address {addr} is not within {UNDERLAY_MULTICAST_SUBNET}"
)]
NotInSubnet { addr: Ipv6Addr },

/// The string could not be parsed as an IPv6 address.
#[error("invalid IPv6 address: {0}")]
InvalidIpv6(#[from] std::net::AddrParseError),
}

/// Error constructing an [`OverlayMulticast`] address.
#[derive(Debug, Clone, Error)]
pub enum OverlayMulticastError {
/// The address is not a multicast address.
#[error("overlay address {addr} is not a multicast address")]
NotMulticast { addr: IpAddr },

/// The string could not be parsed as an IP address.
#[error("invalid IP address: {0}")]
InvalidIp(#[from] std::net::AddrParseError),
}

/// A validated overlay multicast group address (IPv4 or IPv6).
///
/// The application-visible group an operator announces via DDM, e.g.
/// `233.252.0.1` or `ff0e::1`. The overlay group spans both address
/// families, so this type wraps [`IpAddr`] and enforces only that the
/// address is multicast: IPv4 `224.0.0.0/4` per [RFC 1112 §4] or IPv6
/// `ff00::/8` per [RFC 4291 §2.7]. Its admin-local underlay mapping is the
/// separately validated [`UnderlayMulticastIpv6`]. The mapping is defined by
/// [RFD 488].
///
/// [RFC 1112 §4]: https://www.rfc-editor.org/rfc/rfc1112#section-4
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
/// [RFD 488]: https://rfd.shared.oxide.computer/rfd/488
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(try_from = "IpAddr", into = "IpAddr")]
#[schemars(transparent)]
pub struct OverlayMulticast(IpAddr);

impl OverlayMulticast {
/// Create a new validated overlay multicast address.
///
/// # Errors
///
/// Returns [`OverlayMulticastError::NotMulticast`] if the address is not
/// a multicast address.
pub fn new(value: IpAddr) -> Result<Self, OverlayMulticastError> {
if !value.is_multicast() {
return Err(OverlayMulticastError::NotMulticast { addr: value });
}
Ok(Self(value))
}

/// Return the underlying IP address.
#[inline]
pub const fn ip(&self) -> IpAddr {
self.0
}
}

impl fmt::Display for OverlayMulticast {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl TryFrom<IpAddr> for OverlayMulticast {
type Error = OverlayMulticastError;

fn try_from(value: IpAddr) -> Result<Self, Self::Error> {
Self::new(value)
}
}

impl From<OverlayMulticast> for IpAddr {
fn from(addr: OverlayMulticast) -> Self {
addr.0
}
}

impl FromStr for OverlayMulticast {
type Err = OverlayMulticastError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: IpAddr = s.parse()?;
Self::new(addr)
}
}

/// A validated underlay multicast IPv6 address within `ff04::/64`.
///
/// The Oxide rack maps overlay multicast groups 1:1 to admin-local scoped
/// IPv6 multicast addresses in `UNDERLAY_MULTICAST_SUBNET` (`ff04::/64`,
/// admin-local scope per [RFC 4291 §2.7]). The mapping is defined by
/// [RFD 488]. This type enforces the subnet invariant at construction
/// time.
///
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
/// [RFD 488]: https://rfd.shared.oxide.computer/rfd/488
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(try_from = "Ipv6Addr", into = "Ipv6Addr")]
#[schemars(transparent)]
pub struct UnderlayMulticastIpv6(Ipv6Addr);

impl UnderlayMulticastIpv6 {
/// Create a new validated underlay multicast address.
///
/// # Errors
///
/// Returns [`UnderlayMulticastError::NotInSubnet`] if the address is
/// not within `UNDERLAY_MULTICAST_SUBNET` (ff04::/64).
pub fn new(value: Ipv6Addr) -> Result<Self, UnderlayMulticastError> {
if !UNDERLAY_MULTICAST_SUBNET.contains(value) {
return Err(UnderlayMulticastError::NotInSubnet { addr: value });
}
Ok(Self(value))
}

/// Return the underlying IPv6 address.
#[inline]
pub const fn ip(&self) -> Ipv6Addr {
self.0
}
}

impl fmt::Display for UnderlayMulticastIpv6 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl TryFrom<Ipv6Addr> for UnderlayMulticastIpv6 {
type Error = UnderlayMulticastError;

fn try_from(value: Ipv6Addr) -> Result<Self, Self::Error> {
Self::new(value)
}
}

impl From<UnderlayMulticastIpv6> for Ipv6Addr {
fn from(addr: UnderlayMulticastIpv6) -> Self {
addr.0
}
}

impl From<UnderlayMulticastIpv6> for IpAddr {
fn from(addr: UnderlayMulticastIpv6) -> Self {
IpAddr::V6(addr.0)
}
}

impl FromStr for UnderlayMulticastIpv6 {
type Err = UnderlayMulticastError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: Ipv6Addr = s.parse()?;
Self::new(addr)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn overlay_serde_rejects_unicast() {
let json =
serde_json::to_string(&"192.0.2.1".parse::<IpAddr>().unwrap())
.unwrap();
let result: Result<OverlayMulticast, _> = serde_json::from_str(&json);
assert!(result.is_err());
}

#[test]
fn underlay_serde_rejects_invalid() {
// ff0e::1 serialized as an Ipv6Addr, then deserialized as
// UnderlayMulticastIpv6 should fail via try_from.
let json =
serde_json::to_string(&Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 1))
.unwrap();
let result: Result<UnderlayMulticastIpv6, _> =
serde_json::from_str(&json);
assert!(result.is_err());
}
}
Loading