Skip to content

relay-hops: prevent namespace loops across relay cycles - #502

Open
mondain wants to merge 4 commits into
mainfrom
moq-hops
Open

relay-hops: prevent namespace loops across relay cycles#502
mondain wants to merge 4 commits into
mainfrom
moq-hops

Conversation

@mondain

@mondain mondain commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • implement draft-lcurley-moq-relay-hops namespace path ingestion and forwarding
  • generate a stable process-lifetime relay Hop ID and stable per-source-session stand-in IDs for legacy peers
  • advertise relay-hops support on all server transports and upstream sessions
  • drop returning advertisements, reject malformed paths, and honor EXCLUDE_HOP
  • add unit coverage plus a three-relay cycle integration test
  • add the local draft, implementation plan, configuration reference, and deployment/configuration guide

Why

Namespace advertisements can circulate indefinitely when connected relays form a cycle. The negotiated hop path lets each relay recognize its own returning advertisement without requiring centrally assigned identifiers or a YAML topology map.

Deployment impact

Relay-hops negotiation is automatic and session-local; there is no new YAML toggle. Non-negotiated peers retain legacy behavior. Any graph segment containing a legacy relay must remain acyclic because that relay cannot preserve the path.

Each service still supports a single upstream. Multi-upstream path selection and failover remain deferred.

Dependency

Validation

  • 15 relay-policy unit cases across single-thread, multi-thread, and local-forwarder modes
  • relay_chain integration test
  • relay_hops_cycle integration test
  • 31 focused moxygen framing and negotiation tests
  • both complete deployment-guide YAML examples pass validate-config --strict_config
  • ./scripts/format.sh --check
  • git diff --check origin/main..HEAD

This change is Reviewable

@mondain
mondain marked this pull request as ready for review July 23, 2026 14:41

@afrind afrind left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall direction seems ok. Some questions/feedback. I think the most substantive bit is around wether we want to use random numbers to synthesize uniqueness, or use the named relayID_ param we already have as a seed. We could also pad it out to some minimum length by repeating it, then hash? It should be deterministic across restarts, I think?

@afrind reviewed 22 files and all commit messages, and made 14 comments.
Reviewable status: all files reviewed, 12 unresolved discussions (waiting on akash-a-n, mondain, and peterchave).


design/RELAY_HOPS_IMPLEMENTATION_PLAN.md line 1 at r1 (raw file):

# MOQT Relay Hops Implementation Plan

I don't think we should check in implementation plans


docs/draft-lcurley-moq-relay-hops.txt line 5 at r1 (raw file):

moq                                                            L. Curley

Move to docs/ref


docs/relay-hops.md line 34 at r1 (raw file):

