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 all 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
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