Repository: traefik/traefik (language: go)

Issue:
ipStrategy should be IPv6 subnet aware
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

ipStrategy for the RateLimit and InFlightReq middlewares treats a single IP as a client - however in IPv6, it's trivial to spin up billions of billions of IPs within a `/64` subnet and bypass any filtering set by these.

By default, the ipStrategy on IPv6 should treat the whole `/64` network as a 'client'.


Discussion / hints:

Fix patch:
diff --git a/docs/content/middlewares/http/inflightreq.md b/docs/content/middlewares/http/inflightreq.md
index 16809ac80b..48200fd90a 100644
--- a/docs/content/middlewares/http/inflightreq.md
+++ b/docs/content/middlewares/http/inflightreq.md
@@ -101,7 +101,7 @@ If none are set, the default is to use the `requestHost`.
 
 #### `sourceCriterion.ipStrategy`
 
-The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`.
+The `ipStrategy` option defines three parameters that configures how Traefik determines the client IP: `depth`, `excludedIPs` and `ipv6Subnet`.
 
 !!! important "As a middleware, InFlightReq happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through the middleware. Therefore, during InFlightReq, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be used and/or relied upon."
 
@@ -112,6 +112,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
 - If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP is empty.
 - `depth` is ignored if its value is less than or equal to 0.
 
+If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.  
+See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
+
 !!! example "Example of Depth & X-Forwarded-For"
 
     If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used as the criterion is `"12.0.0.1"` (`depth=2`).
@@ -218,6 +221,63 @@ http:
       excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
 ```
 
+##### `ipStrategy.ipv6Subnet`
+
+This strategy applies to `Depth` and `RemoteAddr` strategy only.
+If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
+
+This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
+
+- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
+
+!!! example "Example of ipv6Subnet"
+
+    If `ipv6Subnet` is provided, the IP is transformed in the following way.
+
+    | `IP`                      | `ipv6Subnet` | clientIP              |
+    |---------------------------|--------------|-----------------------|
+    | `"::abcd:1111:2222:3333"` | `64`         | `"::0:0:0:0"`         |
+    | `"::abcd:1111:2222:3333"` | `80`         | `"::abcd:0:0:0:0"`    |
+    | `"::abcd:1111:2222:3333"` | `96`         | `"::abcd:1111:0:0:0"` |
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-inflightreq
+spec:
+  inFlightReq:
+    sourceCriterion:
+      ipStrategy:
+        ipv6Subnet: 64
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-inflightreq:
+      inFlightReq:
+        sourceCriterion:
+          ipStrategy:
+            ipv6Subnet: 64
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-inflightreq.inflightreq]
+    [http.middlewares.test-inflightreq.inFlightReq.sourceCriterion.ipStrategy]
+      ipv6Subnet = 64
+```
+
 #### `sourceCriterion.requestHeaderName`
 
 Name of the header used to group incoming requests.
diff --git a/docs/content/middlewares/http/ipallowlist.md b/docs/content/middlewares/http/ipallowlist.md
index 47c3b0caff..4f9331268b 100644
--- a/docs/content/middlewares/http/ipallowlist.md
+++ b/docs/content/middlewares/http/ipallowlist.md
@@ -75,6 +75,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th
 - If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty.
 - `depth` is ignored if its value is less than or equal to 0.
 
+If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.  
+See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
+
 !!! example "Examples of Depth & X-Forwarded-For"
 
     If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used is `"12.0.0.1"` (`depth=2`).
@@ -204,3 +207,60 @@ http:
     [http.middlewares.test-ipallowlist.ipAllowList.ipStrategy]
       excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
 ```
+
+#### `ipStrategy.ipv6Subnet`
+
+This strategy applies to `Depth` and `RemoteAddr` strategy only.
+If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
+
+This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
+
+- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
+
+!!! example "Example of ipv6Subnet"
+
+    If `ipv6Subnet` is provided, the IP is transformed in the following way.
+
+    | `IP`                      | `ipv6Subnet` | clientIP              |
+    |---------------------------|--------------|-----------------------|
+    | `"::abcd:1111:2222:3333"` | `64`         | `"::0:0:0:0"`         |
+    | `"::abcd:1111:2222:3333"` | `80`         | `"::abcd:0:0:0:0"`    |
+    | `"::abcd:1111:2222:3333"` | `96`         | `"::abcd:1111:0:0:0"` |
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ipallowlist
+spec:
+  ipallowlist:
+    sourceCriterion:
+      ipStrategy:
+        ipv6Subnet: 64
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ipallowlist:
+      ipallowlist:
+        sourceCriterion:
+          ipStrategy:
+            ipv6Subnet: 64
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ipallowlist.ipallowlist]
+    [http.middlewares.test-ipallowlist.ipallowlist.sourceCriterion.ipStrategy]
+      ipv6Subnet = 64
+```
diff --git a/docs/content/middlewares/http/ipwhitelist.md b/docs/content/middlewares/http/ipwhitelist.md
index f25960a525..b6aa601367 100644
--- a/docs/content/middlewares/http/ipwhitelist.md
+++ b/docs/content/middlewares/http/ipwhitelist.md
@@ -81,6 +81,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th
 - If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP will be empty.
 - `depth` is ignored if its value is less than or equal to 0.
 
+If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.  
+See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
+
 !!! example "Examples of Depth & X-Forwarded-For"
 
     If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used for the whitelisting is `"12.0.0.1"` (`depth=2`).
@@ -210,3 +213,60 @@ http:
     [http.middlewares.test-ipwhitelist.ipWhiteList.ipStrategy]
       excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
 ```
+
+#### `ipStrategy.ipv6Subnet`
+
+This strategy applies to `Depth` and `RemoteAddr` strategy only.
+If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
+
+This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
+
+- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
+
+!!! example "Example of ipv6Subnet"
+
+    If `ipv6Subnet` is provided, the IP is transformed in the following way.
+
+    | `IP`                      | `ipv6Subnet` | clientIP              |
+    |---------------------------|--------------|-----------------------|
+    | `"::abcd:1111:2222:3333"` | `64`         | `"::0:0:0:0"`         |
+    | `"::abcd:1111:2222:3333"` | `80`         | `"::abcd:0:0:0:0"`    |
+    | `"::abcd:1111:2222:3333"` | `96`         | `"::abcd:1111:0:0:0"` |
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ipWhiteList.ipWhiteList.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ipWhiteList
+spec:
+  ipWhiteList:
+    sourceCriterion:
+      ipStrategy:
+        ipv6Subnet: 64
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ipWhiteList.ipWhiteList.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ipWhiteList:
+      ipWhiteList:
+        sourceCriterion:
+          ipStrategy:
+            ipv6Subnet: 64
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ipWhiteList.ipWhiteList]
+    [http.middlewares.test-ipWhiteList.ipWhiteList.sourceCriterion.ipStrategy]
+      ipv6Subnet = 64
+```
diff --git a/docs/content/middlewares/http/ratelimit.md b/docs/content/middlewares/http/ratelimit.md
index 39a1f464f7..dc03dc9a37 100644
--- a/docs/content/middlewares/http/ratelimit.md
+++ b/docs/content/middlewares/http/ratelimit.md
@@ -211,7 +211,7 @@ If none are set, the default is to use the request's remote address field (as an
 
 #### `sourceCriterion.ipStrategy`
 
-The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`.
+The `ipStrategy` option defines three parameters that configures how Traefik determines the client IP: `depth`, `excludedIPs` and `ipv6Subnet`.
 
 !!! important "As a middleware, rate-limiting happens before the actual proxying to the backend takes place. In addition, the previous network hop only gets appended to `X-Forwarded-For` during the last stages of proxying, i.e. after it has already passed through rate-limiting. Therefore, during rate-limiting, as the previous network hop is not yet present in `X-Forwarded-For`, it cannot be found and/or relied upon."
 
@@ -222,6 +222,9 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select
 - If `depth` is greater than the total number of IPs in `X-Forwarded-For`, then the client IP is empty.
 - `depth` is ignored if its value is less than or equal to 0.
 
+If `ipStrategy.ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.  
+See [ipStrategy.ipv6Subnet](#ipstrategyipv6subnet) for more details.
+
 !!! example "Example of Depth & X-Forwarded-For"
 
     If `depth` is set to 2, and the request `X-Forwarded-For` header is `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` then the "real" client IP is `"10.0.0.1"` (at depth 4) but the IP used as the criterion is `"12.0.0.1"` (`depth=2`).
@@ -355,6 +358,63 @@ http:
       excludedIPs = ["127.0.0.1/32", "192.168.1.7"]
 ```
 
+##### `ipStrategy.ipv6Subnet`
+
+This strategy applies to `Depth` and `RemoteAddr` strategy only.
+If `ipv6Subnet` is provided and the selected IP is IPv6, the IP is transformed into the first IP of the subnet it belongs to.
+
+This is useful for grouping IPv6 addresses into subnets to prevent bypassing this middleware by obtaining a new IPv6.
+
+- `ipv6Subnet` is ignored if its value is outside of 0-128 interval
+
+!!! example "Example of ipv6Subnet"
+
+    If `ipv6Subnet` is provided, the IP is transformed in the following way.
+
+    | `IP`                      | `ipv6Subnet` | clientIP              |
+    |---------------------------|--------------|-----------------------|
+    | `"::abcd:1111:2222:3333"` | `64`         | `"::0:0:0:0"`         |
+    | `"::abcd:1111:2222:3333"` | `80`         | `"::abcd:0:0:0:0"`    |
+    | `"::abcd:1111:2222:3333"` | `96`         | `"::abcd:1111:0:0:0"` |
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  ratelimit:
+    sourceCriterion:
+      ipStrategy:
+        ipv6Subnet: 64
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.ipv6Subnet=64"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      ratelimit:
+        sourceCriterion:
+          ipStrategy:
+            ipv6Subnet: 64
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.ratelimit]
+    [http.middlewares.test-ratelimit.ratelimit.sourceCriterion.ipStrategy]
+      ipv6Subnet = 64
+```
+
 #### `sourceCriterion.requestHeaderName`
 
 Name of the header used to group incoming requests.
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index e9b9beabc8..77fcdef872 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -85,15 +85,18 @@
 - "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy=true"
 - "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.depth=42"
 - "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.excludedips=foobar, foobar"
+- "traefik.http.middlewares.middleware13.ipallowlist.ipstrategy.ipv6subnet=42"
 - "traefik.http.middlewares.middleware13.ipallowlist.rejectstatuscode=42"
 - "traefik.http.middlewares.middleware13.ipallowlist.sourcerange=foobar, foobar"
 - "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy=true"
 - "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.depth=42"
 - "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.excludedips=foobar, foobar"
+- "traefik.http.middlewares.middleware14.ipwhitelist.ipstrategy.ipv6subnet=42"
 - "traefik.http.middlewares.middleware14.ipwhitelist.sourcerange=foobar, foobar"
 - "traefik.http.middlewares.middleware15.inflightreq.amount=42"
 - "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.depth=42"
 - "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
+- "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.ipstrategy.ipv6subnet=42"
 - "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requestheadername=foobar"
 - "traefik.http.middlewares.middleware15.inflightreq.sourcecriterion.requesthost=true"
 - "traefik.http.middlewares.middleware16.passtlsclientcert.info.issuer.commonname=true"
@@ -125,6 +128,7 @@
 - "traefik.http.middlewares.middleware18.ratelimit.period=42s"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.ipv6subnet=42"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requestheadername=foobar"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.requesthost=true"
 - "traefik.http.middlewares.middleware19.redirectregex.permanent=true"
diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml
index 269467380b..18fc9bc84c 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -227,12 +227,14 @@
         [http.middlewares.Middleware13.ipAllowList.ipStrategy]
           depth = 42
           excludedIPs = ["foobar", "foobar"]
+          ipv6Subnet = 42
     [http.middlewares.Middleware14]
       [http.middlewares.Middleware14.ipWhiteList]
         sourceRange = ["foobar", "foobar"]
         [http.middlewares.Middleware14.ipWhiteList.ipStrategy]
           depth = 42
           excludedIPs = ["foobar", "foobar"]
+          ipv6Subnet = 42
     [http.middlewares.Middleware15]
       [http.middlewares.Middleware15.inFlightReq]
         amount = 42
@@ -242,6 +244,7 @@
           [http.middlewares.Middleware15.inFlightReq.sourceCriterion.ipStrategy]
             depth = 42
             excludedIPs = ["foobar", "foobar"]
+            ipv6Subnet = 42
     [http.middlewares.Middleware16]
       [http.middlewares.Middleware16.passTLSClientCert]
         pem = true
@@ -286,6 +289,7 @@
           [http.middlewares.Middleware18.rateLimit.sourceCriterion.ipStrategy]
             depth = 42
             excludedIPs = ["foobar", "foobar"]
+            ipv6Subnet = 42
     [http.middlewares.Middleware19]
       [http.middlewares.Middleware19.redirectRegex]
         regex = "foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml
index 6f94af2438..d0b0fc0fb7 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -267,6 +267,7 @@ http:
           excludedIPs:
             - foobar
             - foobar
+          ipv6Subnet: 42
         rejectStatusCode: 42
     Middleware14:
       ipWhiteList:
@@ -278,6 +279,7 @@ http:
           excludedIPs:
             - foobar
             - foobar
+          ipv6Subnet: 42
     Middleware15:
       inFlightReq:
         amount: 42
@@ -287,6 +289,7 @@ http:
             excludedIPs:
               - foobar
               - foobar
+            ipv6Subnet: 42
           requestHeaderName: foobar
           requestHost: true
     Middleware16:
@@ -333,6 +336,7 @@ http:
             excludedIPs:
               - foobar
               - foobar
+            ipv6Subnet: 42
           requestHeaderName: foobar
           requestHost: true
     Middleware19:
diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
index 96816d8a4a..226d4a2f25 100644
--- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
+++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
@@ -1458,6 +1458,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
@@ -1491,6 +1497,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   rejectStatusCode:
                     description: |-
@@ -1523,6 +1534,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   sourceRange:
                     description: SourceRange defines the set of allowed IPs (or ranges
@@ -1691,6 +1707,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md
index bd9b99a7ae..01de7ee69c 100644
--- a/docs/content/reference/dynamic-configuration/kv-ref.md
+++ b/docs/content/reference/dynamic-configuration/kv-ref.md
@@ -103,18 +103,21 @@ THIS FILE MUST NOT BE EDITED BY HAND
 | `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/depth` | `42` |
 | `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/0` | `foobar` |
 | `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/excludedIPs/1` | `foobar` |
+| `traefik/http/middlewares/Middleware13/ipAllowList/ipStrategy/ipv6Subnet` | `42` |
 | `traefik/http/middlewares/Middleware13/ipAllowList/rejectStatusCode` | `42` |
 | `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/0` | `foobar` |
 | `traefik/http/middlewares/Middleware13/ipAllowList/sourceRange/1` | `foobar` |
 | `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/depth` | `42` |
 | `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/0` | `foobar` |
 | `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/excludedIPs/1` | `foobar` |
+| `traefik/http/middlewares/Middleware14/ipWhiteList/ipStrategy/ipv6Subnet` | `42` |
 | `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/0` | `foobar` |
 | `traefik/http/middlewares/Middleware14/ipWhiteList/sourceRange/1` | `foobar` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/amount` | `42` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/depth` | `42` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
+| `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/ipStrategy/ipv6Subnet` | `42` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHeaderName` | `foobar` |
 | `traefik/http/middlewares/Middleware15/inFlightReq/sourceCriterion/requestHost` | `true` |
 | `traefik/http/middlewares/Middleware16/passTLSClientCert/info/issuer/commonName` | `true` |
@@ -147,6 +150,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/depth` | `42` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/ipv6Subnet` | `42` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHeaderName` | `foobar` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/requestHost` | `true` |
 | `traefik/http/middlewares/Middleware19/redirectRegex/permanent` | `true` |
diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
index 45b3909723..baf3d5b1ec 100644
--- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
+++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
@@ -734,6 +734,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
@@ -767,6 +773,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   rejectStatusCode:
                     description: |-
@@ -799,6 +810,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   sourceRange:
                     description: SourceRange defines the set of allowed IPs (or ranges
@@ -967,6 +983,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml
index 96816d8a4a..226d4a2f25 100644
--- a/integration/fixtures/k8s/01-traefik-crd.yml
+++ b/integration/fixtures/k8s/01-traefik-crd.yml
@@ -1458,6 +1458,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
@@ -1491,6 +1497,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   rejectStatusCode:
                     description: |-
@@ -1523,6 +1534,11 @@ spec:
                         items:
                           type: string
                         type: array
+                      ipv6Subnet:
+                        description: IPv6Subnet configures Traefik to consider all
+                          IPv6 addresses from the defined subnet as originating from
+                          the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+                        type: integer
                     type: object
                   sourceRange:
                     description: SourceRange defines the set of allowed IPs (or ranges
@@ -1691,6 +1707,12 @@ spec:
                             items:
                               type: string
                             type: array
+                          ipv6Subnet:
+                            description: IPv6Subnet configures Traefik to consider
+                              all IPv6 addresses from the defined subnet as originating
+                              from the same IP. Applies to RemoteAddrStrategy and
+                              DepthStrategy.
+                            type: integer
                         type: object
                       requestHeaderName:
                         description: RequestHeaderName defines the name of the header
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index 9c65338a44..b2dcb084fb 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -1,6 +1,7 @@
 package dynamic
 
 import (
+	"fmt"
 	"net/http"
 	"time"
 
@@ -405,6 +406,8 @@ type IPStrategy struct {
 	Depth int `json:"depth,omitempty" toml:"depth,omitempty" yaml:"depth,omitempty" export:"true"`
 	// ExcludedIPs configures Traefik to scan the X-Forwarded-For header and select the first IP not in the list.
 	ExcludedIPs []string `json:"excludedIPs,omitempty" toml:"excludedIPs,omitempty" yaml:"excludedIPs,omitempty"`
+	// IPv6Subnet configures Traefik to consider all IPv6 addresses from the defined subnet as originating from the same IP. Applies to RemoteAddrStrategy and DepthStrategy.
+	IPv6Subnet *int `json:"ipv6Subnet,omitempty" toml:"ipv6Subnet,omitempty" yaml:"ipv6Subnet,omitempty"`
 	// TODO(mpl): I think we should make RemoteAddr an explicit field. For one thing, it would yield better documentation.
 }
 
@@ -418,8 +421,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
 	}
 
 	if s.Depth > 0 {
+		if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
+			return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
+		}
+
 		return &ip.DepthStrategy{
-			Depth: s.Depth,
+			Depth:      s.Depth,
+			IPv6Subnet: s.IPv6Subnet,
 		}, nil
 	}
 
@@ -433,7 +441,13 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
 		}, nil
 	}
 
-	return &ip.RemoteAddrStrategy{}, nil
+	if s.IPv6Subnet != nil && (*s.IPv6Subnet <= 0 || *s.IPv6Subnet > 128) {
+		return nil, fmt.Errorf("invalid IPv6 subnet %d value, should be greater to 0 and lower or equal to 128", *s.IPv6Subnet)
+	}
+
+	return &ip.RemoteAddrStrategy{
+		IPv6Subnet: s.IPv6Subnet,
+	}, nil
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go
index 890e897002..ba0e966062 100644
--- a/pkg/config/dynamic/zz_generated.deepcopy.go
+++ b/pkg/config/dynamic/zz_generated.deepcopy.go
@@ -704,6 +704,11 @@ func (in *IPStrategy) DeepCopyInto(out *IPStrategy) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.IPv6Subnet != nil {
+		in, out := &in.IPv6Subnet, &out.IPv6Subnet
+		*out = new(int)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/ip/strategy.go b/pkg/ip/strategy.go
index 16e150e95c..ed5e570a40 100644
--- a/pkg/ip/strategy.go
+++ b/pkg/ip/strategy.go
@@ -3,6 +3,7 @@ package ip
 import (
 	"net"
 	"net/http"
+	"net/netip"
 	"strings"
 )
 
@@ -16,7 +17,10 @@ type Strategy interface {
 }
 
 // RemoteAddrStrategy a strategy that always return the remote address.
-type RemoteAddrStrategy struct{}
+type RemoteAddrStrategy struct {
+	// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
+	IPv6Subnet *int
+}
 
 // GetIP returns the selected IP.
 func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
@@ -24,15 +28,22 @@ func (s *RemoteAddrStrategy) GetIP(req *http.Request) string {
 	if err != nil {
 		return req.RemoteAddr
 	}
+
+	if s.IPv6Subnet != nil {
+		return getIPv6SubnetIP(ip, *s.IPv6Subnet)
+	}
+
 	return ip
 }
 
 // DepthStrategy a strategy based on the depth inside the X-Forwarded-For from right to left.
 type DepthStrategy struct {
 	Depth int
+	// IPv6Subnet instructs the strategy to return the first IP of the subnet where IP belongs.
+	IPv6Subnet *int
 }
 
-// GetIP return the selected IP.
+// GetIP returns the selected IP.
 func (s *DepthStrategy) GetIP(req *http.Request) string {
 	xff := req.Header.Get(xForwardedFor)
 	xffs := strings.Split(xff, ",")
@@ -40,7 +51,14 @@ func (s *DepthStrategy) GetIP(req *http.Request) string {
 	if len(xffs) < s.Depth {
 		return ""
 	}
-	return strings.TrimSpace(xffs[len(xffs)-s.Depth])
+
+	ip := strings.TrimSpace(xffs[len(xffs)-s.Depth])
+
+	if s.IPv6Subnet != nil {
+		return getIPv6SubnetIP(ip, *s.IPv6Subnet)
+	}
+
+	return ip
 }
 
 // PoolStrategy is a strategy based on an IP Checker.
@@ -72,3 +90,23 @@ func (s *PoolStrategy) GetIP(req *http.Request) string {
 
 	return ""
 }
+
+// getIPv6SubnetIP returns the IPv6 subnet IP.
+// It returns the original IP when it is not an IPv6, or if parsing the IP has failed with an error.
+func getIPv6SubnetIP(ip string, ipv6Subnet int) string {
+	addr, err := netip.ParseAddr(ip)
+	if err != nil {
+		return ip
+	}
+
+	if !addr.Is6() {
+		return ip
+	}
+
+	prefix, err := addr.Prefix(ipv6Subnet)
+	if err != nil {
+		return ip
+	}
+
+	return prefix.Addr().String()
+}


Test patch:
diff --git a/pkg/config/dynamic/middleware_test.go b/pkg/config/dynamic/middleware_test.go
new file mode 100644
index 0000000000..87a0b34df4
--- /dev/null
+++ b/pkg/config/dynamic/middleware_test.go
@@ -0,0 +1,57 @@
+package dynamic
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_GetStrategy_ipv6Subnet(t *testing.T) {
+	testCases := []struct {
+		desc        string
+		expectError bool
+		ipv6Subnet  *int
+	}{
+		{
+			desc: "Nil subnet",
+		},
+		{
+			desc:        "Zero subnet",
+			expectError: true,
+			ipv6Subnet:  intPtr(0),
+		},
+		{
+			desc:        "Subnet greater that 128",
+			expectError: true,
+			ipv6Subnet:  intPtr(129),
+		},
+		{
+			desc:       "Valid subnet",
+			ipv6Subnet: intPtr(128),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			strategy := IPStrategy{
+				IPv6Subnet: test.ipv6Subnet,
+			}
+
+			get, err := strategy.Get()
+			if test.expectError {
+				require.Error(t, err)
+				assert.Nil(t, get)
+			} else {
+				require.NoError(t, err)
+				assert.NotNil(t, get)
+			}
+		})
+	}
+}
+
+func intPtr(value int) *int {
+	return &value
+}
diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go
index ce020a4385..be9c6f8ccc 100644
--- a/pkg/config/label/label_test.go
+++ b/pkg/config/label/label_test.go
@@ -90,10 +90,12 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.middlewares.Middleware8.headers.stsseconds":                                  "42",
 		"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.depth":                        "42",
 		"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.excludedips":                  "foobar, fiibar",
+		"traefik.http.middlewares.Middleware9.ipallowlist.ipstrategy.ipv6subnet":                   "42",
 		"traefik.http.middlewares.Middleware9.ipallowlist.sourcerange":                             "foobar, fiibar",
 		"traefik.http.middlewares.Middleware10.inflightreq.amount":                                 "42",
 		"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.depth":       "42",
 		"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.excludedips": "foobar, fiibar",
+		"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.ipstrategy.ipv6subnet":  "42",
 		"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requestheadername":      "foobar",
 		"traefik.http.middlewares.Middleware10.inflightreq.sourcecriterion.requesthost":            "true",
 		"traefik.http.middlewares.Middleware11.passtlsclientcert.info.notafter":                    "true",
@@ -123,6 +125,7 @@ func TestDecodeConfiguration(t *testing.T) {
 		"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.requesthost":              "true",
 		"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.depth":         "42",
 		"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.excludedips":   "foobar, foobar",
+		"traefik.http.middlewares.Middleware12.ratelimit.sourcecriterion.ipstrategy.ipv6subnet":    "42",
 		"traefik.http.middlewares.Middleware13.redirectregex.permanent":                            "true",
 		"traefik.http.middlewares.Middleware13.redirectregex.regex":                                "foobar",
 		"traefik.http.middlewares.Middleware13.redirectregex.replacement":                          "foobar",
@@ -392,6 +395,7 @@ func TestDecodeConfiguration(t *testing.T) {
 							IPStrategy: &dynamic.IPStrategy{
 								Depth:       42,
 								ExcludedIPs: []string{"foobar", "fiibar"},
+								IPv6Subnet:  intPtr(42),
 							},
 							RequestHeaderName: "foobar",
 							RequestHost:       true,
@@ -437,6 +441,7 @@ func TestDecodeConfiguration(t *testing.T) {
 							IPStrategy: &dynamic.IPStrategy{
 								Depth:       42,
 								ExcludedIPs: []string{"foobar", "foobar"},
+								IPv6Subnet:  intPtr(42),
 							},
 							RequestHeaderName: "foobar",
 							RequestHost:       true,
@@ -648,6 +653,7 @@ func TestDecodeConfiguration(t *testing.T) {
 								"foobar",
 								"fiibar",
 							},
+							IPv6Subnet: intPtr(42),
 						},
 					},
 				},
@@ -913,6 +919,7 @@ func TestEncodeConfiguration(t *testing.T) {
 							IPStrategy: &dynamic.IPStrategy{
 								Depth:       42,
 								ExcludedIPs: []string{"foobar", "fiibar"},
+								IPv6Subnet:  intPtr(42),
 							},
 							RequestHeaderName: "foobar",
 							RequestHost:       true,
@@ -957,6 +964,7 @@ func TestEncodeConfiguration(t *testing.T) {
 							IPStrategy: &dynamic.IPStrategy{
 								Depth:       42,
 								ExcludedIPs: []string{"foobar", "foobar"},
+								IPv6Subnet:  intPtr(42),
 							},
 							RequestHeaderName: "foobar",
 							RequestHost:       true,
@@ -1176,6 +1184,7 @@ func TestEncodeConfiguration(t *testing.T) {
 								"foobar",
 								"fiibar",
 							},
+							IPv6Subnet: intPtr(42),
 						},
 					},
 				},
@@ -1338,11 +1347,13 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds":                                  "42",
 		"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth":                        "42",
 		"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.ExcludedIPs":                  "foobar, fiibar",
+		"traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.IPv6Subnet":                   "42",
 		"traefik.HTTP.Middlewares.Middleware9.IPAllowList.RejectStatusCode":                        "0",
 		"traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange":                             "foobar, fiibar",
 		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount":                                 "42",
 		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth":       "42",
 		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.ExcludedIPs": "foobar, fiibar",
+		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.IPv6Subnet":  "42",
 		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHeaderName":      "foobar",
 		"traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.RequestHost":            "true",
 		"traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.NotAfter":                    "true",
@@ -1372,6 +1383,7 @@ func TestEncodeConfiguration(t *testing.T) {
 		"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.RequestHost":              "true",
 		"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.Depth":         "42",
 		"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.ExcludedIPs":   "foobar, foobar",
+		"traefik.HTTP.Middlewares.Middleware12.RateLimit.SourceCriterion.IPStrategy.IPv6Subnet":    "42",
 		"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Regex":                                "foobar",
 		"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Replacement":                          "foobar",
 		"traefik.HTTP.Middlewares.Middleware13.RedirectRegex.Permanent":                            "true",
@@ -1486,3 +1498,7 @@ func TestEncodeConfiguration(t *testing.T) {
 	}
 	assert.Equal(t, expected, labels)
 }
