mctx-core 0.2.3

Runtime-agnostic and portable IPv4 and IPv6 multicast sender library.
Documentation
# Usage Guide

`mctx-core` keeps the send path small:

- build a `Context`
- add one or more `PublicationConfig` values
- send payloads through the returned `PublicationId`

## Basic IPv4 Usage

```rust
use mctx_core::{Context, PublicationConfig};
use std::net::Ipv4Addr;

let mut ctx = Context::new();
let id = ctx.add_publication(
    PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000)
        .with_source_addr(Ipv4Addr::new(192, 168, 1, 10))
        .with_ttl(4),
)?;

let report = ctx.send(id, b"hello multicast")?;
println!("source {:?}", report.source_addr);
```

## Basic IPv6 Usage

Same-host IPv6 SSM-style testing:

```rust
use mctx_core::{Context, PublicationConfig};
use std::net::Ipv6Addr;

let mut ctx = Context::new();
let id = ctx.add_publication(
    PublicationConfig::new("ff31::8000:1234".parse::<Ipv6Addr>()?, 5000)
        .with_source_addr(Ipv6Addr::LOCALHOST)
        .with_outgoing_interface(Ipv6Addr::LOCALHOST),
)?;

let report = ctx.send(id, b"hello multicast v6")?;
println!("source {:?}", report.source_addr);
```

Wider-scope IPv6 send:

```rust
use mctx_core::{Context, PublicationConfig};
use std::net::Ipv6Addr;

let mut ctx = Context::new();
let id = ctx.add_publication(
    PublicationConfig::new("ff3e::8000:1234".parse::<Ipv6Addr>()?, 5000)
        .with_source_addr("fd00::10".parse::<Ipv6Addr>()?),
)?;

let report = ctx.send(id, b"hello multicast v6")?;
println!("source {:?}", report.source_addr);
```

## Useful Knobs

- `with_source_addr(...)` pins the exact local wire source
- `with_bind_addr(...)` pins both the local source address and UDP source port
- `with_source_port(...)` binds a deterministic source UDP port
- `with_outgoing_interface(...)` chooses the multicast egress interface by IP
  address
- `with_ipv6_interface_index(...)` chooses the IPv6 multicast egress interface
  by interface index
- `with_ttl(...)` controls IPv4 TTL or IPv6 multicast hop limit
- `with_loopback(...)` toggles local host loopback delivery

## Source Address vs Outgoing Interface

These are different settings:

- source address: which local IP the sender binds before transmitting
- outgoing interface: which interface multicast egress uses

For IPv6, `mctx-core` makes the relationship explicit:

- if you provide an IPv6 source address, `mctx-core` binds that exact address
  and resolves its interface index for `IPV6_MULTICAST_IF`
- if you provide an IPv6 outgoing interface address without a source address,
  `mctx-core` binds to that exact address automatically
- if you provide only an IPv6 interface index, `mctx-core` uses it for
  multicast egress but does not invent a source address for you

This matters for IPv6 SSM-style tests because the receiver's source filter uses
the exact observed sender IP.

## IPv6 Group Guidance

- use `ff3x::/32` groups for IPv6 SSM-oriented testing
- `ff31::/16` is interface-local and is the easiest choice for same-host tests
- `ff32::/16` is link-local and should be paired with a `fe80::...` source
- `ff35::/16` is site-local
- `ff38::/16` is organization-local
- `ff3e::/16` is global scope
- do not treat `ff12::...` as an IPv6 SSM group

`mctx-core` keeps the destination scope ID only for interface-local or
link-local multicast destinations. Wider-scope groups such as `ff35`,
`ff38`, and `ff3e` are connected with destination scope ID `0`.

## Platform Notes

- Windows: do not stuff the interface index into wider-scope IPv6 destination
  addresses; use the bound source plus `IPV6_MULTICAST_IF`
- macOS: link-local groups such as `ff32::/16` should send from `fe80::...`
- Cross-platform: choosing only an interface index is not enough for IPv6
  SSM-style verification when the receiver filters on the exact source IP

## Existing Sockets

If you already manage sockets externally, use
`add_publication_with_socket(...)` or `add_publication_with_udp_socket(...)`.

## Raw Packet Transmit

If your caller already has a complete multicast IPv4 or IPv6 datagram and must
preserve that header on the wire, enable the `raw-packets` feature and use the
parallel raw API instead of the UDP payload API.

```rust
use mctx_core::{RawContext, RawPublicationConfig};
use std::net::Ipv4Addr;

let mut ctx = RawContext::new();
let id = ctx.add_publication(
    RawPublicationConfig::ipv4().with_bind_addr(Ipv4Addr::new(192, 168, 1, 20)),
)?;

let report = ctx.send_raw(id, &ip_datagram)?;
println!("source {:?}", report.source_ip);
```

Important points:

- `send_raw(...)` expects a complete IPv4 or IPv6 datagram, including the IP
  header
- the source IP receivers observe comes from the supplied datagram, not from a
  UDP socket bind
- Linux raw send uses raw IP sockets and typically requires `CAP_NET_RAW` or
  root
- macOS raw send supports IPv4 and IPv6 via raw IP sockets and typically
  requires root
- on Linux and macOS IPv6, the kernel rebuilds the base IPv6 header during raw
  transmit, but the configured source/group tuple, next-header, hop-limit, and
  transport header are preserved for practical AMT/SSM-style UDP forwarding
- Windows raw send currently supports IPv4 only and typically requires
  Administrator rights

See [Raw Packet Transmit](raw-packets.md) for the platform matrix and detailed
limits.

## Python Bindings

If you want to drive the sender from Python, build the sibling `mctx-core-py`
crate. It exposes `Context`, `Publication`, `SendReport`, and a small
`AsyncPublication` helper.

Build and packaging details live in [Python Bindings](python.md).