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
6 changes: 6 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 ddm-admin-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2024"
ignored = ["oxnet", "serde", "uuid"]

[dependencies]
mg-common.workspace = true
oxnet.workspace = true
progenitor.workspace = true
reqwest.workspace = true
Expand Down
48 changes: 48 additions & 0 deletions ddm-admin-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,51 @@ impl std::hash::Hash for types::TunnelOrigin {
self.metric.hash(state);
}
}

impl std::cmp::PartialEq for types::Vni {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
}
}

impl std::cmp::Eq for types::Vni {}

impl std::hash::Hash for types::Vni {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}

impl std::cmp::PartialEq for types::MulticastOrigin {
fn eq(&self, other: &Self) -> bool {
self.overlay_group.eq(&other.overlay_group)
&& self.underlay_group.eq(&other.underlay_group)
&& self.vni.eq(&other.vni)
&& self.source.eq(&other.source)
}
}

impl std::cmp::Eq for types::MulticastOrigin {}

/// Metric is excluded from identity so that metric changes update
/// an existing entry rather than creating a duplicate.
impl std::hash::Hash for types::MulticastOrigin {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.overlay_group.hash(state);
self.underlay_group.hash(state);
self.vni.hash(state);
self.source.hash(state);
}
}

impl From<mg_common::net::MulticastOrigin> for types::MulticastOrigin {
fn from(o: mg_common::net::MulticastOrigin) -> Self {
Self {
overlay_group: o.overlay_group,
underlay_group: o.underlay_group.ip(),
vni: types::Vni(o.vni.as_u32()),
metric: o.metric,
source: o.source,
}
}
}
58 changes: 56 additions & 2 deletions ddm-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use ddm_types_versions::latest;
use ddm_types_versions::v1;
use dropshot::HttpError;
use dropshot::HttpResponseOk;
use dropshot::HttpResponseUpdatedNoContent;
use dropshot::Path;
use dropshot::RequestContext;
use dropshot::TypedBody;
use dropshot_api_manager_types::api_versions;
use mg_common::net::TunnelOrigin;
use mg_common::net::{MulticastOrigin, TunnelOrigin};
use oxnet::Ipv6Net;
use std::collections::{HashMap, HashSet};

Expand All @@ -26,6 +27,7 @@ api_versions!([
// | example for the next person.
// v
// (next_int, IDENT),
(2, MULTICAST_SUPPORT),
(1, INITIAL),
]);

Expand All @@ -45,11 +47,25 @@ api_versions!([
pub trait DdmAdminApi {
type Context;

#[endpoint { method = GET, path = "/peers" }]
#[endpoint {
method = GET,
path = "/peers",
versions = VERSION_MULTICAST_SUPPORT..
}]
async fn get_peers(
ctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<HashMap<u32, latest::db::PeerInfo>>, HttpError>;

/// Returns peers without interface name information.
#[endpoint {
method = GET,
path = "/peers",
versions = ..VERSION_MULTICAST_SUPPORT
}]
async fn get_peers_v1(
ctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<HashMap<u32, v1::db::PeerInfo>>, HttpError>;

#[endpoint { method = DELETE, path = "/peers/{addr}" }]
async fn expire_peer(
ctx: RequestContext<Self::Context>,
Expand Down Expand Up @@ -100,6 +116,44 @@ pub trait DdmAdminApi {
request: TypedBody<HashSet<TunnelOrigin>>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

#[endpoint {
method = GET,
path = "/originated_multicast_groups",
versions = VERSION_MULTICAST_SUPPORT..
}]
async fn get_originated_multicast_groups(
ctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<HashSet<MulticastOrigin>>, HttpError>;

#[endpoint {
method = GET,
path = "/multicast_groups",
versions = VERSION_MULTICAST_SUPPORT..
}]
async fn get_multicast_groups(
ctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<HashSet<latest::db::MulticastRoute>>, HttpError>;

#[endpoint {
method = PUT,
path = "/multicast_group",
versions = VERSION_MULTICAST_SUPPORT..
}]
async fn advertise_multicast_groups(
ctx: RequestContext<Self::Context>,
request: TypedBody<HashSet<MulticastOrigin>>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

#[endpoint {
method = DELETE,
path = "/multicast_group",
versions = VERSION_MULTICAST_SUPPORT..
}]
async fn withdraw_multicast_groups(
ctx: RequestContext<Self::Context>,
request: TypedBody<HashSet<MulticastOrigin>>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

#[endpoint { method = PUT, path = "/sync" }]
async fn sync(
ctx: RequestContext<Self::Context>,
Expand Down
5 changes: 4 additions & 1 deletion ddm-types/versions/src/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ pub mod admin {
}

pub mod db {
pub use crate::v1::db::PeerInfo;
pub use crate::v1::db::PeerStatus;
pub use crate::v1::db::RouterKind;
pub use crate::v1::db::TunnelRoute;
pub use crate::v2::db::MulticastRoute;
pub use crate::v2::db::PeerInfo;
}

pub mod exchange {
pub use crate::v1::exchange::PathVector;
pub use crate::v1::exchange::PathVectorV2;
pub use crate::v2::exchange::MulticastPathHop;
pub use crate::v2::exchange::MulticastPathVector;
}
2 changes: 2 additions & 0 deletions ddm-types/versions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@
pub mod latest;
#[path = "initial/mod.rs"]
pub mod v1;
#[path = "multicast_support/mod.rs"]
pub mod v2;
89 changes: 89 additions & 0 deletions ddm-types/versions/src/multicast_support/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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/.

use std::net::Ipv6Addr;

use mg_common::net::MulticastOrigin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::v1::db::{PeerStatus, RouterKind};
use crate::v2::exchange::MulticastPathHop;

/// A multicast route learned via DDM.
///
/// Carries a MulticastOrigin (overlay group + ff04::/64 underlay
/// mapping) and the path vector from the originating subscriber
/// through intermediate transit routers.
// The path enables loop detection and (in multi-rack topologies)
// replication optimizations (RFD 488) in the future.
//
// Equality and hashing consider only `origin` and `nexthop` so that
// a route update with a longer path replaces the existing entry in
// hash-based collections.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MulticastRoute {
/// The multicast group origin information.
pub origin: MulticastOrigin,

/// Underlay nexthop address (DDM peer that advertised this route).
/// Used to associate the route with a peer for expiration.
pub nexthop: Ipv6Addr,

/// Path vector from the originating subscriber outward.
/// Each hop records the router that redistributed this
/// subscription announcement. Used for loop detection on pull
/// and for future replication optimization in multi-rack
/// topologies.
#[serde(default)]
pub path: Vec<MulticastPathHop>,
}

impl PartialEq for MulticastRoute {
fn eq(&self, other: &Self) -> bool {
self.origin == other.origin && self.nexthop == other.nexthop
}
}

impl Eq for MulticastRoute {}

impl std::hash::Hash for MulticastRoute {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.origin.hash(state);
self.nexthop.hash(state);
}
}

impl From<MulticastRoute> for MulticastOrigin {
fn from(x: MulticastRoute) -> Self {
x.origin
}
}

/// Peer information with an optional interface name.
///
// Adds the `if_name` field to identify which underlay interface the peer
// was discovered on.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct PeerInfo {
pub status: PeerStatus,
pub addr: Ipv6Addr,
pub host: String,
pub kind: RouterKind,
/// Interface name the peer was discovered on (e.g., "tfportrear0_0").
#[serde(default)]
pub if_name: Option<String>,
}

/// Downconvert v2 PeerInfo to v1 PeerInfo by dropping `if_name`.
impl From<PeerInfo> for crate::v1::db::PeerInfo {
fn from(p: PeerInfo) -> Self {
Self {
status: p.status,
addr: p.addr,
host: p.host,
kind: p.kind,
}
}
}
74 changes: 74 additions & 0 deletions ddm-types/versions/src/multicast_support/exchange.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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/.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::Ipv6Addr;

/// A single hop in the multicast path, carrying metadata needed for
/// replication optimization.
// Unlike unicast paths which only need hostnames, multicast hops carry
// additional information for computing optimal replication points
// (RFD 488).
#[derive(
Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema,
)]
pub struct MulticastPathHop {
/// Router identifier (hostname).
pub router_id: String,

/// The underlay address of this router (for replication targeting).
pub underlay_addr: Ipv6Addr,

/// Number of downstream subscribers reachable via this hop.
/// Used for load-aware replication decisions in multi-rack
/// topologies.
#[serde(default)]
pub downstream_subscriber_count: u32,
}

impl MulticastPathHop {
/// Create a hop with the given router identity and a zero subscriber
/// count. The count will be populated once transit routers track
/// downstream subscriber counts for load-aware replication (RFD 488).
pub fn new(router_id: String, underlay_addr: Ipv6Addr) -> Self {
Self {
router_id,
underlay_addr,
downstream_subscriber_count: 0,
}
}
}

/// Multicast group subscription announcement propagating through DDM.
///
/// Contains a MulticastOrigin (overlay group + ff04::/64 underlay
/// mapping) and the path from the original subscriber outward.
// Currently, this is used for loop detection: if our router_id appears in the
// path, the announcement has already traversed us and is dropped. The path
// structure also carries topology information for future replication
// optimizations (RFD 488).
#[derive(
Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema,
)]
pub struct MulticastPathVector {
/// The multicast group origin information.
pub origin: mg_common::net::MulticastOrigin,

/// The path from the original subscriber to the current router.
/// Ordered from subscriber outward (subscriber router first).
pub path: Vec<MulticastPathHop>,
}

impl MulticastPathVector {
/// Append a hop to this path vector.
pub fn with_hop(&self, hop: MulticastPathHop) -> Self {
let mut path = self.path.clone();
path.push(hop);
Self {
origin: self.origin.clone(),
path,
}
}
}
9 changes: 9 additions & 0 deletions ddm-types/versions/src/multicast_support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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/.

//! Types from API version 2 (MULTICAST_SUPPORT) that add multicast
//! group management to the DDM admin API.
pub mod db;
pub mod exchange;
Loading