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

Client feature proposal: set_local_address #1498

Closed
kw217 opened this issue Apr 24, 2018 · 3 comments
Closed

Client feature proposal: set_local_address #1498

kw217 opened this issue Apr 24, 2018 · 3 comments
Assignees
Labels
A-client Area: client. C-feature Category: feature. This is adding a new feature. E-easy Effort: easy. A task that would be a great starting point for a new contributor.

Comments

@kw217
Copy link
Contributor

kw217 commented Apr 24, 2018

Hi,

I am using the Hyper client on a host which has multiple network interfaces. I would like to issue an HTTP request over a specific interface. The syntax should be something like this:

    let bind_addr: Option<IpAddr> = Some("127.0.0.1".parse().unwrap());
    let client = Client::configure().set_local_address(bind_addr).build(&handle);

Below I propose:

  • A configuration syntax.
  • A way of implementing this architecturally, which involves changes to mio and tokio-core.

Would you be happy to receive a PR along these lines, and do you think the approach of enhancing mio and tokio-core is best, or should we simply change hyper?

Details follow.

Uncontroversially we need the following plumbing:

  • in client/mod.rs we add local_address: Option<IpAddr> to Config, default it to None, add set_local_address, and make build set it on HttpConnector if present.
  • Similarly, we add local_address: Option<IpAddr> to the fields of HttpConnector and provide a setter.
  • To convey the local address to the point where the connection is done, the Lazy and Resolving states of HttpConnecting gain an extra tuple field Option<IpAddr>, and ConnectingTcp gains a local_address field. The same will also be required in the hyper-tls connector.

The interesting part is what we replace self.current = tokio::net::TcpStream::connect(&addr, handle); with in ConnectingTcp.

Doing it all purely in Hyper looks like this (replacing a few lines of impl ConnectingTcp around https://github.com/hyperium/hyper/blob/master/src/client/connect.rs#L426):

              if let Some(addr) = self.addrs.next() {
                debug!("connecting to {}", addr);
                let do_bind = |local: &Option<IpAddr>| {
                    let sock = match addr {
                        SocketAddr::V4(..) => TcpBuilder::new_v4(),
                        SocketAddr::V6(..) => TcpBuilder::new_v6(),
                    }?;
                    match local {
                        &None => (), // oops, should add special Windows knowledge here
                        &Some(ref local_addr) => sock.bind(SocketAddr::new(local_addr.clone(), 0)).map(|_| ())?,
                    };
                    sock.to_tcp_stream()
                };

                match do_bind(&self.local_address) {
                    Ok(tcp) => {
                        self.current = Some(mio::net::TcpStream::connect_stream(tcp, &addr, handle));
                        continue;
                    },
                    Err(e) => {
                        err = Some(e)
                    }
                };
            }

This direct approach is unpleasant because it violates the architectural layering and duplicates code. In particular, this code is taken almost verbatim from mio::net::TcpStream::connect() – and as you can see there, it should really include a special Windows workaround here.

It also loses the zero-allocation property: Tokio’s TcpStream::connect_stream can only return a Box<Future>, so we have to change ConnectingTcp to use this boxed future rather than the bare Tokio TcpStreamNew (which we cannot construct outside Tokio).

How could we preserve the architectural layering?

In mio::net::TcpStream we should add a connect_with_local_address method beside connect, to encapsulate the Windows and socket knowledge. We should then expose that in a new tokio::net::TcpStream::connect_with_local_address method. Then this method is available to be used Hyper’s ConnectingTcp:

              if let Some(addr) = self.addrs.next() {
                debug!("connecting to {}", addr);
                self.current = tokio::net::TcpStream::connect_with_local_address(&addr, &local, handle);

I think this a cleaner approach architecturally, and it is also able to preserve zero-allocation. But it does involve making changes to three projects. Which is the best approach?

@seanmonstar
Copy link
Member

Hey, thanks for the detailed write-up! It really helps me to understand what the end goal is. In this case, it seems like a fine feature for an advanced configuration to allow binding to a local address before connecting to a remote. Some jumbled thoughts:

  • I'd probably suggest that it should be an option when configuring an HttpConnector, and not the Client directly, since it doesn't make sense in a generic fashion.
  • It seems we can use TcpStream::connect_std, which doesn't return a boxed future. So, there shouldn't be need to make changes to mio and tokio, right?

@kw217
Copy link
Contributor Author

kw217 commented Apr 25, 2018

Thanks @seanmonstar. Yes, making it an option on the connector makes lots of sense, so it looks like this:

let bind_addr: Option<IpAddr> = Some("127.0.0.1".parse().unwrap());
let mut connector = HttpConnector::new(4);
connector.set_local_address(bind_addr);
let client = Client::builder().build(connector);

Aha, I see now that you've upgraded away from tokio-core (I was looking at the latest 0.11 release, not master). It looks a bit simpler now, and I see the Windows knowledge is already in Hyper so the need for it is no longer a concern. Thanks!

However there is one remaining problem - where previously I had to allocate, now I think there's a case that isn't expressible at all. hyper::client::http::connect() takes an Option<Handle>, and I can extend it to also take a local_address: Option<IpAddr>. If the handle is present, I can bind to the local address before calling TcpStream::connect_std. But if the handle is absent, the only method I can call is TcpStream::connect - and that doesn't provide any way to bind first. Am I safe to call just Handle::current() here and call TcpStream::connect_std anyway, or is something missing from tokio?

@seanmonstar
Copy link
Member

Yes, calling Handle::current() at that point is exactly what TcpStream::connect(addr) does internally, so that's fine. The point is just to call it directly before needing it, instead of calling Handle::current() when the HttpConnector is created, since at that point, the reactor may not be setup yet.

kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 26, 2018
kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 26, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 26, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 26, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
@seanmonstar seanmonstar added A-client Area: client. E-easy Effort: easy. A task that would be a great starting point for a new contributor. C-feature Category: feature. This is adding a new feature. labels Apr 27, 2018
kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 30, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
kw217 added a commit to Metaswitch/hyper that referenced this issue Apr 30, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
kw217 added a commit to Metaswitch/hyper that referenced this issue May 1, 2018
Add `set_local_address` to the `HttpConnector`.
This configures the client to bind the socket to a local address of
the host before it connects to the destination. This is useful on
hosts which have multiple network interfaces, to ensure the request is
issued over a specific interface.

Closes hyperium#1498
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-client Area: client. C-feature Category: feature. This is adding a new feature. E-easy Effort: easy. A task that would be a great starting point for a new contributor.
Projects
None yet
Development

No branches or pull requests

2 participants