Repository: seanmonstar/reqwest (language: rust)

Issue:
local_address doesn't connect unless interface is highest priority
I've been attempting to use the local address option but have noticed unreliable behaviour whereas curl has no issues.
Test system is Ubuntu 20.04 server.

I have two adapters, one with the address `192.168.1.10` and the other with `192.168.2.10`.
If I use the local_address option and define one of these addresses there is a 50% chance the request will work.

Example route table
![image](https://user-images.githubusercontent.com/13043208/134765888-53dec300-374b-4d3d-aceb-f0bcd04bc209.png)

Now say if the interface with `192.168.1.10` has a higher adapter priority, and then `192.168.2.10` has a lower priority, if I set the local address to `192.168.2.10` the request will timeout and fail.

Example route table
![image](https://user-images.githubusercontent.com/13043208/134765862-ca291a19-8efe-4105-b5d2-06e9cbda4358.png)


logs
```
2021-09-25T08:09:52Z DEBUG reqwest::connect] starting new connection: http://ifconfig.co/
[2021-09-25T08:12:02Z ERROR cli::request] error sending request for url (http://ifconfig.co/): error trying to connect: tcp connect error: Operation timed out (os error 110)
```

However with curl using the interface flag `curl --interface eth1 ifconfig.co` this always works successfully for any interface regardless of the adapter priority. Each of these adapters resolve to a different WAN IP, so have confirmed the request and behaviour has always been reliable and successful via curl. 


Discussion / hints:
Thanks for the report! Interesting... Likely the difference is in curl, you're specifying the interface, and I imagine that is using `SO_BINDTODEVICE` with the interface name. Whereas here (actually in hyper), it's just trying to bind a specific address, using `bind(2)` with just the IP address.

Fix patch:
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 912a5d4bd..1f2f9e605 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -142,6 +142,8 @@ struct Config {
     #[cfg(feature = "http2")]
     http2_keep_alive_while_idle: bool,
     local_address: Option<IpAddr>,
+    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+    interface: Option<String>,
     nodelay: bool,
     #[cfg(feature = "cookies")]
     cookie_store: Option<Arc<dyn cookie::CookieStore>>,
@@ -234,6 +236,8 @@ impl ClientBuilder {
                 #[cfg(feature = "http2")]
                 http2_keep_alive_while_idle: false,
                 local_address: None,
+                #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+                interface: None,
                 nodelay: true,
                 trust_dns: cfg!(feature = "trust-dns"),
                 #[cfg(feature = "cookies")]
@@ -430,6 +434,12 @@ impl ClientBuilder {
                         proxies.clone(),
                         user_agent(&config.headers),
                         config.local_address,
+                        #[cfg(any(
+                            target_os = "android",
+                            target_os = "fuchsia",
+                            target_os = "linux"
+                        ))]
+                        config.interface.as_deref(),
                         config.nodelay,
                         config.tls_info,
                     )?
@@ -441,6 +451,8 @@ impl ClientBuilder {
                     proxies.clone(),
                     user_agent(&config.headers),
                     config.local_address,
+                    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+                    config.interface.as_deref(),
                     config.nodelay,
                     config.tls_info,
                 ),
@@ -456,6 +468,12 @@ impl ClientBuilder {
                             config.quic_receive_window,
                             config.quic_send_window,
                             config.local_address,
+                            #[cfg(any(
+                                target_os = "android",
+                                target_os = "fuchsia",
+                                target_os = "linux"
+                            ))]
+                            config.interface.as_deref(),
                             &config.http_version_pref,
                         )?;
                     }
@@ -466,6 +484,12 @@ impl ClientBuilder {
                         proxies.clone(),
                         user_agent(&config.headers),
                         config.local_address,
+                        #[cfg(any(
+                            target_os = "android",
+                            target_os = "fuchsia",
+                            target_os = "linux"
+                        ))]
+                        config.interface.as_deref(),
                         config.nodelay,
                         config.tls_info,
                     )
@@ -592,6 +616,12 @@ impl ClientBuilder {
                         proxies.clone(),
                         user_agent(&config.headers),
                         config.local_address,
+                        #[cfg(any(
+                            target_os = "android",
+                            target_os = "fuchsia",
+                            target_os = "linux"
+                        ))]
+                        config.interface.as_deref(),
                         config.nodelay,
                         config.tls_info,
                     )
@@ -1229,6 +1259,22 @@ impl ClientBuilder {
         self
     }
 
+    /// Bind to an interface by `SO_BINDTODEVICE`.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// let interface = "lo";
+    /// let client = reqwest::Client::builder()
+    ///     .interface(interface)
+    ///     .build().unwrap();
+    /// ```
+    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+    pub fn interface(mut self, interface: &str) -> ClientBuilder {
+        self.config.interface = Some(interface.to_string());
+        self
+    }
+
     /// Set that all sockets have `SO_KEEPALIVE` set with the supplied duration.
     ///
     /// If `None`, the option will not be set.
@@ -2019,6 +2065,11 @@ impl Config {
             f.field("local_address", v);
         }
 
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        if let Some(ref v) = self.interface {
+            f.field("interface", v);
+        }
+
         if self.nodelay {
             f.field("tcp_nodelay", &true);
         }
diff --git a/src/blocking/client.rs b/src/blocking/client.rs
index 83d1f5675..6c6fb9993 100644
--- a/src/blocking/client.rs
+++ b/src/blocking/client.rs
@@ -513,6 +513,21 @@ impl ClientBuilder {
         self.with_inner(move |inner| inner.local_address(addr))
     }
 
+    /// Bind to an interface by `SO_BINDTODEVICE`.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// let interface = "lo";
+    /// let client = reqwest::blocking::Client::builder()
+    ///     .interface(interface)
+    ///     .build().unwrap();
+    /// ```
+    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+    pub fn interface(self, interface: &str) -> ClientBuilder {
+        self.with_inner(move |inner| inner.interface(interface))
+    }
+
     /// Set that all sockets have `SO_KEEPALIVE` set with the supplied duration.
     ///
     /// If `None`, the option will not be set.
diff --git a/src/connect.rs b/src/connect.rs
index 3ad374021..68ef26924 100644
--- a/src/connect.rs
+++ b/src/connect.rs
@@ -86,6 +86,8 @@ impl Connector {
         proxies: Arc<Vec<Proxy>>,
         user_agent: Option<HeaderValue>,
         local_addr: T,
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        interface: Option<&str>,
         nodelay: bool,
         tls_info: bool,
     ) -> crate::Result<Connector>
@@ -94,7 +96,15 @@ impl Connector {
     {
         let tls = tls.build().map_err(crate::error::builder)?;
         Ok(Self::from_built_default_tls(
-            http, tls, proxies, user_agent, local_addr, nodelay, tls_info,
+            http,
+            tls,
+            proxies,
+            user_agent,
+            local_addr,
+            #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+            interface,
+            nodelay,
+            tls_info,
         ))
     }
 
@@ -105,6 +115,8 @@ impl Connector {
         proxies: Arc<Vec<Proxy>>,
         user_agent: Option<HeaderValue>,
         local_addr: T,
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        interface: Option<&str>,
         nodelay: bool,
         tls_info: bool,
     ) -> Connector
@@ -112,6 +124,10 @@ impl Connector {
         T: Into<Option<IpAddr>>,
     {
         http.set_local_address(local_addr.into());
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        if let Some(interface) = interface {
+            http.set_interface(interface);
+        }
         http.set_nodelay(nodelay);
         http.enforce_http(false);
 
@@ -133,6 +149,8 @@ impl Connector {
         proxies: Arc<Vec<Proxy>>,
         user_agent: Option<HeaderValue>,
         local_addr: T,
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        interface: Option<&str>,
         nodelay: bool,
         tls_info: bool,
     ) -> Connector
@@ -140,6 +158,10 @@ impl Connector {
         T: Into<Option<IpAddr>>,
     {
         http.set_local_address(local_addr.into());
+        #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
+        if let Some(interface) = interface {
+            http.set_interface(interface.to_owned());
+        }
         http.set_nodelay(nodelay);
         http.enforce_http(false);
 


Test patch: