Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route get supports querying the route tables #23

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Orycterope
Copy link

Closes #22

This MR adds support for querying the route tables for the best route to a given destination, which as of now only supports dumping all route tables.

Design

The idea behind the design is pretty simple and inspired by what route::add and neighbour::get do: the user gets a RouteRequest<()> from the handle, which they must specialize into either a RouteRequest<Ipv4Addr> or RouteRequest<Ipv6Addr> by calling v4() or v6() before being able to call execute(). Once specialized, the user can optionally add a destination address, but only of the same address family as the generic parameter (you can't put a ipv6 destination in RouteRequest<Ipv4Addr>). Doing this will turn the request into a lookup request instead of dump request.

Unfortunately, this design introduces a change to the API for the user, and will require a version bump.

It also adds a new examples/get_route_to.rs example to showcase this use case.

execute() opaque return type

While writing this PR, I faced a lot of troubles related to opaque types. Like all other requests in this crate, RouteRequest::execute() used to return a opaque impl TryStream<Ok = RouteMessage, Error = Error>. This becomes a problem if the user tries to handle ipv4 and ipv6 in a single code path, like so:

/// Dump either Ipv4 or Ipv6 routes
async fn dump_all_routes(
    handle: Handle,
    ip_version: IpVersion,
) -> Result<(), Error> {
    let mut routes = match ip_version {
        IpVersion::V4 => handle.route().get().v4().execute(),
        IpVersion::V6 => handle.route().get().v6().execute(),
    };
    while let Some(route) = routes.try_next().await? {
        println!("{route:?}");
    }
    Ok(())
}

Because RouteRequest is now generic, the return type of RouteRequest<Ipv4Addr> and RouteRequest<Ipv6Addr> are no longer the same concrete type, even though they have the exact same opaque type signature. Because of this, the two match arms differ in type, and routes cannot have a valid type.

The only way I found around this problem is dynamic dispatch, which means asking the user to turn the concrete opaque type returned by execute() into a Box<dyn TryStream<...>> in both match arms. But for that the user has to write the full type signature of the trait, including the associated type Steam::Item which is erased in the returned opaque type, and this unprovable constraint makes rustc refuse to compile.

I solved this by changing the opaque type returned by execute() into a simpler opaque impl Stream<Item = Result<...>>, which automatically implements TryStream anyway, so this is transparent for the user.

The final solution is

    let mut routes: Box<
        dyn Stream<Item = Result<RouteMessage, rtnetlink::Error>> + Unpin,
    > = match ip_version {
        IpVersion::V4 => Box::new(handle.route().get().v4().execute()),
        IpVersion::V6 => Box::new(handle.route().get().v6().execute()),
    };
    while let Some(route) = routes.try_next().await? {
        println!("{route:?}");
    }

which I'm not super happy about, because it asks the user to write a complicated dyn type.

Another solution would have been to make RouteRequest an exception, and have execute() return a Box<Stream<Item = ...>.

};

use crate::{try_rtnl, Error, Handle};

pub struct RouteGetRequest {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like the generic struct is not required here, you can just use the generic function parameters.

Copy link
Author

Choose a reason for hiding this comment

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

The generic parameter on the struct prevent the user from calling to(ipv6_ipaddr) on an ipv4 RouteGetRequest because it would result in an unsupported rtnetlink request. Once the RouteGetRequest<()> is specialized into a RouteGetRequest<Ipv4Addr>, you can only pass it ipv4 addresses when calling to and from. This is enforced by the type system, and there is no way for the user to turn it into a RouteGetRequest<Ipv6Addr>.

Moving the generics on the function instead would not prevent the user to create a RouteGetRequest, call to::<Ipv4Addr>(dest_ip) and then from::<Ipv6Addr>(source_ip), which is invalid.

@cathay4t
Copy link
Member

This looks similar to #20 . Please try not not break API.

@cathay4t
Copy link
Member

@Orycterope This break the API which is widely used, can we find a way for this without breaking API?

@cathay4t
Copy link
Member

@Orycterope ping again

@cathay4t cathay4t added the stale label Mar 28, 2024
@pseusys
Copy link
Contributor

pseusys commented May 14, 2024

I like these changes and would like to use them. Could you please indicate more clear, what API has been broken? Maybe I could've fixed it.

@hack3ric hack3ric mentioned this pull request Nov 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Looking up best route to destination
4 participants