Repository: pallets/werkzeug

Issue:
stop showing 0.0.0.0
Not sure how feasible this is, but it's one of the more common questions I see on SO. Users for whatever reason run the dev server on 0.0.0.0, see `Running on http://0.0.0.0`, then are confused when they can't get to that IP in most browsers or scripts. Maybe if Werkzeug sees that value, it displays a more helpful message with the real ip (or localhost if not possible) and a warning about running the dev server in production. `Running on all interfaces (try http://{real ip}). Warning: Don't use the development server in production. `


Discussion:
Great idea @davidism, I've seen users hit this too, and was impressed when I tried `npx @11ty/eleventy --serve` for the first time and saw the Browsersync output included my (LAN) external IP, in case that's helpful prior art:
```
Watching…
[Browsersync] Access URLs:
 -------------------------------------
       Local: http://localhost:8080
    External: http://192.168.0.11:8080
 -------------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 -------------------------------------
[Browsersync] Serving files from: _site
```

Something like this would get us that much (based on https://stackoverflow.com/a/28950776):

```python
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    try:
        s.connect(('10.255.255.255', 1))  # need not be reachable
        return s.getsockname()[0]
    except Exception:
        return '127.0.0.1'
```

Not sure it's in scope, but if we wanted to try to get the host's WAN IP, I think we'd either have to try querying the router over UPnP, which requires a UPnP client, as in https://stackoverflow.com/a/41385033:
```python
import miniupnpc
client = miniupnpc.UPnP()
client.discoverdelay = 200
client.discover()
client.selectigd()
return client.externalipaddress()
```
...or resort to querying some public "what is my IP" service (https://ident.me, http://canhazip.com, etc.) as in https://stackoverflow.com/a/41432835.
I'll be working on this.

Resolving patch:
diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
--- a/src/werkzeug/serving.py
+++ b/src/werkzeug/serving.py
@@ -18,7 +18,6 @@
 import socket
 import socketserver
 import sys
-import typing
 import typing as t
 import warnings
 from datetime import datetime as dt
@@ -596,6 +595,24 @@ def get_sockaddr(
     return res[0][4]  # type: ignore
 
 
+def get_interface_ip(family: socket.AddressFamily):
+    """Get the IP address of an external interface. Used when binding to
+    0.0.0.0 or ::1 to show a more useful URL.
+
+    :meta private:
+    """
+    # arbitrary private address
+    host = "fd31:f903:5ab5:1::1" if family == socket.AF_INET6 else "10.253.155.219"
+
+    with socket.socket(family, socket.SOCK_DGRAM) as s:
+        try:
+            s.connect((host, 58162))
+        except OSError:
+            return "::1" if family == socket.AF_INET6 else "127.0.0.1"
+
+        return s.getsockname()[0]
+
+
 class BaseWSGIServer(HTTPServer):
 
     """Simple single-threaded, single-process WSGI server."""
@@ -860,21 +877,33 @@ def run_simple(
         application = SharedDataMiddleware(application, static_files)
 
     def log_startup(sock):
-        display_hostname = hostname if hostname not in ("", "*") else "localhost"
-        quit_msg = "(Press CTRL+C to quit)"
+        all_addresses_message = (
+            " * Running on all addresses.\n"
+            "   WARNING: This is a development server. Do not use it in"
+            " a production deployment."
+        )
+
         if sock.family == af_unix:
-            _log("info", " * Running on %s %s", display_hostname, quit_msg)
+            _log("info", " * Running on %s (Press CTRL+C to quit)", hostname)
         else:
+            if hostname == "0.0.0.0":
+                _log("warning", all_addresses_message)
+                display_hostname = get_interface_ip(socket.AF_INET)
+            elif hostname == "::":
+                _log("warning", all_addresses_message)
+                display_hostname = get_interface_ip(socket.AF_INET6)
+            else:
+                display_hostname = hostname
+
             if ":" in display_hostname:
                 display_hostname = f"[{display_hostname}]"
-            port = sock.getsockname()[1]
+
             _log(
                 "info",
-                " * Running on %s://%s:%d/ %s",
+                " * Running on %s://%s:%d/ (Press CTRL+C to quit)",
                 "http" if ssl_context is None else "https",
                 display_hostname,
-                port,
-                quit_msg,
+                sock.getsockname()[1],
             )
 
     def inner():


Test patch: