From 6f41b600c5f69da9da9b33dafd1e733d2500b37a Mon Sep 17 00:00:00 2001 From: Jonathan Suever <653357+suever@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:58:50 -0400 Subject: [PATCH] fix(client): Do not assume that all non-IP hosts are loopbacks (#3085) * Do not assume that all non-IP hosts are loopbacks * handle localhost and Docker internal hostnames --------- Co-authored-by: Nedyalko Dyakov Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Co-authored-by: ofekshenawa Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- internal_test.go | 35 +++++++++++++++++++++++++++++++++++ osscluster.go | 17 +++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/internal_test.go b/internal_test.go index 4a655cff..8ba92722 100644 --- a/internal_test.go +++ b/internal_test.go @@ -383,3 +383,38 @@ var _ = Describe("ClusterClient", func() { }) }) }) + +var _ = Describe("isLoopback", func() { + DescribeTable("should correctly identify loopback addresses", + func(host string, expected bool) { + result := isLoopback(host) + Expect(result).To(Equal(expected)) + }, + // IP addresses + Entry("IPv4 loopback", "127.0.0.1", true), + Entry("IPv6 loopback", "::1", true), + Entry("IPv4 non-loopback", "192.168.1.1", false), + Entry("IPv6 non-loopback", "2001:db8::1", false), + + // Well-known loopback hostnames + Entry("localhost lowercase", "localhost", true), + Entry("localhost uppercase", "LOCALHOST", true), + Entry("localhost mixed case", "LocalHost", true), + + // Docker-specific loopbacks + Entry("host.docker.internal", "host.docker.internal", true), + Entry("HOST.DOCKER.INTERNAL", "HOST.DOCKER.INTERNAL", true), + Entry("custom.docker.internal", "custom.docker.internal", true), + Entry("app.docker.internal", "app.docker.internal", true), + + // Non-loopback hostnames + Entry("redis hostname", "redis-cluster", false), + Entry("FQDN", "redis.example.com", false), + Entry("docker but not internal", "redis.docker.com", false), + + // Edge cases + Entry("empty string", "", false), + Entry("invalid IP", "256.256.256.256", false), + Entry("partial docker internal", "docker.internal", false), + ) +}) diff --git a/osscluster.go b/osscluster.go index 7ea80cb2..d8894c2d 100644 --- a/osscluster.go +++ b/osscluster.go @@ -781,12 +781,25 @@ func replaceLoopbackHost(nodeAddr, originHost string) string { return net.JoinHostPort(originHost, nodePort) } +// isLoopback returns true if the host is a loopback address. +// For IP addresses, it uses net.IP.IsLoopback(). +// For hostnames, it recognizes well-known loopback hostnames like "localhost" +// and Docker-specific loopback patterns like "*.docker.internal". func isLoopback(host string) bool { ip := net.ParseIP(host) - if ip == nil { + if ip != nil { + return ip.IsLoopback() + } + + if strings.ToLower(host) == "localhost" { return true } - return ip.IsLoopback() + + if strings.HasSuffix(strings.ToLower(host), ".docker.internal") { + return true + } + + return false } func (c *clusterState) slotMasterNode(slot int) (*clusterNode, error) {