Repository: jazzband/django-debug-toolbar

Issue:
The Debug Toolbar has stopped showing under Docker
It seems like the [snippet of code](https://django-debug-toolbar.readthedocs.io/en/stable/installation.html#configure-internal-ips) in the documentation for using Docker has stopped working.

<img width="695" alt="image" src="https://github.com/jazzband/django-debug-toolbar/assets/191620/a4049dbf-ab73-410b-a2e0-7606749eedd0">

That snippet of code will set `INTERNAL_IPS` to `['172.27.0.1', '127.0.0.1', '10.0.2.2']`, however, inside `debug_toolbar.middleware.show_toolbar()`, `request.META.get("REMOTE_ADDR")` returns `'192.168.65.1'`, which is why it ends up not showing the Debug Toolbar.

This snippet of code used to work. I'm guessing something must of changed in a recent version of Docker to cause it to not get the correct IP address. I'm not sure at this point how to get the IP dynamically because even `ifconfig` shows the `172.27.01` IP address instead of the `192.168.65.1` IP address.

Versions:
* Docker version 24.0.6
* Docker Compose version v2.22.0-desktop.2
* macOS Sonoma version 14.0
* django-debug-toolba v3.8.1
* django v4.2.5



Discussion:
If you're absolutely sure that nobody else will access your development server I still prefer the following snippet:

```python
if DEBUG:
    # `debug` is only True in templates if the vistor IP is in INTERNAL_IPS.
    INTERNAL_IPS = type("c", (), {"__contains__": lambda *a: True})()
```

There's no need to determine the IP address, all you have to do is to make `INTERNAL_IPS` contain everything by defining a `__contains__` method which always returns true.

That being said, it would be nice if there was a copy-pasteable and somewhat safer fix for this.
Can confirm the original code for finding the internal IP has stopped working. I've also just allowed all IPs when debug is on. From what I can tell, something like `socket.gethostbyname('host.docker.internal')` is _supposed_ to return the right IP, but doesn't. In my case, the right IP is 192.168.65.1, but host.docker.internal returns 192.168.65.254. Maybe allowing just that 192.168.65.* prefix would be more secure?
@blopker, great find! I'm guessing that the last number will always be 1. Maybe something the following would always work!

```
".".join(socket.gethostbyname('host.docker.internal').rsplit('.')[:-1]) + ".1"
```
I doubt there's anything which is always going to work, whatever docker/podman/etc. does, except for my `INTERNAL_IPS` hack.

I'm slowly leaning towards documenting the hack with a big big caveat. Opinions?
As a user, this has tripped me up every single time I start a new project. I get the need for making sure the toolbar isn't shown in production, but since Docker is such a common development environment, I can't help but think there's a better way to accomplish this?

Maybe this snippet can be included in this library somehow? Then if users are on Docker they can just import a single function and add it to INTERNAL_IPS? The snippet could be more robust then, like checking for DEBUG is True.

But yeah, in lieu of that, documentation is always welcome. A bit more explanation about why the snippet is needed, plus what it's actually doing would be great. Maybe a date/Docker version on when it was last known to work as well? 
@matthiask I'm less sure we should even be supporting an `INTERNAL_IPS` out of the box. That feels like an invitation to utilize the toolbar in a remote environment. Perhaps instead we should default to checking that the user is a super user instead? 
I don't even have DjDT installed in production, but I know not everyone does it that way. 

Checking if someone has superuser permissions wouldn't work for us because we sometimes impersonate regular users for debugging and still need DjDT.

I don't know the history ... why isn't going off what `DEBUG` is set to enough? Isn't it recommended that you don't run a Django project with `DEBUG` set to `True` in production?

The simplest thing to do would be just going off of `DEBUG` and calling it a day. 🙂
One issue is that `INTERNAL_IPS` (https://docs.djangoproject.com/en/5.0/ref/settings/#internal-ips) does other things besides enable the toolbar. 

Specifically: "Allow the [debug()](https://docs.djangoproject.com/en/5.0/ref/templates/api/#django.template.context_processors.debug) context processor to add some variables to the template context.", I suspect (but haven't looked) that the toolbar uses that information internally. However, if the toolbar duplicated this context processor, which itself is pretty small, it could get around that. See https://github.com/django/django/blob/08306bad57761b5eb176894649ac7d4e735c52dd/django/template/context_processors.py#L41
> @matthiask I'm less sure we should even be supporting an `INTERNAL_IPS` out of the box. That feels like an invitation to utilize the toolbar in a remote environment. Perhaps instead we should default to checking that the user is a super user instead?

I'm not sure. I do not have to do anything right now to use the toolbar, and this change would definitely mean I'd have to customize the callback locally, since I use the toolbar also when I'm not authenticated (locally) to quickly see if a particular view requires too many SQL queries for example.
I'm brainstorming now. We have some extra development bandwidth. Maybe we provide various out of the box solutions for the person to configure.

```python
DEBUG_TOOLBAR_CONFIG = {
    "SHOW_TOOLBAR_CALLBACK": "debug_toolbar.show_toolbar.allow_super_users"
}
```

Then maybe we have:
- `debug_toolbar.show_toolbar.restrict_to_internal_ips`
- `debug_toolbar.show_toolbar.allow_all`
- `debug_toolbar.show_toolbar.magic_local_docker_fix`

You'll still need to change something regardless of what we do. And from what this issue sounds like, the docker integration today is already broken.
The thing which is broken right now is the documentation I think? I can see the usefulness of adding a few additional callbacks of course, but when we only evaluate `DEBUG` and `INTERNAL_IPS` we're following Django's lead and I still don't think it's a bad default. People using Docker are probably a bit more advanced already and can for better or worse be expected to add a line or three to their projects, or not?
To me, it's simple. Update the documentation and close this issue. Then, it might be good to start planning a more permanent solution and better DX. With Docker/Docker Compose becoming more the preferred development environment, it would be nice if DjDT worked out of the box by just adding it to `INSTALLED_APPS`. To me, that's the ideal DX to shoot for.
> People using Docker are probably a bit more advanced already and can for better or worse be expected to add a line or three to their projects, or not?

I disagree that people using docker are typically more advanced than those that aren't. Docker is one of those technologies that some folks think everyone should know. That leads to at least some folks having it thrust on them when they arguably should be focused on learning the basics of the language and framework they're using.

I know enough of docker to get a service up and running locally for development. I'd be a bit lost trying to solve this particular problem though.
I agree with @tim-schilling. I manage a team of engineers; some of the people on the team are advanced, and some are not because that isn't their focus. Again, if we're thinking about the ideal DX, it would be great if the only requirement was adding DjDT to `INSTALLED_APPS`.

Resolving patch:
diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py
--- a/debug_toolbar/middleware.py
+++ b/debug_toolbar/middleware.py
@@ -3,6 +3,7 @@
 """
 
 import re
+import socket
 from functools import lru_cache
 
 from django.conf import settings
@@ -19,7 +20,22 @@ def show_toolbar(request):
     """
     Default function to determine whether to show the toolbar on a given page.
     """
-    return settings.DEBUG and request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS
+    internal_ips = settings.INTERNAL_IPS.copy()
+
+    try:
+        # This is a hack for docker installations. It attempts to look
+        # up the IP address of the docker host.
+        # This is not guaranteed to work.
+        docker_ip = (
+            # Convert the last segment of the IP address to be .1
+            ".".join(socket.gethostbyname("host.docker.internal").rsplit(".")[:-1])
+            + ".1"
+        )
+        internal_ips.append(docker_ip)
+    except socket.gaierror:
+        # It's fine if the lookup errored since they may not be using docker
+        pass
+    return settings.DEBUG and request.META.get("REMOTE_ADDR") in internal_ips
 
 
 @lru_cache(maxsize=None)


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2,6 +2,7 @@
 import re
 import time
 import unittest
+from unittest.mock import patch
 
 import html5lib
 from django.contrib.staticfiles.testing import StaticLiveServerTestCase
@@ -66,6 +67,14 @@ def test_show_toolbar_INTERNAL_IPS(self):
         with self.settings(INTERNAL_IPS=[]):
             self.assertFalse(show_toolbar(self.request))
 
+    @patch("socket.gethostbyname", return_value="127.0.0.255")
+    def test_show_toolbar_docker(self, mocked_gethostbyname):
+        with self.settings(INTERNAL_IPS=[]):
+            # Is true because REMOTE_ADDR is 127.0.0.1 and the 255
+            # is shifted to be 1.
+            self.assertTrue(show_toolbar(self.request))
+        mocked_gethostbyname.assert_called_once_with("host.docker.internal")
+
     def test_should_render_panels_RENDER_PANELS(self):
         """
         The toolbar should force rendering panels on each request