mctx-core
mctx-core is a runtime-agnostic and portable IPv4 and IPv6 multicast sender
library.
It is built for applications that want a small multicast send core with explicit socket ownership, a non-blocking send path, and optional async or metrics add-ons.
Highlights
- IPv4 multicast send support
- IPv6 multicast send support for ASM and SSM-oriented testing
- Explicit separation between sender source address and outgoing interface
- Exact IPv4 or IPv6 local bind control for announce-style senders
- Predictable IPv6 destination scope handling for
ff31/ff32vsff35/ff38/ff3e - Non-blocking send API
- Immediate-ready publications with caller-owned context and socket extraction
- Caller-provided socket support
- Optional Tokio adapter via the
tokiofeature - Optional send metrics via the
metricsfeature
Install
With the optional Tokio adapter:
With optional metrics:
Quick Start
IPv4:
use ;
use Ipv4Addr;
let mut ctx = new;
let config = new
.with_source_addr
.with_ttl;
let id = ctx.add_publication?;
let report = ctx.send?;
println!;
println!;
IPv6 same-host SSM-style send:
use ;
use Ipv6Addr;
let mut ctx = new;
let config = new
.with_source_addr
.with_outgoing_interface;
let id = ctx.add_publication?;
let report = ctx.send?;
println!;
println!;
Source Address vs Outgoing Interface
mctx-core keeps these concepts distinct:
- source address: the exact local IP the sender binds before transmitting
- outgoing interface: the interface used for multicast egress
For IPv4, these map to the usual bind-address and IP_MULTICAST_IF behavior.
For IPv6, the distinction matters much more:
- if you set
with_source_addr(...)to an IPv6 address,mctx-corebinds that exact local IPv6 address - it also resolves that address to an interface index and sets
IPV6_MULTICAST_IF - if you set
with_outgoing_interface(...)to an IPv6 address and do not setwith_source_addr(...),mctx-coreauto-binds to that exact IPv6 address - if you use
with_ipv6_interface_index(...),mctx-coreuses that interface for multicast egress without inventing a source address for you
This keeps IPv6 SSM-style sender behavior predictable across macOS, Linux, and Windows.
IPv6 SSM Notes
Receiver-side source filtering keys off the exact sender IP, so the sender's bound source address matters.
Group rules:
- valid IPv6 SSM groups are in
ff3x::/32 ff31::/16is interface-local and works well for same-host testsff32::/16is link-local and only works on the local L2 linkff35::/16is site-localff38::/16is organization-localff3e::/16is global scope- do not treat
ff12::...as an IPv6 SSM group
Practical rules:
- for
ff32::/16, send from a link-localfe80::...source - wider-scope groups such as
ff35::...,ff38::..., andff3e::...should use a routable ULA or global source valid on that network - destination scope IDs are only kept for interface-local and link-local groups; they are cleared for wider scopes so Windows does not reject them
Existing Sockets
Use add_publication_with_socket() when you need to create or bind the socket
yourself:
use ;
use ;
use Ipv6Addr;
let mut ctx = new;
let config = new
.with_source_addr
.with_outgoing_interface;
let socket = new?;
let id = ctx.add_publication_with_socket?;
ctx.send?;
Or hand in a std::net::UdpSocket directly:
use ;
use ;
let mut ctx = new;
let config = new;
let socket = bind?;
let id = ctx.add_publication_with_udp_socket?;
ctx.send?;
Event Loop Integration
Borrow the live socket from a publication:
let publication = ctx.get_publication.unwrap;
let socket = publication.socket;
let raw = publication.as_raw_fd;
Or extract the publication and move it into another loop or runtime:
let publication = ctx.take_publication.unwrap;
let parts = publication.into_parts;
let socket = parts.socket;
If you need the exact announce tuple used by the wire format:
let publication = ctx.get_publication.unwrap;
let = publication.announce_tuple?;
Tokio Integration
With the tokio feature enabled, you can wrap an extracted publication and
send asynchronously:
use TokioPublication;
let publication = ctx.take_publication.unwrap;
let publication = new?;
publication.send.await?;
Run the Tokio example with:
Demo Binaries
Basic IPv4 send:
IPv6 same-host SSM-style send:
IPv6 cross-machine SSM-style send on the same network:
IPv6 link-local send:
Optional Metrics
If you need send counters, enable the metrics feature and query snapshots:
let publication = ctx.get_publication.unwrap;
let metrics = publication.metrics_snapshot;
println!;
println!;
mctx_send also supports Heimdall-style single-header JSONL output:
MCTX_METRICS_SUMMARY_FILE=results/sender-0001/network.jsonl \
MCTX_METRICS_SUMMARY_SECS=1 \
node_id defaults to the parent directory of the output path, then the file
stem, and the header flags map can be extended with
MCTX_METRICS_FLAGS_JSON='{"experiment":"baseline"}'.
Documentation
Platform Support
| OS | IPv4 send | IPv6 ASM send | IPv6 SSM-style send | Notes |
|---|---|---|---|---|
| macOS | ✅ | ✅ | ✅ | ff32::/16 should use a fe80:: source |
| Linux | ✅ | ✅ | ✅ | intended support |
| Windows | ✅ | ✅ | ✅ | keep scope ID only for ff31 / ff32 |
License
BSD 2-Clause