```bash
sudo deps/moxygen/standalone/install-system-deps.sh
./scripts/build.sh setup

I don't think the relay-hops guide needs to restate how to build and run, only how to deploy a mesh network


docs/relay-hops.md line 105 at r1 (raw file):

support on any other connection.

## Configure a Root Relay

Do you still need a root relay with hops? Do we need to repeat the entire config format


src/MoqxPicoRelayServer.cpp line 90 at r1 (raw file):

      listenerCfg_(listenerCfg), context_(std::move(context)),
      evb_(ioExecutor->getAllEventBases()[0].get()) {
  addSetupParameter(SetupParameter(folly::to_underlying(SetupKey::RELAY_HOPS), std::string{}));

I wonder if we want to universally negotiate this parameter, of if we want to enable it via config. I'm open to hardcoding it now if there's good reason.


src/MoqxRelay.h line 122 at r1 (raw file):

  )
      : relayID_(std::move(relayID)),
        relayHopID_(relayHopID == 0 ? moxygen::generateRelayHopID() : relayHopID),

moqx upstreams have string names. Rather than generate random IDs, perhaps we should hash the names?


src/MoqxRelay.h line 126 at r1 (raw file):

        useLocalForwarders_(useLocalForwarders), maxDeselected_(maxDeselected),
        idleTimeout_(idleTimeout), activityThreshold_(activityThreshold) {
    XCHECK_LE(relayHopID_, moxygen::kMaxRelayHopID);

Is this less than 2^64-1? I guess if relay-hops runs on draft-16 it needs to be


src/MoqxRelay.h line 405 at r1 (raw file):

  moxygen::TrackNamespace allowedNamespacePrefix_;
  std::string relayID_;
  uint64_t relayHopID_;

Yeah having two fields seems confusing.


src/MoqxRelay.cpp line 416 at r1 (raw file):

  std::vector<uint64_t> relayHopPath;
  if (!session->isRelayHopsNegotiated()) {
    relayHopPath.push_back(session->getRelayHopSourceID());

This seems more like a "publisher ID" rather than a relay source ID, since it might not be a relay at all.


src/MoqxRelay.cpp line 418 at r1 (raw file):

    relayHopPath.push_back(session->getRelayHopSourceID());
  } else {
    const auto hopPathKey = folly::to_underlying(TrackRequestParamKey::HOP_PATH);

The next 5 lines or so probably warrant a helper in moxygen like getParamValue or something


src/MoqxRelay.cpp line 429 at r1 (raw file):

    auto version = session->getNegotiatedVersion();
    XCHECK(version.has_value());
    auto decoded = decodeRelayHopPath(it->asString, *version);

This might be feedback for Luke -- is the hop path encoded canonically with QUIC varints or is it based on the underlying transport protocol version (which is what I think you have here).


src/MoqxRelay.cpp line 1574 at r1 (raw file):

        if (node->publisherSession() && node->publisherSession() != session &&
            (incomingPeerID.empty() || node->publisherPeerID() != incomingPeerID)) {
          if (excludesHop(excludeHop, node->relayHopPath(), relayHopID_)) {

You didn't start this fire, but this seems to be duplicated with the above section and we should see if we can use a helper to reduce duplication and boilerplate?


src/MoqxRelayContext.cpp line 50 at r1 (raw file):

          MoqxRelay::kDefaultIdleTimeout,
          MoqxRelay::kDefaultActivityThreshold,
          relayHopID_

Reorder this param to be closer to relayID (unless we replace it with hash(relayID), then you don't have to pass the defaults here.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Reviewable

Addressed in 8df7fd4, with moxygen dependency commit 41f3bfb6. Hop IDs remain random because the draft requires opaque randomly chosen values that are stable for the endpoint lifetime, rather than deterministic across restarts. Generation and legacy-publisher ownership now live in moqx, while relayID remains the operational routing identity.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

design/RELAY_HOPS_IMPLEMENTATION_PLAN.md line 1 at r1

Removed the checked-in implementation plan.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

docs/draft-lcurley-moq-relay-hops.txt line 5 at r1

Moved the draft to docs/ref/draft-lcurley-moq-relay-hops.txt and updated the guide link.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

docs/relay-hops.md line 34 at r1

Removed the duplicated build, basic run, container, and general configuration instructions. The guide now focuses on relay-hop negotiation, mesh and cycle deployment, rolling upgrades, and verification.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

docs/relay-hops.md line 105 at r1

Removed the root-relay and full configuration walkthroughs. Relay hops do not require a distinguished root; the revised guide describes only the upstream relationships needed for a mesh or cycle.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxPicoRelayServer.cpp line 90 at r1

Kept universal advertisement for now. Negotiation is bilateral and session-local, so the wire extension remains inactive with an unsupported peer, while advertising it consistently across listeners and upstream clients avoids configuration-dependent loop-safety gaps. Added comments and deployment guidance explaining that choice.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.h line 122 at r1

Kept Hop IDs random rather than hashing relayID because draft-lcurley-moq-relay-hops requires each relay and origin publisher to choose an opaque random Hop ID. relayID remains the operational string identity; relayHopID is stable for the process context lifetime and may change after restart as the draft permits.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.h line 126 at r1

Moved the bound into moqx and defined it as 2^62 minus 1, matching the maximum QUIC variable-length integer used by draft 16. Generation masks to that bound and rejects zero.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.h line 405 at r1

Kept both fields because they have different contracts: relayID is an operational string used for authentication and upstream routing, while relayHopID is the opaque random protocol loop identifier. Added comments at the declarations and moved Hop ID generation fully into moqx to make the ownership explicit.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.cpp line 416 at r1

Removed getRelayHopSourceID from moxygen. moqx now assigns each non-negotiating publisher session a stable random legacy-publisher Hop ID, stores it with a weak session reference, and prunes expired entries.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.cpp line 418 at r1

Added Parameters::getFirstParam in moxygen and use it here for HOP_PATH and EXCLUDE_HOP lookup.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.cpp line 429 at r1

The encoding is version-aware: draft 16 uses QUIC variable-length integers and draft 17 or later uses the MOQT variable-integer encoding. encodeRelayHopPath and decodeRelayHopPath dispatch from the negotiated MOQT version, and the tests cover drafts 16, 17, and 18.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelay.cpp line 1574 at r1

Extracted shouldForwardNamespace and now use it for both immediate forwarding and replay of existing namespaces. It centralizes source-session suppression, subscription options, and EXCLUDE_HOP filtering.

@mondain

mondain commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

src/MoqxRelayContext.cpp line 50 at r1

Moved relayHopID next to relayID in the MoqxRelay constructor and updated internal call sites, eliminating the need to pass every intervening default. The public MoqxRelayContext argument order remains unchanged to preserve source compatibility.

@michalhosna michalhosna added this to the Milestone 3 milestone Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants