From ddd54e9b20427b716e13d83884b4b0db03953210 Mon Sep 17 00:00:00 2001
From: cgzones <cgzones@googlemail.com>
Date: Thu, 1 Dec 2022 19:51:04 +0100
Subject: [PATCH] feat(localip): use reserved remote address (#4648)

Instead of the remote address of 8.8.8.8 (Google DNS) in the crate
local_ipaddress use a reserved IPv4 address, that should never be
assigned.
Also forward the underlying error on failure.

Supersedes: #4614
---
 Cargo.lock             |  7 ------
 Cargo.toml             |  1 -
 src/modules/localip.rs | 57 +++++++++++++++++++++++++++++-------------
 3 files changed, 40 insertions(+), 25 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 3bbd4fb9..072f277d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1630,12 +1630,6 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
 
-[[package]]
-name = "local_ipaddress"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6a104730949fbc4c78e4fa98ed769ca0faa02e9818936b61032d2d77526afa9"
-
 [[package]]
 name = "lock_api"
 version = "0.4.8"
@@ -2770,7 +2764,6 @@ dependencies = [
  "guess_host_triple",
  "home",
  "indexmap",
- "local_ipaddress",
  "log",
  "mockall",
  "nix 0.26.1",
diff --git a/Cargo.toml b/Cargo.toml
index 9fc98129..43f432a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,7 +52,6 @@ git-features = { version = "0.24.1", optional = true }
 # default feature restriction addresses https://github.com/starship/starship/issues/4251
 git-repository = { version = "0.29.0", default-features = false, features = ["max-performance-safe"] }
 indexmap = { version = "1.9.2", features = ["serde"] }
-local_ipaddress = "0.1.3"
 log = { version = "0.4.17", features = ["std"] }
 # nofity-rust is optional (on by default) because the crate doesn't currently build for darwin with nix
 # see: https://github.com/NixOS/nixpkgs/issues/160876
diff --git a/src/modules/localip.rs b/src/modules/localip.rs
index b5385105..fbdb2c95 100644
--- a/src/modules/localip.rs
+++ b/src/modules/localip.rs
@@ -4,14 +4,27 @@ use crate::config::ModuleConfig;
 use crate::configs::localip::LocalipConfig;
 use crate::formatter::StringFormatter;
 
+use std::io::Error;
+use std::net::UdpSocket;
+
+fn get_local_ipv4() -> Result<String, Error> {
+    let socket = UdpSocket::bind("0.0.0.0:0")?;
+    socket.connect("192.0.2.0:80")?;
+
+    let addr = socket.local_addr()?;
+
+    Ok(addr.ip().to_string())
+}
+
 /// Creates a module with the ipv4 address of the local machine.
 ///
-/// The `local_ipaddress` crate is used to determine the local IP address of your machine.
-/// An accurate and fast way, especially if there are multiple IP addresses available,
-/// is to connect a UDP socket and then reading its local endpoint.
+/// The IP address is gathered from the local endpoint of an UDP socket
+/// connected to a reserved IPv4 remote address, which is an accurate and fast
+/// way, especially if there are multiple IP addresses available.
+/// There should be no actual packets send over the wire.
 ///
 /// Will display the ip if all of the following criteria are met:
-///     - localip.disabled is false
+///     - `localip.disabled` is false
 ///     - `localip.ssh_only` is false OR the user is currently connected as an SSH session (`$SSH_CONNECTION`)
 pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
     let mut module = context.new_module("localip");
@@ -28,11 +41,18 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
         return None;
     }
 
-    let localip = local_ipaddress::get().unwrap_or_default();
-    if localip.is_empty() {
-        log::warn!("unable to determine local ipv4 address");
-        return None;
-    }
+    let localip = match get_local_ipv4() {
+        Ok(ip) => ip,
+        Err(e) => {
+            // ErrorKind::NetworkUnreachable is unstable
+            if cfg!(target_os = "linux") && e.raw_os_error() == Some(101) {
+                "NetworkUnreachable".to_string()
+            } else {
+                log::warn!("unable to determine local ipv4 address: {e}");
+                return None;
+            }
+        }
+    };
 
     let parsed = StringFormatter::new(config.format).and_then(|formatter| {
         formatter
@@ -60,19 +80,22 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
 
 #[cfg(test)]
 mod tests {
+    use crate::modules::localip::get_local_ipv4;
     use crate::test::ModuleRenderer;
     use nu_ansi_term::{Color, Style};
 
     macro_rules! get_localip {
         () => {
-            if let Some(localip) = local_ipaddress::get() {
-                localip
-            } else {
-                println!(
-                    "localip was not tested because socket connection failed! \
-                     This could be caused by an unconventional network setup."
-                );
-                return;
+            match get_local_ipv4() {
+                Ok(ip) => ip,
+                Err(e) => {
+                    println!(
+                        "localip was not tested because socket connection failed! \
+                        This could be caused by an unconventional network setup. \
+                        Error: {e}"
+                    );
+                    return;
+                }
             }
         };
     }