从 http. Request 获取客户端 IP 地址的正确方法

http.Request获取所有客户端 IP 地址的正确方法是什么?在 PHP有很多 变量,我应该检查。围棋也是这样吗?

我发现的一个问题是:

req.RemoteAddr

请求区分大小写吗? 例如,x-forwarded-forX-Forwarded-ForX-FORWARDED-FOR相同? (来自 req.Header.Get("X-FORWARDED-FOR"))

240674 次浏览

In PHP there are a lot of variables that I should check. Is it the same on Go?

This has nothing to do with Go (or PHP for that matter). It just depends on what the client, proxy, load-balancer, or server is sending. Get the one you need depending on your environment.

http.Request.RemoteAddr contains the remote IP address. It may or may not be your actual client.

And is the request case sensitive? for example x-forwarded-for is the same as X-Forwarded-For and X-FORWARDED-FOR? (from req.Header.Get("X-FORWARDED-FOR"))

No, why not try it yourself? http://play.golang.org/p/YMf_UBvDsH

Looking at http.Request you can find the following member variables:

// HTTP defines that header names are case-insensitive.
// The request parser implements this by canonicalizing the
// name, making the first character and any characters
// following a hyphen uppercase and the rest lowercase.
//
// For client requests certain headers are automatically
// added and may override values in Header.
//
// See the documentation for the Request.Write method.
Header Header


// RemoteAddr allows HTTP servers and other software to record
// the network address that sent the request, usually for
// logging. This field is not filled in by ReadRequest and
// has no defined format. The HTTP server in this package
// sets RemoteAddr to an "IP:port" address before invoking a
// handler.
// This field is ignored by the HTTP client.
RemoteAddr string

You can use RemoteAddr to get the remote client's IP address and port (the format is "IP:port"), which is the address of the original requestor or the last proxy (for example a load balancer which lives in front of your server).

This is all you have for sure.

Then you can investigate the headers, which are case-insensitive (per documentation above), meaning all of your examples will work and yield the same result:

req.Header.Get("X-Forwarded-For") // capitalisation
req.Header.Get("x-forwarded-for") // doesn't
req.Header.Get("X-FORWARDED-FOR") // matter

This is because internally http.Header.Get will normalise the key for you. (If you want to access header map directly, and not through Get, you would need to use http.CanonicalHeaderKey first.)

Finally, "X-Forwarded-For" is probably the field you want to take a look at in order to grab more information about client's IP. This greatly depends on the HTTP software used on the remote side though, as client can put anything in there if it wishes to. Also, note the expected format of this field is the comma+space separated list of IP addresses. You will need to parse it a little bit to get a single IP of your choice (probably the first one in the list), for example:

// Assuming format is as expected
ips := strings.Split("10.0.0.1, 10.0.0.2, 10.0.0.3", ", ")
for _, ip := range ips {
fmt.Println(ip)
}

will produce:

10.0.0.1
10.0.0.2
10.0.0.3

Here a completely working example

package main


import (
// Standard library packages
"fmt"
"strconv"
"log"
"net"
"net/http"


// Third party packages
"github.com/julienschmidt/httprouter"
"github.com/skratchdot/open-golang/open"
)






// https://blog.golang.org/context/userip/userip.go
func getIP(w http.ResponseWriter, req *http.Request, _ httprouter.Params){
fmt.Fprintf(w, "<h1>static file server</h1><p><a href='./static'>folder</p></a>")


ip, port, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
//return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)


fmt.Fprintf(w, "userip: %q is not IP:port", req.RemoteAddr)
}


userIP := net.ParseIP(ip)
if userIP == nil {
//return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
fmt.Fprintf(w, "userip: %q is not IP:port", req.RemoteAddr)
return
}


// This will only be defined when site is accessed via non-anonymous proxy
// and takes precedence over RemoteAddr
// Header.Get is case-insensitive
forward := req.Header.Get("X-Forwarded-For")


fmt.Fprintf(w, "<p>IP: %s</p>", ip)
fmt.Fprintf(w, "<p>Port: %s</p>", port)
fmt.Fprintf(w, "<p>Forwarded for: %s</p>", forward)
}




func main() {
myport := strconv.Itoa(10002);




// Instantiate a new router
r := httprouter.New()


r.GET("/ip", getIP)


// Add a handler on /test
r.GET("/test", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Simply write some test data for now
fmt.Fprint(w, "Welcome!\n")
})




l, err := net.Listen("tcp", "localhost:" + myport)
if err != nil {
log.Fatal(err)
}
// The browser can connect now because the listening socket is open.




//err = open.Start("http://localhost:"+ myport + "/test")
err = open.Start("http://localhost:"+ myport + "/ip")
if err != nil {
log.Println(err)
}


// Start the blocking server loop.
log.Fatal(http.Serve(l, r))
}

This is how I come up with the IP

func ReadUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
}
return IPAddress
}
  • X-Real-Ip - fetches first true IP (if the requests sits behind multiple NAT sources/load balancer)

  • X-Forwarded-For - if for some reason X-Real-Ip is blank and does not return response, get from X-Forwarded-For

  • Remote Address - last resort (usually won't be reliable as this might be the last ip or if it is a naked http request to server ie no load balancer)

According to Mozilla MDN: "The X-Forwarded-For (XFF) header is a de-facto standard header for identifying the originating IP address of a client."
They publish clear information in their X-Forwarded-For article.

When using Cloudfront the client IP address is in the x-original-forwarded-for. Below is a Javascript example.

function getIp(request) {
const { headers, connection, socket } = request
const connectionSocket = connection && connection.socket


return (
(headers && headers['x-original-forwarded-for']) ||
(connection && connection.remoteAddress) ||
(socket && socket.remoteAddress) ||
(connectionSocket && connectionSocket.remoteAddress) ||
null
)
}

I think I have a better way than the current method posted.

package main


import (
"fmt"
"log"
"net"
"net/http"
"strings"
)


func main() {
http.HandleFunc("/", getUserIP)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}


// Get the IP address of the server's connected user.
func getUserIP(httpWriter http.ResponseWriter, httpServer *http.Request) {
var userIP string
if len(httpServer.Header.Get("CF-Connecting-IP")) > 1 {
userIP = httpServer.Header.Get("CF-Connecting-IP")
fmt.Println(net.ParseIP(userIP))
} else if len(httpServer.Header.Get("X-Forwarded-For")) > 1 {
userIP = httpServer.Header.Get("X-Forwarded-For")
fmt.Println(net.ParseIP(userIP))
} else if len(httpServer.Header.Get("X-Real-IP")) > 1 {
userIP = httpServer.Header.Get("X-Real-IP")
fmt.Println(net.ParseIP(userIP))
} else {
userIP = httpServer.RemoteAddr
if strings.Contains(userIP, ":") {
fmt.Println(net.ParseIP(strings.Split(userIP, ":")[0]))
} else {
fmt.Println(net.ParseIP(userIP))
}
}
}

The client can set the X-Forwarded-For header to any arbitrary value it wants. Usage X-Forwarded-For without check trusted proxies may lead to ip spoofing.

Header example: X-Forwarded-For: <client>, <proxy1>, <proxy2>

For example someone may call:

curl -H "X-Forwarded-For: 136.226.254.1" -H "X-Real-Ip: 136.226.254.2" "http://super.com"

If your L7 balancer don't check and cleanup this headers you will get ip spoofing in your code (136.226.254.1). If you have some logic based on client IP addresses, it won't work correctly. Throttling based on ip for example.

For example nginx module http://nginx.org/ru/docs/http/ngx_http_realip_module.html used logic based on getting last untrusted ip address in chain X-Forwarded-For addresses. I didn't find right middleware for go with same logic and write it: https://github.com/thrownew/go-middlewares/tree/main/clientip