Repository: caddyserver/caddy (language: go)

Issue:
can't disable IPv4/IPv6 for dynamic reverse proxy from Caddyfile
As per the forum discussion at https://caddy.community/t/reverse-proxy-to-dynamic-ipv4-only/24773/3 , I found it's not possible to disable IPv4 or IPv6 in a `reverse_proxy` `dynamic a` block from the Caddyfile.

If I use
```
:9000 {
	reverse_proxy {
		dynamic a potoroo 8123 {
			versions ipv4
		}
	}
}
```

then I get a caddy.json which has `"versions":{"ipv4":true}}`, but Caddy still uses IPv6 if available. 

If I explicitly add `"ipv6":"false"` then it works, but this can't be set from the Caddyfile (that I'm aware of).


Discussion / hints:
Good find, thanks. Anyone is welcome to work on this! :)
Is the expected logic here that when `ipv4` is specified in `versions` , we should automatically set `ipv6` to false or are we adding the ability for the users to set `ipv6`  (or `ipv4` for instance) to false via the caddy file? 

Fix patch:
diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go
index 46e45c64652..c8ba930d203 100644
--- a/modules/caddyhttp/reverseproxy/upstreams.go
+++ b/modules/caddyhttp/reverseproxy/upstreams.go
@@ -231,6 +231,19 @@ type IPVersions struct {
 	IPv6 *bool `json:"ipv6,omitempty"`
 }
 
+func resolveIpVersion(versions *IPVersions) string {
+	resolveIpv4 := versions == nil || (versions.IPv4 == nil && versions.IPv6 == nil) || (versions.IPv4 != nil && *versions.IPv4)
+	resolveIpv6 := versions == nil || (versions.IPv6 == nil && versions.IPv4 == nil) || (versions.IPv6 != nil && *versions.IPv6)
+	switch {
+	case resolveIpv4 && !resolveIpv6:
+		return "ip4"
+	case !resolveIpv4 && resolveIpv6:
+		return "ip6"
+	default:
+		return "ip"
+	}
+}
+
 // AUpstreams provides upstreams from A/AAAA lookups.
 // Results are cached and refreshed at the configured
 // refresh interval.
@@ -313,9 +326,6 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
 func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
 
-	resolveIpv4 := au.Versions == nil || au.Versions.IPv4 == nil || *au.Versions.IPv4
-	resolveIpv6 := au.Versions == nil || au.Versions.IPv6 == nil || *au.Versions.IPv6
-
 	// Map ipVersion early, so we can use it as part of the cache-key.
 	// This should be fairly inexpensive and comes and the upside of
 	// allowing the same dynamic upstream (name + port combination)
@@ -324,15 +334,7 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 	// It also forced a cache-miss if a previously cached dynamic
 	// upstream changes its ip version, e.g. after a config reload,
 	// while keeping the cache-invalidation as simple as it currently is.
-	var ipVersion string
-	switch {
-	case resolveIpv4 && !resolveIpv6:
-		ipVersion = "ip4"
-	case !resolveIpv4 && resolveIpv6:
-		ipVersion = "ip6"
-	default:
-		ipVersion = "ip"
-	}
+	ipVersion := resolveIpVersion(au.Versions)
 
 	auStr := repl.ReplaceAll(au.String()+ipVersion, "")
 


Test patch:
diff --git a/modules/caddyhttp/reverseproxy/upstreams_test.go b/modules/caddyhttp/reverseproxy/upstreams_test.go
new file mode 100644
index 00000000000..48e2d2a6310
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/upstreams_test.go
@@ -0,0 +1,56 @@
+package reverseproxy
+
+import "testing"
+
+func TestResolveIpVersion(t *testing.T) {
+	falseBool := false
+	trueBool := true
+	tests := []struct {
+		Versions          *IPVersions
+		expectedIpVersion string
+	}{
+		{
+			Versions:          &IPVersions{IPv4: &trueBool},
+			expectedIpVersion: "ip4",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &falseBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &trueBool, IPv6: &falseBool},
+			expectedIpVersion: "ip4",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &trueBool},
+			expectedIpVersion: "ip6",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &falseBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &trueBool, IPv4: &falseBool},
+			expectedIpVersion: "ip6",
+		},
+		{
+			Versions:          &IPVersions{},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &trueBool, IPv6: &trueBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &falseBool, IPv6: &falseBool},
+			expectedIpVersion: "ip",
+		},
+	}
+	for _, test := range tests {
+		ipVersion := resolveIpVersion(test.Versions)
+		if ipVersion != test.expectedIpVersion {
+			t.Errorf("resolveIpVersion(): Expected %s got %s", test.expectedIpVersion, ipVersion)
+		}
+	}
+
+}