Repository: cloudwego/hertz (go)

Issue:
"ClientIP" handling is unsafe

https://github.com/cloudwego/hertz/blob/3ac19d5b1158badab0c7fc8e47b2e4ba667c4453/pkg/app/context.go#L980-L994

This code has a security risk, when using the ClientIP function it is easy to be spoofed by "X-Real-IP" and "X-Forwarded-For" to bypass the checks。

This problem also occurs with the Gin framework -> https://github.com/gin-gonic/gin/issues/2473
The fix can be found in their issue。


Pull request description:
fix: ClientIP handling is unsafe
#### What type of PR is this?

fix

#### Check the PR title.

- [x] This PR title match the format: \<type\>(optional scope): \<description\>
- [x] The description of this PR title is user-oriented and clear enough for others to understand.

#### (Optional) Translate the PR title into Chinese.

修复使用 ClientIP() 获取客户端 IP 时存在的安全问题

#### (Optional) More detail description for this PR(en: English/zh: Chinese).

en:
Problems.
1. The current ClientIP gets the client IP only by getting the "X-Real-IP" and "X-Forwarded-For" fields in the request header, which can be forged directly. The server has no ability to discern this. 
2. The "X-Forwarded-For" header is intended to be used to append the IP address of the proxy server in order when a proxy server is forwarded, and when requesting a client IP, it should be resolved in reverse until the first IP is resolved as the initial client IP (why not just get the first IP?). Because the proxy forwarding process is not necessarily completely secure, if an illegal proxy server px is added in the middle, such as client -> p0 -> p1 -> server becomes client -> p0 -> px -> p1 -> server then for security reasons, the ClientIP method should be called to get the IP of px instead of the client (which is why it is resolved in reverse order, rather than directly to the first IP in the "X-Forwarded-For" list).

Solution.
Referring to gin's fix idea, add TrustedProxies list to store a list of trusted proxy server addresses for the server, by default "0.0.0.0", the logic within the ClientIP method is as follows.
1. need to distinguish the difference between client accessing server directly and client accessing server through a proxy.
2. ctx.RemoteAddr() gets the IP address of the requesting party and determines whether the IP of the requesting party is in the trusted list, and returns the IP if the ClientIP is not trusted. 
3. If it is in the trusted list, it gets the headers of "X-Real-IP" and "X-Forwarded-For", and parses them in reverse order (the difference between the two is that X-Forwarded-For records the append result of the IP at the time of forwarding, but X-Real-IP only stores the IP of the requesting party of the previous hop, and under normal circumstances the last IP of X-Forwarded-For is the same as the IP of the requesting party. Forwarded-For last IP is the same as X-Real-IP), and returns the IP if it encounters a non-trusted IP address, or if it resolves to the first IP address. 
4. The difference with gin is that gin writes the TrustedProxies configuration into the Engine structure, whereas hertz allows user-defined implementations of the ClientIP function, so the fix refers to the resolution process, but does not dump it into the hertz Engine structure.

zh(optional): 
问题：
1. 当前 ClientIP 获取客户端 IP 只通过获取请求头中的 "X-Real-IP" 和 "X-Forwarded-For" 字段，而这个字段是可以直接伪造的。服务端没有判别能力。
2. 而且  "X-Forwarded-For" 头本意是用于当出现代理服务器转发时，可以在该请求头中依次 append 代理服务器的 IP 地址，在请求获取客户端 IP 时，应该反向解析，直到解析得到第一个 IP 作为最初的客户端的 IP（为什么不直接获取第一个 IP ？因为代理转发过程并不一定是完全安全的，如果中间有一个非法的代理服务器 px 加入，如client -> p0 -> p1 -> server 变为 client -> p0 -> px -> p1 -> server 则处于安全性考虑，调用 ClientIP 方法应该获取 px 的 IP 而非 client 的 IP，这也是为什么要逆序解析，而非直接解析 "X-Forwarded-For" 列表的首个 IP）。

解决方案：
参考了 gin 的修复思路，增加 TrustedProxies 列表存放服务器可信的代理服务器地址列表，默认情况下为 "0.0.0.0"，ClientIP 方法内逻辑如下：
1. 需要区分 client 直接访问 server 与 client 通过代理访问 server 的不同。
2. ctx.RemoteAddr() 获取请求方 IP 地址，判断请求来源的 IP 是否在可信列表中，不可信 ClientIP  返回该 IP。
3. 如果在可信列表中，则依次获取 "X-Real-IP" 和 "X-Forwarded-For" 的头部信息，逆序解析（二者区别在于 X-Forwarded-For 记录了转发时 IP 的 append 结果，但是 X-Real-IP 只是存放了前一跳的请求方的 IP，正常情况下 X-Forwarded-For  最后一个 IP 与 X-Real-IP 相同），如果遇到非可信的 IP 地址，或者解析到了第一个 IP 地址，则返回该 IP。
4. 与 gin 的不同在于，gin 将 TrustedProxies 相关配置写入了 Engine 结构，而 hertz 允许用户自定义 ClientIP 函数的实现，因此修复时参考了解析流程，但未侵入 hertz 的 Engine 结构。

#### Which issue(s) this PR fixes:

Fixes #161 


Patch:
diff --git a/cmd/hz/app/app.go b/cmd/hz/app/app.go
index a3ba38b..2069d55 100644
--- a/cmd/hz/app/app.go
+++ b/cmd/hz/app/app.go
@@ -53,6 +53,13 @@ func New(c *cli.Context) error {
 	if err != nil {
 		return cli.Exit(err, meta.PluginError)
 	}
+	// ".hz" file converges to the hz tool
+	manifest := new(meta.Manifest)
+	args.InitManifest(manifest)
+	err = manifest.Persist(args.OutDir)
+	if err != nil {
+		return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError)
+	}
 	return nil
 }
 
@@ -62,25 +69,28 @@ func Update(c *cli.Context) error {
 	if err != nil {
 		return cli.Exit(err, meta.LoadError)
 	}
+	setLogVerbose(args.Verbose)
+	logs.Debugf("Args: %#v\n", args)
+
 	manifest := new(meta.Manifest)
-	err = manifest.Validate(args.OutDir)
+	err = manifest.InitAndValidate(args.OutDir)
 	if err != nil {
 		return cli.Exit(err, meta.LoadError)
 	}
-
-	setLogVerbose(args.Verbose)
-	logs.Debugf("Args: %#v\n", args)
+	// update argument by ".hz", can automatically get "handler_dir"/"model_dir"/"router_dir"
+	args.UpdateByManifest(manifest)
 
 	err = TriggerPlugin(args)
 	if err != nil {
 		return cli.Exit(err, meta.PluginError)
 	}
-
-	manifest.Version = meta.GoVersion
-	err = manifest.Persist(".")
+	// If the "handler_dir"/"model_dir" is updated, write it back to ".hz"
+	args.UpdateManifest(manifest)
+	err = manifest.Persist(args.OutDir)
 	if err != nil {
 		return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError)
 	}
+
 	return nil
 }
 
@@ -139,8 +149,9 @@ func Init() *cli.App {
 	moduleFlag := cli.StringFlag{Name: "module", Aliases: []string{"mod"}, Usage: "Specify the Go module name.", Destination: &globalArgs.Gomod}
 	serviceNameFlag := cli.StringFlag{Name: "service", Usage: "Specify the service name.", Destination: &globalArgs.ServiceName}
 	outDirFlag := cli.StringFlag{Name: "out_dir", Usage: "Specify the project path.", Destination: &globalArgs.OutDir}
-	handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler path.", Destination: &globalArgs.HandlerDir}
-	modelDirFlag := cli.StringFlag{Name: "model_dir", Usage: "Specify the model path.", Destination: &globalArgs.ModelDir}
+	handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler relative path (based on \"out_dir\").", Destination: &globalArgs.HandlerDir}
+	modelDirFlag := cli.StringFlag{Name: "model_dir", Usage: "Specify the model relative path (based on \"out_dir\").", Destination: &globalArgs.ModelDir}
+	routerDirFlag := cli.StringFlag{Name: "router_dir", Usage: "Specify the router relative path (based on \"out_dir\").", Destination: &globalArgs.RouterDir}
 	baseDomainFlag := cli.StringFlag{Name: "base_domain", Usage: "Specify the request domain.", Destination: &globalArgs.BaseDomain}
 	clientDirFlag := cli.StringFlag{Name: "client_dir", Usage: "Specify the client path. If not specified, IDL generated path is used for 'client' command; no client code is generated for 'new' command", Destination: &globalArgs.ClientDir}
 
@@ -186,6 +197,7 @@ func Init() *cli.App {
 				&outDirFlag,
 				&handlerDirFlag,
 				&modelDirFlag,
+				&routerDirFlag,
 				&clientDirFlag,
 
 				&includesFlag,
@@ -308,6 +320,9 @@ func GenerateLayout(args *config.Argument) error {
 		ServiceName:     args.ServiceName,
 		UseApacheThrift: args.IdlType == meta.IdlThrift,
 		HasIdl:          0 != len(args.IdlPaths),
+		ModelDir:        args.ModelDir,
+		HandlerDir:      args.HandlerDir,
+		RouterDir:       args.RouterDir,
 	}
 
 	if args.CustomizeLayout == "" {
diff --git a/cmd/hz/config/argument.go b/cmd/hz/config/argument.go
index bf15fdf..253b2e2 100644
--- a/cmd/hz/config/argument.go
+++ b/cmd/hz/config/argument.go
@@ -24,6 +24,7 @@ import (
 
 	"github.com/cloudwego/hertz/cmd/hz/meta"
 	"github.com/cloudwego/hertz/cmd/hz/util"
+	"github.com/cloudwego/hertz/cmd/hz/util/logs"
 	"github.com/urfave/cli/v2"
 )
 
@@ -115,6 +116,21 @@ func (arg *Argument) parseStringSlice(c *cli.Context) {
 	arg.ProtobufPlugins = c.StringSlice("protoc-plugins")
 }
 
+func (arg *Argument) UpdateByManifest(m *meta.Manifest) {
+	if arg.HandlerDir == "" && m.HandlerDir != "" {
+		logs.Infof("use \"handler_dir\" in \".hz\" as the handler generated dir\n")
+		arg.HandlerDir = m.HandlerDir
+	}
+	if arg.ModelDir == "" && m.ModelDir != "" {
+		logs.Infof("use \"model_dir\" in \".hz\" as the model generated dir\n")
+		arg.ModelDir = m.ModelDir
+	}
+	if len(m.RouterDir) != 0 {
+		logs.Infof("use \"router_dir\" in \".hz\" as the router generated dir\n")
+		arg.RouterDir = m.RouterDir
+	}
+}
+
 // checkPath sets the project path and verifies that the model、handler、router and client path is compliant
 func (arg *Argument) checkPath() error {
 	dir, err := os.Getwd()
@@ -169,7 +185,7 @@ func (arg *Argument) checkIDL() error {
 	return nil
 }
 
-func (arg Argument) IsUpdate() bool {
+func (arg *Argument) IsUpdate() bool {
 	return arg.CmdType == meta.CmdUpdate
 }
 
@@ -330,3 +346,21 @@ func (arg *Argument) GetClientDir() (string, error) {
 	}
 	return util.RelativePath(arg.ClientDir)
 }
+
+func (arg *Argument) InitManifest(m *meta.Manifest) {
+	m.Version = meta.Version
+	m.HandlerDir = arg.HandlerDir
+	m.ModelDir = arg.ModelDir
+	m.RouterDir = arg.RouterDir
+}
+
+func (arg *Argument) UpdateManifest(m *meta.Manifest) {
+	m.Version = meta.Version
+	if arg.HandlerDir != m.HandlerDir {
+		m.HandlerDir = arg.HandlerDir
+	}
+	if arg.HandlerDir != m.ModelDir {
+		m.ModelDir = arg.ModelDir
+	}
+	// "router_dir" must not be defined by "update" command
+}
diff --git a/cmd/hz/generator/layout.go b/cmd/hz/generator/layout.go
index 82f566c..2db26b2 100644
--- a/cmd/hz/generator/layout.go
+++ b/cmd/hz/generator/layout.go
@@ -21,7 +21,9 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 	"reflect"
+	"strings"
 
 	"github.com/cloudwego/hertz/cmd/hz/util"
 	"gopkg.in/yaml.v2"
@@ -29,10 +31,14 @@ import (
 
 // Layout contains the basic information of idl
 type Layout struct {
+	OutDir          string
 	GoModule        string
 	ServiceName     string
 	UseApacheThrift bool
 	HasIdl          bool
+	ModelDir        string
+	HandlerDir      string
+	RouterDir       string
 }
 
 // LayoutGenerator contains the information generated by generating the layout template
@@ -95,6 +101,25 @@ func (lg *LayoutGenerator) GenerateByService(service Layout) error {
 		return err
 	}
 
+	if len(service.HandlerDir) != 0 {
+		// override the default "biz/handler/ping.go" to "HANDLER_DIR/ping.go"
+		defaultPingDir := defaultHandlerDir + sp + "ping.go"
+		if tpl, exist := lg.tpls[defaultPingDir]; exist {
+			delete(lg.tpls, defaultPingDir)
+			newPingDir := filepath.Clean(service.HandlerDir + sp + "ping.go")
+			lg.tpls[newPingDir] = tpl
+		}
+	}
+
+	if len(service.RouterDir) != 0 {
+		defaultRegisterDir := defaultRouterDir + sp + registerTplName
+		if tpl, exist := lg.tpls[defaultRegisterDir]; exist {
+			delete(lg.tpls, defaultRegisterDir)
+			newRegisterDir := filepath.Clean(service.RouterDir + sp + registerTplName)
+			lg.tpls[newRegisterDir] = tpl
+		}
+	}
+
 	sd, err := serviceToLayoutData(service)
 	if err != nil {
 		return err
@@ -105,7 +130,12 @@ func (lg *LayoutGenerator) GenerateByService(service Layout) error {
 		return err
 	}
 	if service.HasIdl {
-		delete(lg.tpls, defaultRouterDir+sp+registerTplName)
+		for k, _ := range lg.tpls {
+			if strings.Contains(k, registerTplName) {
+				delete(lg.tpls, k)
+				break
+			}
+		}
 	}
 
 	data := map[string]interface{}{
@@ -133,10 +163,18 @@ func serviceToLayoutData(service Layout) (map[string]interface{}, error) {
 
 // serviceToRouterData stores the registers function, router import path, handler import path
 func serviceToRouterData(service Layout) (map[string]interface{}, error) {
+	routerDir := sp + defaultRouterDir
+	handlerDir := sp + defaultHandlerDir
+	if len(service.RouterDir) != 0 {
+		routerDir = sp + service.RouterDir
+	}
+	if len(service.HandlerDir) != 0 {
+		handlerDir = sp + service.HandlerDir
+	}
 	return map[string]interface{}{
 		"Registers":      []string{},
-		"RouterPkgPath":  service.GoModule + util.PathToImport(sp+defaultRouterDir, ""),
-		"HandlerPkgPath": service.GoModule + util.PathToImport(sp+defaultHandlerDir, ""),
+		"RouterPkgPath":  filepath.Clean(service.GoModule + util.PathToImport(routerDir, "")),
+		"HandlerPkgPath": filepath.Clean(service.GoModule + util.PathToImport(handlerDir, "")),
 	}, nil
 }
 
diff --git a/cmd/hz/generator/layout_tpl.go b/cmd/hz/generator/layout_tpl.go
index 072c4c3..6d88bac 100644
--- a/cmd/hz/generator/layout_tpl.go
+++ b/cmd/hz/generator/layout_tpl.go
@@ -35,8 +35,8 @@ const (
 )
 
 const (
-	routerGenIndex = 9
-	routerIndex    = 10
+	routerGenIndex = 8
+	routerIndex    = 9
 
 	RegisterFile = "router_gen.go"
 )
@@ -122,13 +122,6 @@ _testmain.go
 dumped_hertz_remote_config.json
 		  `,
 		},
-		{
-			Path:   ".hz",
-			Delims: [2]string{"{{", "}}"},
-			Body: `// Code generated by hz. DO NOT EDIT.
-
-hz version: {{.hzVersion}}`,
-		},
 		{
 			Path: defaultHandlerDir + sp + "ping.go",
 			Body: `// Code generated by hertz generator.
diff --git a/cmd/hz/meta/manifest.go b/cmd/hz/meta/manifest.go
index 9f465eb..a4a7434 100644
--- a/cmd/hz/meta/manifest.go
+++ b/cmd/hz/meta/manifest.go
@@ -17,20 +17,22 @@
 package meta
 
 import (
+	"bytes"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 
-	"github.com/cloudwego/hertz/cmd/hz/util/logs"
 	gv "github.com/hashicorp/go-version"
+	"gopkg.in/yaml.v2"
 )
 
 const ManifestFile = ".hz"
 
 type Manifest struct {
-	Version *gv.Version
+	Version    string `yaml:"hz version"`
+	HandlerDir string `yaml:"handlerDir"`
+	ModelDir   string `yaml:"modelDir"`
+	RouterDir  string `yaml:"routerDir"`
 }
 
 var GoVersion *gv.Version
@@ -40,36 +42,35 @@ func init() {
 	GoVersion, _ = gv.NewVersion(Version)
 }
 
-var regVersion = regexp.MustCompile(`hz version: v([0-9a-z.]*)`)
-
-func (manifest *Manifest) Validate(dir string) error {
-	rd, err := ioutil.ReadFile(filepath.Join(dir, ManifestFile))
+func (manifest *Manifest) InitAndValidate(dir string) error {
+	m, err := loadConfigFile(filepath.Join(dir, ManifestFile))
 	if err != nil {
-		return err
+		return fmt.Errorf("can not load \".hz\", err: %v", err)
 	}
 
-	ms := regVersion.FindSubmatchIndex(rd)
-	if len(ms) != 4 || ms[2] < 19 {
-		logs.Errorf("Invalid manifest file: %s, pattern '%s' not match\n", filepath.Join(dir, ManifestFile), regVersion.String())
-		return fmt.Errorf("current project doesn't belong to hertz framework")
+	if len(m.Version) == 0 {
+		return fmt.Errorf("can not get hz version form \".hz\", current project doesn't belong to hertz framework")
 	}
-	v, err := gv.NewVersion(string(rd[ms[2]:ms[3]]))
+
+	*manifest = *m
+	_, err = gv.NewVersion(manifest.Version)
 	if err != nil {
-		return err
-	}
-	*manifest = Manifest{
-		Version: v,
+		return fmt.Errorf("invalid hz version in \".hz\", err: %v", err)
 	}
+
 	return nil
 }
 
-func (manifest Manifest) String() string {
-	return `// Code generated by hz. DO NOT EDIT.
+const hzTitle = "// Code generated by hz. DO NOT EDIT.\n"
 
-hz version: v` + manifest.Version.String() + "\n"
+func (manifest *Manifest) String() string {
+	conf, _ := yaml.Marshal(*manifest)
+
+	return hzTitle + "\n" +
+		string(conf)
 }
 
-func (manifest Manifest) Persist(dir string) error {
+func (manifest *Manifest) Persist(dir string) error {
 	file := filepath.Join(dir, ManifestFile)
 	fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
 	if err != nil {
@@ -79,3 +80,17 @@ func (manifest Manifest) Persist(dir string) error {
 	_, err = fd.WriteString(manifest.String())
 	return err
 }
+
+// loadConfigFile load config file from path
+func loadConfigFile(path string) (*Manifest, error) {
+	file, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	var manifest Manifest
+	file = bytes.TrimPrefix(file, []byte(hzTitle))
+	if err = yaml.Unmarshal(file, &manifest); err != nil {
+		return nil, fmt.Errorf("decode \".hz\" failed, err: %v", err)
+	}
+	return &manifest, nil
+}
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 157674c..e86cc4b 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -78,22 +78,72 @@ type Handler interface {
 
 type ClientIP func(ctx *RequestContext) string
 
-var defaultClientIP = func(ctx *RequestContext) string {
-	RemoteIPHeaders := []string{"X-Real-IP", "X-Forwarded-For"}
-	for _, headerName := range RemoteIPHeaders {
-		ip := ctx.Request.Header.Get(headerName)
-		if ip != "" {
-			return ip
+type ClientIPOptions struct {
+	RemoteIPHeaders []string
+	TrustedProxies  map[string]bool
+}
+
+var defaultClientIPOptions = ClientIPOptions{
+	RemoteIPHeaders: []string{"X-Real-IP", "X-Forwarded-For"},
+	TrustedProxies: map[string]bool{
+		"0.0.0.0": true,
+	},
+}
+
+// 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
+		TrustedProxies := opts.TrustedProxies
+
+		remoteIP, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String()))
+		if err != nil {
+			return ""
+		}
+		trusted := isTrustedProxy(TrustedProxies, remoteIP)
+
+		if trusted {
+			for _, headerName := range RemoteIPHeaders {
+				ip, valid := validateHeader(TrustedProxies, ctx.Request.Header.Get(headerName))
+				if valid {
+					return ip
+				}
+			}
 		}
+
+		return remoteIP
 	}
+}
 
-	if ip, _, err := net.SplitHostPort(strings.TrimSpace(ctx.RemoteAddr().String())); err == nil {
-		return ip
+// isTrustedProxy will check whether the IP address is included in the trusted list according to TrustedProxies
+func isTrustedProxy(trustedProxies map[string]bool, remoteIP string) bool {
+	return trustedProxies[remoteIP]
+}
+
+// validateHeader will parse X-Real-IP and X-Forwarded-For header and return the Initial client IP address or an untrusted IP address
+func validateHeader(trustedProxies map[string]bool, header string) (clientIP string, valid bool) {
+	if header == "" {
+		return "", false
 	}
+	items := strings.Split(header, ",")
+	for i := len(items) - 1; i >= 0; i-- {
+		ipStr := strings.TrimSpace(items[i])
+		ip := net.ParseIP(ipStr)
+		if ip == nil {
+			break
+		}
 
-	return ""
+		// X-Forwarded-For is appended by proxy
+		// Check IPs in reverse order and stop when find untrusted proxy
+		if (i == 0) || (!isTrustedProxy(trustedProxies, ipStr)) {
+			return ipStr, true
+		}
+	}
+	return "", false
 }
 
+var defaultClientIP = ClientIPWithOption(defaultClientIPOptions)
+
 // SetClientIPFunc sets ClientIP function implementation to get ClientIP.
 // Deprecated: Use engine.SetClientIPFunc instead of SetClientIPFunc
 func SetClientIPFunc(fn ClientIP) {


Test patch:
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 4e62a94..31d6da0 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -673,18 +673,48 @@ func TestContextContentType(t *testing.T) {
 func TestClientIp(t *testing.T) {
 	c := NewContext(0)
 	c.conn = mock.NewConn("")
-	// Case 1
-	c.Request.Header.Set("X-Forwarded-For", "126.0.0.2")
+	// 0.0.0.0 simulates a trusted proxy server
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
 	val := c.ClientIP()
 	if val != "126.0.0.2" {
 		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
 	}
-	// Case 2
+	// no proxy server
+	c = NewContext(0)
+	c.conn = mock.NewConn("")
 	c.Request.Header.Set("X-Real-Ip", "126.0.0.1")
 	val = c.ClientIP()
 	if val != "126.0.0.1" {
 		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.1")
 	}
+	// custom RemoteIPHeaders and TrustedProxies
+	opts := ClientIPOptions{
+		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+		TrustedProxies: map[string]bool{
+			"0.0.0.0": true,
+		},
+	}
+	c = NewContext(0)
+	c.SetClientIPFunc(ClientIPWithOption(opts))
+	c.conn = mock.NewConn("")
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
+	val = c.ClientIP()
+	if val != "126.0.0.2" {
+		t.Fatalf("unexpected %v. Expecting %v", val, "126.0.0.2")
+	}
+	// no trusted proxy server
+	opts = ClientIPOptions{
+		RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+		TrustedProxies:  nil,
+	}
+	c = NewContext(0)
+	c.SetClientIPFunc(ClientIPWithOption(opts))
+	c.conn = mock.NewConn("")
+	c.Request.Header.Set("X-Forwarded-For", "  126.0.0.2, 0.0.0.0 ")
+	val = c.ClientIP()
+	if val != "0.0.0.0" {
+		t.Fatalf("unexpected %v. Expecting %v", val, "0.0.0.0")
+	}
 }
 
 func TestSetClientIPFunc(t *testing.T) {