Skip to content

feat(iroh)!: add address filtering and reordering for Address Lookup Services#3960

Merged
ramfox merged 16 commits intomainfrom
ramfox/filter-addr
Feb 27, 2026
Merged

feat(iroh)!: add address filtering and reordering for Address Lookup Services#3960
ramfox merged 16 commits intomainfrom
ramfox/filter-addr

Conversation

@ramfox
Copy link
Copy Markdown
Member

@ramfox ramfox commented Feb 20, 2026

Address Filtering for Address Lookup Services

Why

The Context

PR #3691 surfaced an assumption mismatch between users and iroh's address lookup service. This user expected that relay URLs were being published over mDNS and could not understand why connections weren't being established. We never considered publishing relay URLs on mDNS internally and so our implementation only publishes ip addresses and user defined data.

It turned out there was a valid reason the user needed this: their environment had a firewall that permitted broadcast UDP traffic (which mDNS uses) but blocked other kinds of UDP traffic. So mDNS address lookup was working, but without a relay URL in the advertisement, the two endpoints had no way to reach each other — direct connections were blocked and there was no relay fallback advertised. The fix for their use case was to include the relay URL in mDNS advertisements.

This incident revealed that we had left the choice of what to and not to publish to the address lookup implementations entirely, and didn't leave room for the possibility/flexibility that users may have specific needs while advertising their addresses.

Custom Transports Make This Worse

The upcoming Custom Transports feature will introduce a Custom(CustomAddr) variant to TransportAddr, allowing users to plug in arbitrary transport layers (e.g., Bluetooth). This sharpens the problem:

  • Users may want to advertise a custom transport address on one address lookup service but not another.
  • Most of our existing address lookup services are built on top of DNS TXT records, which have hard byte-size limits. Custom transport addresses will likely need to be encoded into these records, and there may simply not be room for all of them, and we have no guidelines on what to do if this happens.
  • There is no current mechanism for users to express preferences about which addresses get published where.

This is manageable — an address lookup service can document its constraints and make best-effort decisions about what it can fit — but we need to give users a way to participate in that decision. For example, in a byte-constrained mDNS advertisement, a user might want to ensure their preferred custom transport address is published first, rather than leaving the ordering up to the address lookup service itself.

So What Do We Need?

We need a mechanism that:

  1. Makes it clear to users that they can influence which addresses each address lookup service publishes.
  2. Makes it clear to address lookup implementors that they need to reckon with publishing addresses that are not just IP addrs.
  3. Still allows each address lookup service to make its own final decisions about what it can actually publish, given its own constraints.

The fourth and nice-to-have would be some way to force the address lookup implementors into ensuring they have good documentation about the preference of the address lookup service when it comes to illustrating what it can and cannot publish (in terms of kind of addresses as well as number of addresses). Haven't figured this out, so it may just be that we lead by example and have good documentation ourselves.


What

Proposed Design

We introduce a concrete AddrFilter type and add required methods to the AddressLookup and IntoAddressLookup traits that ensures every address lookup implementation accepts a filter. Whether or not they use/respect the filter, is not something we can control in this setup, but it at least gives the expectation that we expect it.

AddrFilter

/// A filter and/or reordering function applied to transport addresses before publishing.
///
/// Takes the full set of transport addresses and returns them as an ordered `Vec`,
/// allowing both filtering (by omitting addresses) and reordering (by controlling
/// the output order). A `BTreeSet` cannot preserve a custom order, so the return
/// type is `Vec` to make reordering possible.
///
/// See the documentation for each address lookup implementation for details on
/// what additional filtering the implementation may perform on top.
// KASEY:
/// Our recommendation would be that the address lookup service will apply this
/// before its own internal filtering, before it publishes.
#[derive(Clone)]
pub struct AddrFilter(
    Arc<dyn Fn(BTreeSet<TransportAddr>) -> Vec<TransportAddr> + Send + Sync + 'static>
);

impl AddrFilter {
    // KASEY: I don't know if this is necessary, but would likely make it nicer to
    // work with
    pub fn new(
        f: impl Fn(BTreeSet<TransportAddr>) -> Vec<TransportAddr> + Send + Sync + 'static
    ) -> Self {
        Self(Arc::new(f))
    }

