NewLife/NewLife.Redis

Merge pull request #169 from NewLifeX/copilot/add-disable-ipv6-network-setting

Add IPv6/dual-stack connection controls for RedisClient
Stone authored at 2026-03-07 22:35:05 GitHub committed at 2026-03-07 22:35:05
4034579
Tree
2 Parent(s) 2e63c3a + 9fd18e9
Summary: 3 changed files with 160 additions and 16 deletions.
Modified +13 -1
Modified +48 -15
Added +99 -0
Modified +13 -1
diff --git a/NewLife.Redis/Redis.cs b/NewLife.Redis/Redis.cs
index 7f8131f..f6522ec 100644
--- a/NewLife.Redis/Redis.cs
+++ b/NewLife.Redis/Redis.cs
@@ -67,6 +67,12 @@ public class Redis : Cache, IConfigMapping, ILogFeature
     /// <remarks>var cert = new X509Certificate2("abc.pem", "pass");</remarks>
     public X509Certificate2? Certificate { get; set; }
 
+    /// <summary>是否启用IPv6。默认true</summary>
+    public Boolean IPv6 { get; set; } = true;
+
+    /// <summary>是否启用双栈套接字。默认true,仅在IPv6启用时有效</summary>
+    public Boolean DualMode { get; set; } = true;
+
     /// <summary>失败时抛出异常。默认true</summary>
     public Boolean ThrowOnFailure { get; set; } = true;
 
@@ -267,6 +273,12 @@ public class Redis : Cache, IConfigMapping, ILogFeature
             else if (dic.TryGetValue("connectTimeout", out str))
                 Timeout = str.ToInt();
 
+            if (dic.TryGetValue("IPv6", out str))
+                IPv6 = str.ToBoolean();
+
+            if (dic.TryGetValue("DualMode", out str))
+                DualMode = str.ToBoolean();
+
             if (dic.TryGetValue("ThrowOnFailure", out str))
                 ThrowOnFailure = str.ToBoolean();
 
@@ -1260,4 +1272,4 @@ public class Redis : Cache, IConfigMapping, ILogFeature
     /// <param name="args"></param>
     public void WriteLog(String format, params Object?[] args) => Log?.Info(format, args);
     #endregion
-}
\ No newline at end of file
+}
Modified +48 -15
diff --git a/NewLife.Redis/RedisClient.cs b/NewLife.Redis/RedisClient.cs
index c33f1f3..4e3e88b 100644
--- a/NewLife.Redis/RedisClient.cs
+++ b/NewLife.Redis/RedisClient.cs
@@ -1,5 +1,6 @@
-using System.Buffers;
+using System.Buffers;
 using System.Collections.Concurrent;
+using System.Net;
 using System.Net.Security;
 using System.Net.Sockets;
 using System.Security.Authentication;
@@ -111,15 +112,11 @@ public class RedisClient : DisposeBase
 
             var timeout = Timeout;
             if (timeout == 0) timeout = Host.Timeout;
-            tc = new TcpClient
-            {
-                SendTimeout = timeout,
-                ReceiveTimeout = timeout
-            };
+            tc = CreateClient(timeout);
 
             var uri = Server;
-            var addrs = uri.GetAddresses();
-            DefaultSpan.Current?.AppendTag($"addrs={addrs.Join()} port={uri.Port}");
+            var addrs = FilterAddresses(uri.GetAddresses());
+            DefaultSpan.Current?.AppendTag($"addrs={addrs.Join()} port={uri.Port} ipv6={Host.IPv6} dualMode={Host.DualMode}");
             tc.Connect(addrs, uri.Port);
 
             Client = tc;
@@ -168,15 +165,11 @@ public class RedisClient : DisposeBase
 
             var timeout = Timeout;
             if (timeout == 0) timeout = Host.Timeout;
-            tc = new TcpClient
-            {
-                SendTimeout = timeout,
-                ReceiveTimeout = timeout
-            };
+            tc = CreateClient(timeout);
 
             var uri = Server;
-            var addrs = uri.GetAddresses();
-            DefaultSpan.Current?.AppendTag($"addrs={addrs.Join()} port={uri.Port}");
+            var addrs = FilterAddresses(uri.GetAddresses());
+            DefaultSpan.Current?.AppendTag($"addrs={addrs.Join()} port={uri.Port} ipv6={Host.IPv6} dualMode={Host.DualMode}");
             await tc.ConnectAsync(addrs, uri.Port).ConfigureAwait(false);
 
             Client = tc;
@@ -198,6 +191,46 @@ public class RedisClient : DisposeBase
         return ns;
     }
 
+    /// <summary>创建Tcp客户端</summary>
+    /// <param name="timeout">超时时间</param>
+    /// <returns></returns>
+    protected virtual TcpClient CreateClient(Int32 timeout)
+    {
+        var tc = Host.IPv6 ? new TcpClient(AddressFamily.InterNetworkV6) : new TcpClient(AddressFamily.InterNetwork)
+        {
+            SendTimeout = timeout,
+            ReceiveTimeout = timeout
+        };
+
+        if (Host.IPv6 && tc.Client.AddressFamily == AddressFamily.InterNetworkV6)
+            tc.Client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, !Host.DualMode ? 1 : 0);
+
+        return tc;
+    }
+
+    /// <summary>过滤可连接地址</summary>
+    /// <param name="addrs">地址列表</param>
+    /// <returns></returns>
+    protected virtual IPAddress[] FilterAddresses(IPAddress[] addrs)
+    {
+        if (addrs == null) throw new ArgumentNullException(nameof(addrs));
+
+        if (Host.IPv6)
+        {
+            if (!Host.DualMode)
+                addrs = Array.FindAll(addrs, e => e.AddressFamily == AddressFamily.InterNetworkV6);
+        }
+        else
+        {
+            addrs = Array.FindAll(addrs, e => e.AddressFamily == AddressFamily.InterNetwork);
+        }
+
+        if (addrs.Length == 0)
+            throw new SocketException((Int32)SocketError.AddressNotAvailable);
+
+        return addrs;
+    }
+
     private Boolean OnCertificateValidationCallback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
     {
         // 如果没有证书,全部通过
Added +99 -0
diff --git a/XUnitTest/RedisClientTests.cs b/XUnitTest/RedisClientTests.cs
new file mode 100644
index 0000000..39eaa5c
--- /dev/null
+++ b/XUnitTest/RedisClientTests.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using NewLife.Caching;
+using NewLife.Net;
+using Xunit;
+
+namespace XUnitTest;
+
+public class RedisClientTests
+{
+    [Fact(DisplayName = "连接串支持IPv6和双栈开关")]
+    public void InitConfigWithIPv6Options()
+    {
+        var redis = new Redis();
+        redis.Init("server=127.0.0.1:6379;db=9;timeout=5000;IPv6=false;DualMode=false;");
+
+        Assert.Equal("127.0.0.1:6379", redis.Server);
+        Assert.Equal(9, redis.Db);
+        Assert.Equal(5000, redis.Timeout);
+        Assert.False(redis.IPv6);
+        Assert.False(redis.DualMode);
+    }
+
+    [Fact(DisplayName = "禁用IPv6时仅保留IPv4地址")]
+    public void FilterAddressesDisableIPv6()
+    {
+        var redis = new Redis
+        {
+            IPv6 = false
+        };
+        var client = new TestRedisClient(redis);
+
+        var addrs = client.Filter([
+            IPAddress.Loopback,
+            IPAddress.IPv6Loopback,
+            IPAddress.Parse("::ffff:127.0.0.1")
+        ]);
+
+        Assert.Single(addrs);
+        Assert.Equal(AddressFamily.InterNetwork, addrs[0].AddressFamily);
+        Assert.Equal(IPAddress.Loopback, addrs[0]);
+    }
+
+    [Fact(DisplayName = "禁用双栈时仅保留IPv6地址")]
+    public void FilterAddressesDisableDualMode()
+    {
+        var redis = new Redis
+        {
+            IPv6 = true,
+            DualMode = false
+        };
+        var client = new TestRedisClient(redis);
+
+        var addrs = client.Filter([
+            IPAddress.Loopback,
+            IPAddress.IPv6Loopback,
+            IPAddress.Parse("::ffff:127.0.0.1")
+        ]);
+
+        Assert.Equal(2, addrs.Length);
+        Assert.All(addrs, e => Assert.Equal(AddressFamily.InterNetworkV6, e.AddressFamily));
+    }
+
+    [Fact(DisplayName = "禁用IPv6时创建IPv4客户端")]
+    public void CreateClientDisableIPv6()
+    {
+        var redis = new Redis
+        {
+            IPv6 = false
+        };
+        using var client = new TestRedisClient(redis);
+        using var tcp = client.Create(3_000);
+
+        Assert.Equal(AddressFamily.InterNetwork, tcp.Client.AddressFamily);
+    }
+
+    [Fact(DisplayName = "禁用IPv6且无IPv4地址时抛出异常")]
+    public void FilterAddressesThrowWhenMissingIPv4()
+    {
+        var redis = new Redis
+        {
+            IPv6 = false
+        };
+        var client = new TestRedisClient(redis);
+
+        var ex = Assert.Throws<SocketException>(() => client.Filter([IPAddress.IPv6Loopback]));
+        Assert.Equal(SocketError.AddressNotAvailable, ex.SocketErrorCode);
+    }
+
+    private class TestRedisClient : RedisClient
+    {
+        public TestRedisClient(Redis redis) : base(redis, new NetUri("tcp://test.invalid:6379")) { }
+
+        public IPAddress[] Filter(IPAddress[] addrs) => base.FilterAddresses(addrs);
+
+        public TcpClient Create(Int32 timeout) => base.CreateClient(timeout);
+    }
+}