Repository: cloudwego/hertz (go)

Issue:
ClientIP() not parse 'X-Forwarded-For'
**Describe the bug**
`ctx.ClientIP()` can't parse `X-Forwarded-For` while using Unix Domain Sock.

**To Reproduce**
```go
import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/common/config"
)

func main() {
	options := []config.Option{
		server.WithHostPorts("ingress.sock"),
		server.WithNetwork("unix"),
	}
	h := server.Default(options...)
	h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
		clientIp := ctx.ClientIP()
		ctx.JSON(200, map[string]any{
			"header":    ctx.Request.Header.Get("X-Forwarded-For"),
			"client_ip": clientIp,
		})
	})
	h.Spin()
}
```
run `socat` to emulate a nginx:
```
socat TCP-LISTEN:12345 UNIX-CONNECT:ingress.sock
```

**Expected behavior**

`header` = `client_ip` in the response body.

**Screenshots**

![Image](https://github.com/user-attachments/assets/c3700603-2f4d-457f-b500-6fe55f4fa285)

**Hertz version:**

v0.9.5



Pull request:
fix: ClientIP() handles uds instead of returning empty
fixes #1263

#### What type of PR is this?
fix

<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
- [ ] The description of this PR title is user-oriented and clear enough for others to understand.
- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.


#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en:
zh(optional):

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

Patch:
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 738c123..32e6785 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -99,43 +99,43 @@ var defaultClientIPOptions = ClientIPOptions{
 	TrustedCIDRs:    defaultTrustedCIDRs,
 }
 
+var loopbackIP = net.ParseIP("127.0.0.1")
+
 // ClientIPWithOption used to generate custom ClientIP function and set by engine.SetClientIPFunc
 func ClientIPWithOption(opts ClientIPOptions) ClientIP {
 	return func(ctx *RequestContext) string {
-		RemoteIPHeaders := opts.RemoteIPHeaders
-		TrustedCIDRs := opts.TrustedCIDRs
-
-		remoteIPStr, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
-		if err != nil {
-			return ""
-		}
-
-		remoteIP := net.ParseIP(remoteIPStr)
-		if remoteIP == nil {
-			return ""
+		remoteIPStr := ""
+		trustedProxy := false
+		if addr := ctx.RemoteAddr(); strings.HasPrefix(addr.Network(), "unix") {
+			// unix, unixgram, unixpacket is considered same as "127.0.0.1"
+			remoteIPStr = addr.String()
+			trustedProxy = isTrustedProxy(opts.TrustedCIDRs, loopbackIP)
+		} else {
+			h, _, err := net.SplitHostPort(strings.TrimSpace(addr.String()))
+			if err != nil {
+				return ""
+			}
+			remoteIPStr = h
+			trustedProxy = isTrustedProxy(opts.TrustedCIDRs, net.ParseIP(h))
 		}
 
-		trusted := isTrustedProxy(TrustedCIDRs, remoteIP)
-
-		if trusted {
-			for _, headerName := range RemoteIPHeaders {
-				ip, valid := validateHeader(TrustedCIDRs, ctx.Request.Header.Get(headerName))
+		if trustedProxy {
+			for _, headerName := range opts.RemoteIPHeaders {
+				ip, valid := validateHeader(opts.TrustedCIDRs, ctx.Request.Header.Get(headerName))
 				if valid {
 					return ip
 				}
 			}
 		}
-
 		return remoteIPStr
 	}
 }
 
 // isTrustedProxy will check whether the IP address is included in the trusted list according to trustedCIDRs
 func isTrustedProxy(trustedCIDRs []*net.IPNet, remoteIP net.IP) bool {
-	if trustedCIDRs == nil {
+	if trustedCIDRs == nil || remoteIP == nil {
 		return false
 	}
-
 	for _, cidr := range trustedCIDRs {
 		if cidr.Contains(remoteIP) {
 			return true


Test patch:
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index b2aef63..a5cb00a 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -848,32 +848,27 @@ func TestContextContentType(t *testing.T) {
 	assert.DeepEqual(t, consts.MIMEApplicationJSONUTF8, bytesconv.B2s(c.ContentType()))
 }
 
-type MockIpConn struct {
+type MockConn struct {
 	*mock.Conn
-	RemoteIp string
-	Port     int
+
+	remote net.Addr
 }
 
-func (c *MockIpConn) RemoteAddr() net.Addr {
-	return &net.UDPAddr{
-		IP:   net.ParseIP(c.RemoteIp),
-		Port: c.Port,
-	}
+func (c *MockConn) RemoteAddr() net.Addr {
+	return c.remote
 }
 
 func newContextClientIPTest() *RequestContext {
 	c := NewContext(0)
-	c.conn = &MockIpConn{
-		Conn:     mock.NewConn(""),
-		RemoteIp: "127.0.0.1",
-		Port:     8080,
+	c.conn = &MockConn{
+		remote: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
 	}
 	c.Request.Header.Set("X-Real-IP", " 10.10.10.10  ")
 	c.Request.Header.Set("X-Forwarded-For", "  20.20.20.20, 30.30.30.30")
 	return c
 }
 
-func TestClientIp(t *testing.T) {
+func TestClientIP(t *testing.T) {
 	c := newContextClientIPTest()
 	// default X-Forwarded-For and X-Real-IP behaviour
 	assert.DeepEqual(t, "20.20.20.20", c.ClientIP())
@@ -908,6 +903,14 @@ func TestClientIp(t *testing.T) {
 	}
 	c.SetClientIPFunc(ClientIPWithOption(opts))
 	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
+
+	// UDS
+	c.conn = &MockConn{remote: &net.UnixAddr{Net: "unix", Name: "/tmp/test.sock"}}
+	assert.DeepEqual(t, "30.30.30.30", c.ClientIP())
+
+	// err: Addr not host:port
+	c.conn = &MockConn{remote: &net.UnixAddr{Net: "tcp", Name: "/tmp/test.sock"}}
+	assert.DeepEqual(t, "", c.ClientIP())
 }
 
 func TestSetClientIPFunc(t *testing.T) {