feat(iroh)!: add address filtering and reordering for Address Lookup Services#3960
feat(iroh)!: add address filtering and reordering for Address Lookup Services#3960
Conversation
Also adds `AddrFilter` struct that contains a filter function for filtering and reordering transport addresses before the are published
|
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 |
iroh-relay/src/endpoint_info.rs
Outdated
| /// | ||
| /// Returns a vec to preserve re-ordering of addresses. | ||
| pub fn filtered_addrs(&self, filter: AddrFilter) -> Vec<TransportAddr> { | ||
| filter.apply(self.addrs.clone()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Is there any downside in taking the BTreeSet by ref? |
|
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 |
…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.
|
@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 |
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 Other than that all of this seems fine. |
rklaehn
left a comment
There was a problem hiding this comment.
See my note about the name of IntoAddressLookup, otherwise I think this is fine.
rklaehn
left a comment
There was a problem hiding this comment.
Approved if we rename IntoAddressLookup
c5a9f51 to
5981b6e
Compare
|
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 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 |
## 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)
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 toTransportAddr, allowing users to plug in arbitrary transport layers (e.g., Bluetooth). This sharpens the problem: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:
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
AddrFiltertype and add required methods to theAddressLookupandIntoAddressLookuptraitsthat 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.AddrFilterChanges to
IntoAddressLookupWe need to be able to add the
AddrFilterwhen building the discovery service. To do this, we can add a requiredwith_addr_filtermethod is added toIntoAddressLookupUsage
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 internallyAlternatives
We also change the
AddressLookuptrait to require aset_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_filterorwith_addr_filtermethod on theAddressLookuptrait itself. However, the user would need to hold onto a reference/clone of the address lookup service themselves, since calling this onep.address_lookup().with_addr_filterwould 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
publishexpects anEndpointAddrwith aBTreeSet<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 theEndpointAddrto be a vec, but I don't think we want that?Do something weird with
OrdandTransportAddrTbh 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
AddrFiltermechanism 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
CustomAddrvariants 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:
AddrFilteris applied first.Also, every address lookup implementation should document:
AddrFilteris applied.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
AddrFilteris 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 explicitinclude_direct_addressesoption.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_builderand 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_addressesfor, egDhtAddressLookup, 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
irohremovedDhtAddressLookupBuilder::include_direct_addressesremoved. You can effect what addresses are published by using the newDhtAddressLookupBuilder::set_addr_filtermethod.changedIntoAddressLookuprenamed toAddressLookupBuilderChange checklist