Răsfoiți Sursa

Merge pull request #2499 from nginx-proxy/ipv6

feat: basic implementation of IPv6 for IPv6 docker networks
Nicolas Duchon 6 luni în urmă
părinte
comite
b6c8851794

+ 9 - 0
.github/workflows/test.yml

@@ -36,6 +36,15 @@ jobs:
           pip install -r python-requirements.txt
         working-directory: test/requirements
 
+      - name: Login to DockerHub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Pull nginx:alpine image
+        run: docker pull nginx:alpine
+
       - name: Build Docker web server image
         run: make build-webserver
 

+ 13 - 1
docs/README.md

@@ -618,7 +618,19 @@ If the default certificate is also missing, nginx-proxy will:
 
 ## IPv6 Support
 
-You can activate the IPv6 support for the nginx-proxy container by passing the value `true` to the `ENABLE_IPV6` environment variable:
+### IPv6 Docker Networks
+
+nginx-proxy support both IPv4 and IPv6 on Docker networks.
+
+By default nginx-proxy will prefer IPv4: if a container can be reached over both IPv4 and IPv6, only its IPv4 will be used.
+
+This can be changed globally by setting the environment variable `PREFER_IPV6_NETWORK` to `true` on the proxy container: with this setting the proxy will only use IPv6 for containers that can be reached over both IPv4 and IPv6.
+
+IPv4 and IPv6 are never both used at the same time on containers that use both IP stacks to avoid artificially inflating the effective round robin weight of those containers.
+
+### Listening on IPv6
+
+By default the nginx-proxy container will only listen on IPv4. To enable listening on IPv6 too, set the `ENABLE_IPV6` environment variable to `true`:
 
 ```console
 docker run -d -p 80:80 -e ENABLE_IPV6=true -v /var/run/docker.sock:/tmp/docker.sock:ro nginxproxy/nginx-proxy

+ 30 - 13
nginx.tmpl

@@ -23,6 +23,7 @@
 {{- $_ := set $config "trust_downstream_proxy" ($globals.Env.TRUST_DOWNSTREAM_PROXY | default "true" | parseBool) }}
 {{- $_ := set $config "enable_access_log" ($globals.Env.DISABLE_ACCESS_LOGS | default "false" | parseBool | not) }}
 {{- $_ := set $config "enable_ipv6" ($globals.Env.ENABLE_IPV6 | default "false" | parseBool) }}
+{{- $_ := set $config "prefer_ipv6_network" ($globals.Env.PREFER_IPV6_NETWORK | default "false" | parseBool) }}
 {{- $_ := set $config "ssl_policy" ($globals.Env.SSL_POLICY | default "Mozilla-Intermediate") }}
 {{- $_ := set $config "enable_debug_endpoint" ($globals.Env.DEBUG_ENDPOINT | default "false") }}
 {{- $_ := set $config "hsts" ($globals.Env.HSTS | default "max-age=31536000") }}
@@ -76,7 +77,8 @@
      * The return value will be added to the dot dict with key "ip".
      */}}
 {{- define "container_ip" }}
-    {{- $ip := "" }}
+    {{- $ipv4 := "" }}
+    {{- $ipv6 := "" }}
     #     networks:
     {{- range sortObjectsByKeysAsc $.container.Networks "Name" }}
         {{- /*
@@ -91,17 +93,17 @@
             {{- /* Handle containers in host nework mode */}}
             {{- if (index $.globals.networks "host") }}
     #         both container and proxy are in host network mode, using localhost IP
-                {{- $ip = "127.0.0.1" }}
+                {{- $ipv4 = "127.0.0.1" }}
                 {{- continue }}
             {{- end }}
             {{- range sortObjectsByKeysAsc $.globals.CurrentContainer.Networks "Name" }}
                 {{- if and . .Gateway (not .Internal) }}
     #         container is in host network mode, using {{ .Name }} gateway IP
-                    {{- $ip = .Gateway }}
+                    {{- $ipv4 = .Gateway }}
                     {{- break }}
                 {{- end }}
             {{- end }}
-            {{- if $ip }}
+            {{- if $ipv4 }}
                 {{- continue }}
             {{- end }}
         {{- end }}
@@ -111,26 +113,41 @@
         {{- end }}
         {{- /*
              * Do not emit multiple `server` directives for this container if it
-             * is reachable over multiple networks.  This avoids accidentally
-             * inflating the effective round-robin weight of a server due to the
-             * redundant upstream addresses that nginx sees as belonging to
+             * is reachable over multiple networks or multiple IP stacks. This avoids 
+             * accidentally inflating the effective round-robin weight of a server due
+             * to the redundant upstream addresses that nginx sees as belonging to
              * distinct servers.
              */}}
-        {{- if $ip }}
+        {{- if or $ipv4 $ipv6 }}
     #         {{ .Name }} (ignored; reachable but redundant)
             {{- continue }}
         {{- end }}
     #         {{ .Name }} (reachable)
         {{- if and . .IP }}
-            {{- $ip = .IP }}
-        {{- else }}
-    #             /!\ No IP for this network!
+            {{- $ipv4 = .IP }}
+        {{- end }}
+        {{- if and . .GlobalIPv6Address }}
+            {{- $ipv6 = .GlobalIPv6Address }}
+        {{- end }}
+        {{- if and (empty $ipv4) (empty $ipv6) }}
+    #             /!\ No IPv4 or IPv6 for this network!
         {{- end }}
     {{- else }}
     #         (none)
     {{- end }}
-    #     IP address: {{ if $ip }}{{ $ip }}{{ else }}(none usable){{ end }}
-    {{- $_ := set $ "ip" $ip }}
+    {{ if and $ipv6 $.globals.config.prefer_ipv6_network }}
+    #     IPv4 address: {{ if $ipv4 }}{{ $ipv4 }} (ignored; reachable but IPv6 prefered){{ else }}(none usable){{ end }}
+    #     IPv6 address: {{ $ipv6 }}
+        {{- $_ := set $ "ip" (printf "[%s]" $ipv6) }}
+    {{- else }}
+    #     IPv4 address: {{ if $ipv4 }}{{ $ipv4 }}{{ else }}(none usable){{ end }}
+    #     IPv6 address: {{ if $ipv6 }}{{ $ipv6 }}{{ if $ipv4 }} (ignored; reachable but IPv4 prefered){{ end }}{{ else }}(none usable){{ end }}
+        {{- if $ipv4 }}
+            {{- $_ := set $ "ip" $ipv4 }}
+        {{- else if $ipv6}}
+            {{- $_ := set $ "ip" (printf "[%s]" $ipv6) }}
+        {{- end }}
+    {{- end }}
 {{- end }}
 
 {{- /*

+ 0 - 0
test/test_ipv6.py → test/test_ipv6/test_ipv6.py


+ 0 - 0
test/test_ipv6.yml → test/test_ipv6/test_ipv6.yml


+ 19 - 0
test/test_ipv6/test_ipv6_prefer_ipv4_network.py

@@ -0,0 +1,19 @@
+import pytest
+
+
+def test_forwards_to_ipv4_only_network(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://ipv4only.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 80\n"
+
+
+def test_forwards_to_dualstack_network(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://dualstack.nginx-proxy.tld")
+    assert r.status_code == 200   
+    assert "Welcome to nginx!" in r.text
+
+
+def test_dualstack_network_prefer_ipv4_config(docker_compose, nginxproxy):
+    conf = nginxproxy.get_conf().decode('ASCII')
+    assert "IPv6 address: fd00:cafe:face:feed::2 (ignored; reachable but IPv4 prefered)" in conf
+    assert "server 172.16.20.2:80;" in conf

+ 45 - 0
test/test_ipv6/test_ipv6_prefer_ipv4_network.yml

@@ -0,0 +1,45 @@
+version: "2"
+
+networks:
+  ipv4net:
+    ipam:
+      config:
+        - subnet: 172.16.10.0/24
+  dualstacknet:
+    enable_ipv6: true
+    ipam:
+      config:
+        - subnet: 172.16.20.0/24
+        - subnet: fd00:cafe:face:feed::/64
+
+services:
+  ipv4only:
+    image: web
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: ipv4only.nginx-proxy.tld
+    networks:
+      ipv4net:
+        ipv4_address: 172.16.10.2
+
+  dualstack:
+    image: nginx:alpine
+    environment:
+      VIRTUAL_HOST: dualstack.nginx-proxy.tld
+    networks:
+      dualstacknet:
+        ipv4_address: 172.16.20.2
+        ipv6_address: fd00:cafe:face:feed::2
+
+  sut:
+    image: nginxproxy/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+    networks:
+      ipv4net:
+        ipv4_address: 172.16.10.3
+      dualstacknet:
+        ipv4_address: 172.16.20.3
+        ipv6_address: fd00:cafe:face:feed::3

+ 19 - 0
test/test_ipv6/test_ipv6_prefer_ipv6_network.py

@@ -0,0 +1,19 @@
+import pytest
+
+
+def test_forwards_to_ipv4_only_network(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://ipv4only.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 80\n"
+
+
+def test_forwards_to_dualstack_network(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://dualstack.nginx-proxy.tld")
+    assert r.status_code == 200   
+    assert "Welcome to nginx!" in r.text
+
+
+def test_dualstack_network_prefer_ipv6_config(docker_compose, nginxproxy):
+    conf = nginxproxy.get_conf().decode('ASCII')
+    assert "IPv4 address: 172.16.20.2 (ignored; reachable but IPv6 prefered)" in conf
+    assert "server [fd00:cafe:face:feed::2]:80;" in conf

+ 47 - 0
test/test_ipv6/test_ipv6_prefer_ipv6_network.yml

@@ -0,0 +1,47 @@
+version: "2"
+
+networks:
+  ipv4net:
+    ipam:
+      config:
+        - subnet: 172.16.10.0/24
+  dualstacknet:
+    enable_ipv6: true
+    ipam:
+      config:
+        - subnet: 172.16.20.0/24
+        - subnet: fd00:cafe:face:feed::/64
+
+services:
+  ipv4only:
+    image: web
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: ipv4only.nginx-proxy.tld
+    networks:
+      ipv4net:
+        ipv4_address: 172.16.10.2
+
+  dualstack:
+    image: nginx:alpine
+    environment:
+      VIRTUAL_HOST: dualstack.nginx-proxy.tld
+    networks:
+      dualstacknet:
+        ipv4_address: 172.16.20.2
+        ipv6_address: fd00:cafe:face:feed::2
+
+  sut:
+    image: nginxproxy/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+    environment:
+      PREFER_IPV6_NETWORK: "true"
+    networks:
+      ipv4net:
+        ipv4_address: 172.16.10.3
+      dualstacknet:
+        ipv4_address: 172.16.20.3
+        ipv6_address: fd00:cafe:face:feed::3