    // KASEY: I don't know if this is necessary, but would likely make it nicer to
    // work with
    pub fn apply(&self, addrs: BTreeSet<TransportAddr>) -> Vec<TransportAddr> {
        (self.0)(addrs)
    }
}

Changes to IntoAddressLookup

We need to be able to add the AddrFilter when building the discovery service. To do this, we can add a required with_addr_filter method is added to IntoAddressLookup

pub trait IntoAddressLookup: std::fmt::Debug + Send + Sync + 'static {
    /// Sets the address filter for this address lookup service.
    fn with_addr_filter(self, filter: AddrFilter) -> Self where Self: Sized;

    fn into_address_lookup(
        self,
        endpoint: &Endpoint,
    ) -> Result<impl AddressLookup, IntoAddressLookupError>;
}

Usage

What would this look like?

endpoint
    .address_lookup(PkarrPublisher::n0_dns()
        .with_addr_filter(AddrFilter::new(|addrs| {
            // only publish relay and IP addresses to pkarr, not custom transports,
            // returned as a Vec to allow reordering
            addrs.into_iter()
                .filter(|a| !matches!(a, TransportAddr::Custom(_)))
                .collect::<Vec<_>>()
        })))
    .address_lookup(MyBluetoothLookup::new()) // handles Custom addrs internally

Alternatives

We also change the AddressLookup trait to require a set_addr_filter.

It's likely we only need to set this once, when the address lookup is built. BUT if we want to give users the ability to adjust this dynamically, it may be best to also add a set_addr_filter or with_addr_filter method on the AddressLookup trait itself. However, the user would need to hold onto a reference/clone of the address lookup service themselves, since calling this on ep.address_lookup().with_addr_filter would likely mean setting that specific filter to all the different lookup services that exists. But maybe we want that functionality too!

Each address lookup system has its own builder methods, no trait change

Each address lookup service builder could independently add a filter method to its own builder. No changes to the traits would be required.

I rejected this because it meant no consistency and no way to enforce even limited consistency/expectations. Users have no reason to expect filtering to be available or to look for it and address lookup service implementators get no guidance.

This isn't a concern for the address lookup services at all - but for the endpoint

The other option is that this isn't a setting on the address lookup service, but is instead a setting on the endpoint. There are two issues here, I believe.
First, this would likely mean having a single blanket filter/reduce, so the user can't customize it per-address lookup. Second, since publish expects an EndpointAddr with a BTreeSet<TransportAddr> we cannot preserve order once we turn the vec back into a btreeset. The other other option would be then to adjust the field in the EndpointAddr to be a vec, but I don't think we want that?

Do something weird with Ord and TransportAddr

Tbh I'm not sure where you would start with this, since you would want it to be user supplied...

Other options?

I am open to hearing them :)


Potential issues

This gives tools and expectations, not guarantees

The AddrFilter mechanism gives users a way to express intent — "these are the addresses I want published to this service" — and creates the expectation that the address lookup service will use it. But the address lookup service is the final authority over what it actually publishes, so it could decide not to use the user supplied filter and can decide for itself what addresses it wants to publish.

But likely, this is for the best. Address lookup services can have structural constraints the user cannot override. Eg, An address lookup service that encodes addresses as DNS TXT records cannot publish infinite CustomAddr variants it needs to be able to confidently only publish what it can. Other services may have reasons they can't publish them entirely.

But this also means we should have a lot of documentation about our expectations.

The filter application should be consistent across all implementation:

  1. The user-provided AddrFilter is applied first.
  2. The address lookup service's own internal filtering is applied second.

Also, every address lookup implementation should document:

  • Whether and how the user-provided AddrFilter is applied.
  • What additional internal filtering the implementation applies (e.g., "only relay URLs are published; direct addresses are dropped regardless of the filter").
  • Any capacity limits that may cause addresses to be dropped even after filtering (e.g., "only the first N addresses are published due to DNS TXT record size constraints").
  • When capacity limits exist, whether addresses are dropped silently or with a warning log.

The goal is that a user reading the documentation for any address lookup service can predict exactly what will be published, without reading the source.

Feedback

I'd love a way to have more guarantees here, but I haven't figured out the structure that would allow it. In fact, potentially the fact that this isn't a 100% rigid structure may actually be beneficial for some address lookup services.

Behavior Changes

By default, no address lookup service applies any filtering — all addresses are passed through to publishing as-is. Filtering only occurs if an AddrFilter is explicitly set, either via a preset or a user-supplied closure. This is a breaking change for Pkarr publishing, which checked to see if a relay url existed, and if so, cleared all ip addresses, and Dht publishing, which had an explicit include_direct_addresses option.

For users using our defaults, this will not effect them. For users that have relays disabled, this will effect them if they use the default n0 preset, BUT, likely this will not effect most, because a user that has disabled relays they probably have not used the default n0 preset, and instead used the empty_builder and built whatever address lookup services they needed on their own.

Presets

Since we are leaving the choice for what to publish to the users, I've updated the Presets to include that the default explicitly filters address to only include the relay url. This is the same behaviour as before, but needed to be updated because the option to include_direct_addresses for, eg DhtAddressLookup, was removed.

New preset: N0DisabledRelay
I added a new preset that filters relays from published addresses and sets the relay mode to disabled. Not married to keeping it, but could be a good shorthand for folks. Did not add this as an option on the Endpoint::builder (eg Endpoint::disabled_relay_builder), however.

Breaking Changes

  • iroh
    • removed
      • DhtAddressLookupBuilder::include_direct_addresses removed. You can effect what addresses are published by using the new DhtAddressLookupBuilder::set_addr_filter method.
    • changed
      • trait IntoAddressLookup renamed to AddressLookupBuilder

Change checklist

  • Self-review.
  • Documentation updates following the style guide, if relevant.
  • Tests if relevant.
  • All breaking changes documented.

Also adds `AddrFilter` struct that contains a filter function for filtering and reordering transport addresses before the are published
@n0bot n0bot bot added this to iroh Feb 20, 2026
@github-project-automation github-project-automation bot moved this to 🚑 Needs Triage in iroh Feb 20, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 20, 2026

Documentation for this PR has been generated and is available at: https://n0-computer.github.io/iroh/pr/3960/docs/iroh/

Last updated: 2026-02-27T16:40:39Z

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 20, 2026

Netsim report & logs for this PR have been generated and is available at: LOGS
This report will remain available for 3 days.

Last updated for commit: f7e90c8