+
+func intPtr(value int) *int {
+	return &value
+}
diff --git a/pkg/ip/strategy_test.go b/pkg/ip/strategy_test.go
index b836223eb6..08178525a3 100644
--- a/pkg/ip/strategy_test.go
+++ b/pkg/ip/strategy_test.go
@@ -9,23 +9,81 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+const (
+	ipv6Basic            = "::abcd:ffff:c0a8:1"
+	ipv6BracketsPort     = "[::abcd:ffff:c0a8:1]:80"
+	ipv6BracketsZonePort = "[::abcd:ffff:c0a8:1%1]:80"
+)
+
 func TestRemoteAddrStrategy_GetIP(t *testing.T) {
 	testCases := []struct {
-		desc     string
-		expected string
+		desc       string
+		expected   string
+		remoteAddr string
+		ipv6Subnet *int
 	}{
+		// Valid IP format
 		{
-			desc:     "Use RemoteAddr",
+			desc:     "Use RemoteAddr, ipv4",
 			expected: "192.0.2.1",
 		},
+		{
+			desc:       "Use RemoteAddr, ipv6 brackets with port, no IPv6 subnet",
+			remoteAddr: ipv6BracketsPort,
+			expected:   "::abcd:ffff:c0a8:1",
+		},
+		{
+			desc:       "Use RemoteAddr, ipv6 brackets with zone and port, no IPv6 subnet",
+			remoteAddr: ipv6BracketsZonePort,
+			expected:   "::abcd:ffff:c0a8:1%1",
+		},
+
+		// Invalid IPv6 format
+		{
+			desc:       "Use RemoteAddr, ipv6 basic, missing brackets, no IPv6 subnet",
+			remoteAddr: ipv6Basic,
+			expected:   ipv6Basic,
+		},
+
+		// Valid IP format with subnet
+		{
+			desc:       "Use RemoteAddr, ipv4, ignore subnet",
+			expected:   "192.0.2.1",
+			ipv6Subnet: intPtr(24),
+		},
+		{
+			desc:       "Use RemoteAddr, ipv6 brackets with port, subnet",
+			remoteAddr: ipv6BracketsPort,
+			expected:   "::abcd:0:0:0",
+			ipv6Subnet: intPtr(80),
+		},
+		{
+			desc:       "Use RemoteAddr, ipv6 brackets with zone and port, subnet",
+			remoteAddr: ipv6BracketsZonePort,
+			expected:   "::abcd:0:0:0",
+			ipv6Subnet: intPtr(80),
+		},
+
+		// Valid IP, invalid subnet
+		{
+			desc:       "Use RemoteAddr, ipv6 brackets with port, invalid subnet",
+			remoteAddr: ipv6BracketsPort,
+			expected:   "::abcd:ffff:c0a8:1",
+			ipv6Subnet: intPtr(500),
+		},
 	}
 
 	for _, test := range testCases {
 		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
-			strategy := RemoteAddrStrategy{}
+			strategy := RemoteAddrStrategy{
+				IPv6Subnet: test.ipv6Subnet,
+			}
 			req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
+			if test.remoteAddr != "" {
+				req.RemoteAddr = test.remoteAddr
+			}
 			actual := strategy.GetIP(req)
 			assert.Equal(t, test.expected, actual)
 		})
@@ -38,6 +96,7 @@ func TestDepthStrategy_GetIP(t *testing.T) {
 		depth         int
 		xForwardedFor string
 		expected      string
+		ipv6Subnet    *int
 	}{
 		{
 			desc:          "Use depth",
@@ -57,13 +116,30 @@ func TestDepthStrategy_GetIP(t *testing.T) {
 			xForwardedFor: "10.0.0.2,10.0.0.1",
 			expected:      "10.0.0.2",
 		},
+		{
+			desc:          "Use depth with IPv4 subnet",
+			depth:         2,
+			xForwardedFor: "10.0.0.3,10.0.0.2,10.0.0.1",
+			expected:      "10.0.0.2",
+			ipv6Subnet:    intPtr(80),
+		},
+		{
+			desc:          "Use depth with IPv6 subnet",
+			depth:         2,
+			xForwardedFor: "10.0.0.3," + ipv6Basic + ",10.0.0.1",
+			expected:      "::abcd:0:0:0",
+			ipv6Subnet:    intPtr(80),
+		},
 	}
 
 	for _, test := range testCases {
 		t.Run(test.desc, func(t *testing.T) {
 			t.Parallel()
 
-			strategy := DepthStrategy{Depth: test.depth}
+			strategy := DepthStrategy{
+				Depth:      test.depth,
+				IPv6Subnet: test.ipv6Subnet,
+			}
 			req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
 			req.Header.Set(xForwardedFor, test.xForwardedFor)
 			actual := strategy.GetIP(req)
@@ -121,3 +197,7 @@ func TestTrustedIPsStrategy_GetIP(t *testing.T) {
 		})
 	}
 }
+
+func intPtr(value int) *int {
+	return &value
+}