Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib/internal/httptrace: store client_ip in tags for backend WAF #1328

Merged
merged 14 commits into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 92 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.RemoteAddr, r.Header, clientIPHeader); 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,69 @@ func FinishRequestSpan(s tracer.Span, status int, opts ...tracer.FinishOption) {
}
s.Finish(opts...)
}

// Helper function to return the IP network out of a string.
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
func ippref(s string) *netaddr.IPPrefix {
if prefix, err := netaddr.ParseIPPrefix(s); err == nil {
return &prefix
}
return nil
}

// getClientIP uses the request headers to resolve the client IP. If a specific header to check is provided through
// DD_CLIENT_IP_HEADER, then only this header is checked.
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
func getClientIP(remoteAddr string, headers http.Header, clientIPHeader string) netaddr.IP {
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
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 := headers.Get(hdr); v != "" {
if ip := check(v); ip.IsValid() {
return ip
}
}
}
if remoteIP := parseIP(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
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
globalCheck := !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
if !globalCheck || !ip.Is6() {
return globalCheck
}
for _, n := range ipv6SpecialNetworks {
if n.Contains(ip) {
return false
}
}
return globalCheck
}
197 changes: 197 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,197 @@ 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
userIPHeader 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},
userIPHeader: "custom-header",
},
{
name: "user-header-not-found",
expectedIP: netaddr.IP{},
headers: map[string]string{"x-forwarded-for": ipv4Global},
userIPHeader: "custom-header",
},
}, tcs...)

return tcs
}

func TestIPHeaders(t *testing.T) {
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)
}
require.Equal(t, tc.expectedIP.String(), getClientIP(tc.remoteAddr, header, tc.userIPHeader).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