@ramfox ramfox moved this from 🚑 Needs Triage to 🏗 In progress in iroh Feb 20, 2026
@ramfox ramfox added this to the iroh: v0.98 milestone Feb 20, 2026
@ramfox ramfox self-assigned this Feb 20, 2026
@ramfox ramfox added the address-lookup iroh's address lookup subsystem, that turns endpoint id's into the endpoint's addresses label Feb 20, 2026
///
/// Returns a vec to preserve re-ordering of addresses.
pub fn filtered_addrs(&self, filter: AddrFilter) -> Vec<TransportAddr> {
filter.apply(self.addrs.clone())
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.

We are cloning the addrs and then making a vec out of them. This seems inefficient.

The order of elements in a BTreeSet is the natural order of the elements according to its Ord instance. And this takes an owned BTreeSet. So I see 2 options: 1. we filter the cloned BTreeSet in place or 2. we change this API to return an iterator and avoid the new allocation entirely.

Not sure if the latter will work with all the dyn, but currently I don't see why it would not work.

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.

Ah, never mind, you also want to allow reordering. Still, the two copies are unnecessary. I guess the comment should be "to allow reordering" then.

@rklaehn
Copy link
Copy Markdown
Contributor

rklaehn commented Feb 25, 2026

Is there any downside in taking the BTreeSet by ref?

#3972

@rklaehn
Copy link
Copy Markdown
Contributor

rklaehn commented Feb 25, 2026

Not quite sure if this is a net win, but it does reduce boilerplate for all AddrLookup implementers:

https://github.com/n0-computer/iroh/tree/rklaehn/identity-filter

rklaehn and others added 4 commits February 25, 2026 11:20
…copy. (#3972)

## Description

We still return a Vec so we can reorder.

Also adds common factory functions for relay only and ip only
That way we don't have to wrap it in an Option everywhere.
@ramfox
Copy link
Copy Markdown
Member Author

ramfox commented Feb 25, 2026

@rklaehn merged your PR in and cherry picked the identity commit in too. Very in favor of all those changes.

Fixed the doc line for EndpointInfo::filtered_addrs as well.

@ramfox ramfox requested a review from rklaehn February 25, 2026 17:41
@rklaehn
Copy link
Copy Markdown
Contributor

rklaehn commented Feb 27, 2026

Custom Transports Make This Worse

I don't really think custom transports are that different. You just need more filters. Custom transport addresses will have a default way to be stored, printed, parsed. So it's just another addr type. ip_only and relay_only will already get rid of them.

So, to clarify: the fn with_addr_filter on the IntoAddressLookup is just to set the expectation that a builder should have a way to set the filter. It is not called when creating the AddressLookup, otherwise the filter could just be an argument to into_address_lookup.

Also, for the default impl we can't implement it and just return self. If you already have an AddressLookup there is nothing you can do because as of now we don't implement dynamic filter change.

So yes, that makes sense. Make the builders consistent. But then the name IntoAddressLookup somehow seems weird. An Into... trait that has some weird extra method? Maybe we can just make this trait the trait you should implement for builders, so maybe call it just AddressLookupBuilder?

Other than that all of this seems fine.

Copy link
Copy Markdown
Contributor

@rklaehn rklaehn left a comment

Choose a reason for hiding this comment

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

See my note about the name of IntoAddressLookup, otherwise I think this is fine.

Copy link
Copy Markdown
Contributor

@rklaehn rklaehn left a comment

Choose a reason for hiding this comment

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

Approved if we rename IntoAddressLookup

@ramfox ramfox force-pushed the ramfox/filter-addr branch from c5a9f51 to 5981b6e Compare February 27, 2026 16:22
@ramfox ramfox changed the title feat(iroh): add address filtering and reordering for Address Lookup Services feat(iroh)!: add address filtering and reordering for Address Lookup Services Feb 27, 2026
@ramfox ramfox added this pull request to the merge queue Feb 27, 2026
Merged via the queue into main with commit d33a32f Feb 27, 2026
29 of 30 checks passed
@github-project-automation github-project-automation bot moved this from 🏗 In progress to ✅ Done in iroh Feb 27, 2026
@Frando
Copy link
Copy Markdown
Member

Frando commented Feb 28, 2026

I'm having another look at this even though it's merged - and am wondering why you didn't go for a combinator style approach instead of having each address lookup impl deal with the address filter themselves?

i.e. have AddresssLookupBuilder::with_address_filter be a default impl on the trait that returns Filtered<Box<dyn DynAddressLookupBuilder>> and build() on that returns a Filtered<AddressLookup> which handles the filter-before-publish.

I can give it a try but before doing so wanted to check if this was considered already or not.

Edit: Gave it a try here: #3987

@ramfox ramfox deleted the ramfox/filter-addr branch March 2, 2026 14:04
github-merge-queue bot pushed a commit that referenced this pull request Mar 6, 2026
## Description

This changes the address filtering API added in #3960: Instead of having
each service deal with the filtering, we layer this as a combinator on
top. `AddressLookupBuilder` now has a `with_addr_filter` method with a
default impl, and returns a `FilteredBuilder` which again impls
`AddressLookupBuilder` itself. Through this, this all works
transparently as currently, but the individual services don't need to
care about the filtering at all.

## Breaking Changes

<!-- Optional, if there are any breaking changes document them,
including how to migrate older code. -->

## Notes & open questions


Are there any downsides? This would save people who impl address lookup
services from having to deal with the filtering themselves.

## Change checklist
<!-- Remove any that are not relevant. -->
- [ ] Self-review.
- [ ] Documentation updates following the [style
guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text),
if relevant.
- [ ] Tests if relevant.
- [ ] All breaking changes documented.
- [ ] List all breaking changes in the above "Breaking Changes" section.
- [ ] Open an issue or PR on any number0 repos that are affected by this
breaking change. Give guidance on how the updates should be handled or
do the actual updates themselves. The major ones are:
    - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc)
    - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip)
    - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs)
    - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe)
    - [ ] [`sendme`](https://github.com/n0-computer/sendme)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

address-lookup iroh's address lookup subsystem, that turns endpoint id's into the endpoint's addresses

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants