Merge pull request #169 from NewLifeX/copilot/add-disable-ipv6-network-setting Add IPv6/dual-stack connection controls for RedisClientStone authored at 2026-03-07 22:35:05 GitHub committed at 2026-03-07 22:35:05
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
+}
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)
{
// 如果没有证书,全部通过
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);
+ }
+}