Skip to content

Commit

Permalink
contrib/internal/httptrace: store client ip in span tags (#1328)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hellzy committed Jun 15, 2022
1 parent d57ebc3 commit 495c17f
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 1 deletion.
93 changes: 93 additions & 0 deletions contrib/internal/httptrace/httptrace.go
Expand Up @@ -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) {
Expand All @@ -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))
}
Expand All @@ -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
}
201 changes: 201 additions & 0 deletions contrib/internal/httptrace/httptrace_test.go
Expand Up @@ -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"

Expand All @@ -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
}
}
}
3 changes: 3 additions & 0 deletions ddtrace/ext/tags.go
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
)
11 changes: 10 additions & 1 deletion go.sum
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down

0 comments on commit 495c17f

Please sign in to comment.