diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index fe08e1c38d..8d9fdca727 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -10,14 +10,37 @@ package httptrace import ( "context" "fmt" + "net" "net/http" + "os" "strconv" + "strings" + + "inet.af/netaddr" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" ) +var ( + ipv6SpecialNetworks = []*netaddr.IPPrefix{ + ippref("fec0::/10"), // site local + } + defaultIPHeaders = []string{ + "x-forwarded-for", + "x-real-ip", + "x-client-ip", + "x-forwarded", + "x-cluster-client-ip", + "forwarded-for", + "forwarded", + "via", + "true-client-ip", + } + clientIPHeader = os.Getenv("DD_TRACE_CLIENT_IP_HEADER") +) + // StartRequestSpan starts an HTTP request span with the standard list of HTTP request span tags (http.method, http.url, // http.useragent). Any further span start option can be added with opts. func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.Span, context.Context) { @@ -34,6 +57,9 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. tracer.Tag("http.host", r.Host), }, opts...) } + if ip := getClientIP(r); ip.IsValid() { + opts = append(opts, tracer.Tag(ext.HTTPClientIP, ip.String())) + } if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil { opts = append(opts, tracer.ChildOf(spanctx)) } @@ -55,3 +81,70 @@ func FinishRequestSpan(s tracer.Span, status int, opts ...tracer.FinishOption) { } s.Finish(opts...) } + +// ippref returns the IP network from an IP address string s. If not possible, it returns nil. +func ippref(s string) *netaddr.IPPrefix { + if prefix, err := netaddr.ParseIPPrefix(s); err == nil { + return &prefix + } + return nil +} + +// getClientIP attempts to find the client IP address in the given request r. +func getClientIP(r *http.Request) netaddr.IP { + ipHeaders := defaultIPHeaders + if len(clientIPHeader) > 0 { + ipHeaders = []string{clientIPHeader} + } + check := func(s string) netaddr.IP { + for _, ipstr := range strings.Split(s, ",") { + ip := parseIP(strings.TrimSpace(ipstr)) + if !ip.IsValid() { + continue + } + if isGlobal(ip) { + return ip + } + } + return netaddr.IP{} + } + for _, hdr := range ipHeaders { + if v := r.Header.Get(hdr); v != "" { + if ip := check(v); ip.IsValid() { + return ip + } + } + } + if remoteIP := parseIP(r.RemoteAddr); remoteIP.IsValid() && isGlobal(remoteIP) { + return remoteIP + } + return netaddr.IP{} +} + +func parseIP(s string) netaddr.IP { + if ip, err := netaddr.ParseIP(s); err == nil { + return ip + } + if h, _, err := net.SplitHostPort(s); err == nil { + if ip, err := netaddr.ParseIP(h); err == nil { + return ip + } + } + return netaddr.IP{} +} + +func isGlobal(ip netaddr.IP) bool { + // IsPrivate also checks for ipv6 ULA. + // We care to check for these addresses are not considered public, hence not global. + // See https://www.rfc-editor.org/rfc/rfc4193.txt for more details. + isGlobal := !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() + if !isGlobal || !ip.Is6() { + return isGlobal + } + for _, n := range ipv6SpecialNetworks { + if n.Contains(ip) { + return false + } + } + return isGlobal +} diff --git a/contrib/internal/httptrace/httptrace_test.go b/contrib/internal/httptrace/httptrace_test.go index 1cd93636de..391d5f8dfa 100644 --- a/contrib/internal/httptrace/httptrace_test.go +++ b/contrib/internal/httptrace/httptrace_test.go @@ -6,10 +6,13 @@ package httptrace import ( + "math/rand" "net/http" "net/http/httptest" "testing" + "inet.af/netaddr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,3 +30,201 @@ func TestStartRequestSpan(t *testing.T) { require.Len(t, spans, 1) assert.Equal(t, "example.com", spans[0].Tag("http.host")) } + +type IPTestCase struct { + name string + remoteAddr string + headers map[string]string + expectedIP netaddr.IP + clientIPHeader string +} + +func genIPTestCases() []IPTestCase { + ipv4Global := randGlobalIPv4().String() + ipv6Global := randGlobalIPv6().String() + ipv4Private := randPrivateIPv4().String() + ipv6Private := randPrivateIPv6().String() + tcs := []IPTestCase{} + // Simple ipv4 test cases over all headers + for _, header := range defaultIPHeaders { + tcs = append(tcs, IPTestCase{ + name: "ipv4-global." + header, + headers: map[string]string{header: ipv4Global}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }) + tcs = append(tcs, IPTestCase{ + name: "ipv4-private." + header, + headers: map[string]string{header: ipv4Private}, + expectedIP: netaddr.IP{}, + }) + } + // Simple ipv6 test cases over all headers + for _, header := range defaultIPHeaders { + tcs = append(tcs, IPTestCase{ + name: "ipv6-global." + header, + headers: map[string]string{header: ipv6Global}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }) + tcs = append(tcs, IPTestCase{ + name: "ipv6-private." + header, + headers: map[string]string{header: ipv6Private}, + expectedIP: netaddr.IP{}, + }) + } + // private and global in same header + tcs = append([]IPTestCase{ + { + name: "ipv4-private+global", + headers: map[string]string{"x-forwarded-for": ipv4Private + "," + ipv4Global}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }, + { + name: "ipv4-global+private", + headers: map[string]string{"x-forwarded-for": ipv4Global + "," + ipv4Private}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }, + { + name: "ipv6-private+global", + headers: map[string]string{"x-forwarded-for": ipv6Private + "," + ipv6Global}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }, + { + name: "ipv6-global+private", + headers: map[string]string{"x-forwarded-for": ipv6Global + "," + ipv6Private}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }, + }, tcs...) + // Invalid IPs (or a mix of valid/invalid over a single or multiple headers) + tcs = append([]IPTestCase{ + { + name: "invalid-ipv4", + headers: map[string]string{"x-forwarded-for": "127..0.0.1"}, + expectedIP: netaddr.IP{}, + }, + { + name: "invalid-ipv4-recover", + headers: map[string]string{"x-forwarded-for": "127..0.0.1, " + ipv4Global}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }, + { + name: "invalid-ipv4-recover-multi-header-1", + headers: map[string]string{"x-forwarded-for": "127..0.0.1", "forwarded-for": ipv4Global}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }, + { + name: "invalid-ipv4-recover-multi-header-2", + headers: map[string]string{"forwarded-for": ipv4Global, "x-forwarded-for": "127..0.0.1"}, + expectedIP: netaddr.MustParseIP(ipv4Global), + }, + { + name: "invalid-ipv6", + headers: map[string]string{"x-forwarded-for": "2001:0db8:2001:zzzz::"}, + expectedIP: netaddr.IP{}, + }, + { + name: "invalid-ipv6-recover", + headers: map[string]string{"x-forwarded-for": "2001:0db8:2001:zzzz::, " + ipv6Global}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }, + { + name: "invalid-ipv6-recover-multi-header-1", + headers: map[string]string{"x-forwarded-for": "2001:0db8:2001:zzzz::", "forwarded-for": ipv6Global}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }, + { + name: "invalid-ipv6-recover-multi-header-2", + headers: map[string]string{"forwarded-for": ipv6Global, "x-forwarded-for": "2001:0db8:2001:zzzz::"}, + expectedIP: netaddr.MustParseIP(ipv6Global), + }, + }, tcs...) + tcs = append([]IPTestCase{ + { + name: "no-headers", + expectedIP: netaddr.IP{}, + }, + { + name: "header-case", + expectedIP: netaddr.MustParseIP(ipv4Global), + headers: map[string]string{"X-fOrWaRdEd-FoR": ipv4Global}, + }, + { + name: "user-header", + expectedIP: netaddr.MustParseIP(ipv4Global), + headers: map[string]string{"x-forwarded-for": ipv6Global, "custom-header": ipv4Global}, + clientIPHeader: "custom-header", + }, + { + name: "user-header-not-found", + expectedIP: netaddr.IP{}, + headers: map[string]string{"x-forwarded-for": ipv4Global}, + clientIPHeader: "custom-header", + }, + }, tcs...) + + return tcs +} + +func TestIPHeaders(t *testing.T) { + // Make sure to restore the real value of clientIPHeader at the end of the test + defer func(s string) { clientIPHeader = s }(clientIPHeader) + for _, tc := range genIPTestCases() { + t.Run(tc.name, func(t *testing.T) { + header := http.Header{} + for k, v := range tc.headers { + header.Add(k, v) + } + r := http.Request{Header: header, RemoteAddr: tc.remoteAddr} + clientIPHeader = tc.clientIPHeader + require.Equal(t, tc.expectedIP.String(), getClientIP(&r).String()) + }) + } +} + +func randIPv4() netaddr.IP { + return netaddr.IPv4(uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32())) +} + +func randIPv6() netaddr.IP { + return netaddr.IPv6Raw([16]byte{ + uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), + uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), + uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), + uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()), + }) +} + +func randGlobalIPv4() netaddr.IP { + for { + ip := randIPv4() + if isGlobal(ip) { + return ip + } + } +} + +func randGlobalIPv6() netaddr.IP { + for { + ip := randIPv6() + if isGlobal(ip) { + return ip + } + } +} + +func randPrivateIPv4() netaddr.IP { + for { + ip := randIPv4() + if !isGlobal(ip) && ip.IsPrivate() { + return ip + } + } +} + +func randPrivateIPv6() netaddr.IP { + for { + ip := randIPv6() + if !isGlobal(ip) && ip.IsPrivate() { + return ip + } + } +} diff --git a/ddtrace/ext/tags.go b/ddtrace/ext/tags.go index e7337d7ae7..767bd4ea7c 100644 --- a/ddtrace/ext/tags.go +++ b/ddtrace/ext/tags.go @@ -36,6 +36,9 @@ const ( // HTTPUserAgent is the user agent header value of the HTTP request. HTTPUserAgent = "http.useragent" + // HTTPClientIP sets the HTTP client IP tag. + HTTPClientIP = "http.client_ip" + // SpanName is a pseudo-key for setting a span's operation name by means of // a tag. It is mostly here to facilitate vendor-agnostic frameworks like Opentracing // and OpenCensus. diff --git a/go.mod b/go.mod index e902d1f554..40d565ff3a 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,7 @@ require ( gorm.io/driver/postgres v1.0.0 gorm.io/driver/sqlserver v1.0.4 gorm.io/gorm v1.20.6 + inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 k8s.io/apimachinery v0.17.0 k8s.io/client-go v0.17.0 ) diff --git a/go.sum b/go.sum index 20458c9550..5ef083d7e9 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,7 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= @@ -793,6 +794,10 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -966,6 +971,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1049,8 +1055,9 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200527183253-8e7acdbce89d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1185,6 +1192,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM= k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo=