coder/coderd/httpmw/realip_test.go

650 lines
17 KiB
Go

package httpmw_test
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/httpmw"
)
// TestExtractAddress checks the ExtractAddress function.
func TestExtractAddress(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Config *httpmw.RealIPConfig
Header http.Header
RemoteAddr string
TLS bool
ExpectedRemoteAddr string
ExpectedTLS bool
}{
{
Name: "default-nil-config",
RemoteAddr: "123.45.67.89",
ExpectedRemoteAddr: "123.45.67.89",
},
{
Name: "default-empty-config",
RemoteAddr: "123.45.67.89",
ExpectedRemoteAddr: "123.45.67.89",
Config: &httpmw.RealIPConfig{},
},
{
Name: "default-filter-headers",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("10.0.0.0"),
Mask: net.CIDRMask(8, 32),
},
},
},
RemoteAddr: "123.45.67.89",
Header: http.Header{
"X-Forwarded-For": []string{
"127.0.0.1",
"10.0.0.5",
"10.0.0.5,4.4.4.4",
},
},
ExpectedRemoteAddr: "123.45.67.89",
},
{
Name: "multiple-x-forwarded-for",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Forwarded-For",
},
},
RemoteAddr: "123.45.67.89",
Header: http.Header{
"X-Forwarded-For": []string{
"10.24.1.1,1.2.3.4,1.1.1.1,4.5.6.7",
"10.0.0.5",
"10.0.0.5,4.4.4.4",
},
},
ExpectedRemoteAddr: "10.24.1.1",
},
{
Name: "single-real-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Real-Ip",
},
},
RemoteAddr: "123.45.67.89",
TLS: true,
Header: http.Header{
"X-Real-Ip": []string{"8.8.8.8"},
},
ExpectedRemoteAddr: "8.8.8.8",
ExpectedTLS: true,
},
{
Name: "multiple-real-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Real-Ip",
},
},
RemoteAddr: "123.45.67.89",
Header: http.Header{
"X-Real-Ip": []string{"4.4.4.4", "8.8.8.8"},
},
ExpectedRemoteAddr: "4.4.4.4",
},
{
// Has X-Forwarded-For and X-Real-Ip, prefers X-Real-Ip
Name: "prefer-real-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Real-Ip",
"X-Forwarded-For",
},
},
RemoteAddr: "123.45.67.89",
Header: http.Header{
"X-Forwarded-For": []string{"8.8.8.8"},
"X-Real-Ip": []string{"4.4.4.4"},
},
ExpectedRemoteAddr: "4.4.4.4",
},
{
// Has X-Forwarded-For, X-Real-Ip, and True-Client-Ip, prefers
// True-Client-Ip
Name: "prefer-true-client-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("123.45.0.0"),
Mask: net.CIDRMask(16, 32),
},
},
TrustedHeaders: []string{
"True-Client-Ip",
"X-Forwarded-For",
"X-Real-Ip",
},
},
RemoteAddr: "123.45.67.89",
TLS: true,
Header: http.Header{
"X-Forwarded-For": []string{"1.2.3.4"},
"X-Real-Ip": []string{"4.4.4.4", "8.8.8.8"},
"True-Client-Ip": []string{"5.6.7.8", "9.8.7.6"},
},
ExpectedRemoteAddr: "5.6.7.8",
ExpectedTLS: true,
},
{
// Has X-Forwarded-For, X-Real-Ip, True-Client-Ip, and
// Cf-Connecting-Ip, prefers Cf-Connecting-Ip
Name: "prefer-cf-connecting-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("123.45.67.89"),
Mask: net.CIDRMask(32, 32),
},
},
TrustedHeaders: []string{
"Cf-Connecting-Ip",
"X-Forwarded-For",
"X-Real-Ip",
"True-Client-Ip",
},
},
RemoteAddr: "123.45.67.89",
Header: http.Header{
"X-Forwarded-For": []string{"1.2.3.4,100.12.1.3,10.10.10.10"},
"X-Real-Ip": []string{"4.4.4.4", "8.8.8.8"},
"True-Client-Ip": []string{"5.6.7.8", "9.8.7.6"},
"Cf-Connecting-Ip": []string{"100.10.2.2"},
},
ExpectedRemoteAddr: "100.10.2.2",
},
}
for _, test := range tests {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
// Default to a direct (unproxied) connection over HTTP
req.RemoteAddr = test.RemoteAddr
if test.TLS {
req.TLS = &tls.ConnectionState{}
} else {
req.TLS = nil
}
req.Header = test.Header
info, err := httpmw.ExtractRealIPAddress(test.Config, req)
require.NoError(t, err, "unexpected error in ExtractAddress")
require.Equal(t, test.ExpectedRemoteAddr, info.String(), "expected info.String() to match")
})
}
}
// TestTrustedOrigins tests different settings for TrustedOrigins.
func TestTrustedOrigins(t *testing.T) {
t.Parallel()
// Remote client protocol: HTTP or HTTPS
for _, proto := range []string{"http", "https"} {
// Trusted origin
// all: default behavior, trust all origins
// none: use an empty set (nothing will be accepted in this case)
// ipv4: trust an IPv6 network
// ipv6: trust an IPv4 network
for _, trusted := range []string{"none", "ipv4", "ipv6"} {
for _, header := range []string{"Cf-Connecting-Ip", "True-Client-Ip", "X-Real-Ip", "X-Forwarded-For"} {
trusted := trusted
header := header
proto := proto
name := fmt.Sprintf("%s-%s-%s", trusted, proto, strings.ToLower(header))
t.Run(name, func(t *testing.T) {
t.Parallel()
remoteAddr := "10.10.10.10"
actualAddr := "12.34.56.78"
config := &httpmw.RealIPConfig{
TrustedHeaders: []string{
"Cf-Connecting-Ip",
"X-Forwarded-For",
"X-Real-Ip",
"True-Client-Ip",
},
}
switch trusted {
case "none":
config.TrustedOrigins = []*net.IPNet{}
case "ipv4":
config.TrustedOrigins = []*net.IPNet{
{
IP: net.ParseIP("10.0.0.0"),
Mask: net.CIDRMask(24, 32),
},
}
remoteAddr = "10.0.0.1"
case "ipv6":
config.TrustedOrigins = []*net.IPNet{
{
IP: net.ParseIP("2606:4700::0"),
Mask: net.CIDRMask(32, 128),
},
}
remoteAddr = "2606:4700:4700::1111"
}
middleware := httpmw.ExtractRealIP(config)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set(header, actualAddr)
req.RemoteAddr = remoteAddr
if proto == "https" {
req.TLS = &tls.ConnectionState{}
}
handlerCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// If nothing is trusted, the remoteAddr should be unchanged
if trusted == "none" {
require.Equal(t, remoteAddr, req.RemoteAddr, "remote address should be unchanged")
} else {
require.Equal(t, actualAddr, req.RemoteAddr, "actual address should be trusted")
}
handlerCalled = true
})
middleware(nextHandler).ServeHTTP(httptest.NewRecorder(), req)
require.True(t, handlerCalled, "expected handler to be invoked")
})
}
}
}
}
// TestCorruptedHeaders tests the middleware when the reverse proxy
// supplies unparsable content.
func TestCorruptedHeaders(t *testing.T) {
t.Parallel()
for _, header := range []string{"Cf-Connecting-Ip", "True-Client-Ip", "X-Real-Ip", "X-Forwarded-For"} {
header := header
name := strings.ToLower(header)
t.Run(name, func(t *testing.T) {
t.Parallel()
remoteAddr := "10.10.10.10"
config := &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("10.0.0.0"),
Mask: net.CIDRMask(8, 32),
},
},
TrustedHeaders: []string{
"Cf-Connecting-Ip",
"X-Forwarded-For",
"X-Real-Ip",
"True-Client-Ip",
},
}
middleware := httpmw.ExtractRealIP(config)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set(header, "12.34.56!78")
req.RemoteAddr = remoteAddr
handlerCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Since the header is unparsable, the remoteAddr should be unchanged
require.Equal(t, remoteAddr, req.RemoteAddr, "remote address should be unchanged")
handlerCalled = true
})
middleware(nextHandler).ServeHTTP(httptest.NewRecorder(), req)
require.True(t, handlerCalled, "expected handler to be invoked")
})
}
}
// TestAddressFamilies tests the middleware using different combinations of
// address families for remote and proxy endpoints.
func TestAddressFamilies(t *testing.T) {
t.Parallel()
for _, clientFamily := range []string{"ipv4", "ipv6"} {
for _, proxyFamily := range []string{"ipv4", "ipv6"} {
for _, header := range []string{"Cf-Connecting-Ip", "True-Client-Ip", "X-Real-Ip", "X-Forwarded-For"} {
clientFamily := clientFamily
proxyFamily := proxyFamily
header := header
name := fmt.Sprintf("%s-%s-%s", strings.ToLower(header), clientFamily, proxyFamily)
t.Run(name, func(t *testing.T) {
t.Parallel()
clientAddr := "123.123.123.123"
if clientFamily == "ipv6" {
clientAddr = "2a03:2880:f10c:83:face:b00c:0:25de"
}
proxyAddr := "4.4.4.4"
if proxyFamily == "ipv6" {
proxyAddr = "2001:4860:4860::8888"
}
config := &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
{
IP: net.ParseIP("0::"),
Mask: net.CIDRMask(0, 128),
},
},
TrustedHeaders: []string{
"Cf-Connecting-Ip",
"X-Forwarded-For",
"X-Real-Ip",
"True-Client-Ip",
},
}
middleware := httpmw.ExtractRealIP(config)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set(header, clientAddr)
req.RemoteAddr = proxyAddr
handlerCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
require.Equal(t, clientAddr, req.RemoteAddr, "remote address should match remote client")
handlerCalled = true
})
middleware(nextHandler).ServeHTTP(httptest.NewRecorder(), req)
require.True(t, handlerCalled, "expected handler to be invoked")
})
}
}
}
}
// TestFilterUntrusted tests that untrusted headers are removed from the request.
func TestFilterUntrusted(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Config *httpmw.RealIPConfig
Header http.Header
RemoteAddr string
ExpectedHeader http.Header
ExpectedRemoteAddr string
}{
{
Name: "untrusted-origin",
Config: &httpmw.RealIPConfig{
TrustedOrigins: nil,
TrustedHeaders: []string{
"Cf-Connecting-Ip",
"X-Forwarded-For",
"X-Real-Ip",
"True-Client-Ip",
},
},
Header: http.Header{
"X-Forwarded-For": []string{"1.2.3.4,123.45.67.89"},
"X-Forwarded-Proto": []string{"https"},
"X-Real-Ip": []string{"4.4.4.4"},
"True-Client-Ip": []string{"5.6.7.8"},
"Authorization": []string{"Bearer 123"},
"Accept-Encoding": []string{"gzip", "compress", "deflate", "identity"},
},
RemoteAddr: "1.2.3.4",
ExpectedHeader: http.Header{
"Authorization": []string{"Bearer 123"},
"Accept-Encoding": []string{"gzip", "compress", "deflate", "identity"},
"X-Forwarded-Proto": []string{"https"},
},
ExpectedRemoteAddr: "1.2.3.4",
},
}
for _, test := range tests {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header = test.Header
req.RemoteAddr = test.RemoteAddr
httpmw.FilterUntrustedOriginHeaders(test.Config, req)
require.Equal(t, test.ExpectedRemoteAddr, req.RemoteAddr, "remote address should match")
require.Equal(t, test.ExpectedHeader, req.Header, "filtered headers should match")
})
}
}
// TestApplicationProxy checks headers passed to DevURL services are as expected.
func TestApplicationProxy(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Config *httpmw.RealIPConfig
Header http.Header
RemoteAddr string
TLS bool
ExpectedHeader http.Header
ExpectedRemoteAddr string
}{
{
Name: "untrusted-origin-http",
Config: nil,
Header: http.Header{
"X-Forwarded-For": []string{"123.45.67.89,10.10.10.10"},
},
RemoteAddr: "17.18.19.20",
TLS: false,
ExpectedHeader: http.Header{
"X-Forwarded-For": []string{"17.18.19.20"},
"X-Forwarded-Proto": []string{"http"},
},
ExpectedRemoteAddr: "17.18.19.20",
},
{
Name: "untrusted-origin-https",
Config: nil,
Header: http.Header{
"X-Forwarded-For": []string{"123.45.67.89,10.10.10.10"},
},
RemoteAddr: "17.18.19.20",
TLS: true,
ExpectedHeader: http.Header{
"X-Forwarded-For": []string{"17.18.19.20"},
"X-Forwarded-Proto": []string{"https"},
},
ExpectedRemoteAddr: "17.18.19.20",
},
{
Name: "trusted-real-ip",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Real-Ip",
},
},
Header: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
"X-Forwarded-For": []string{"123.45.67.89,10.10.10.10"},
"X-Forwarded-Proto": []string{"https"},
},
RemoteAddr: "17.18.19.20",
TLS: true,
ExpectedHeader: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
"X-Forwarded-For": []string{"99.88.77.66,17.18.19.20"},
"X-Forwarded-Proto": []string{"https"},
},
ExpectedRemoteAddr: "99.88.77.66",
},
{
Name: "trusted-real-ip-and-forwarded-conflict",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Forwarded-For",
"X-Real-Ip",
},
},
Header: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
"X-Forwarded-For": []string{"123.45.67.89,10.10.10.10"},
"X-Forwarded-Proto": []string{"https"},
},
RemoteAddr: "17.18.19.20",
TLS: false,
ExpectedHeader: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
// Even though X-Real-Ip and X-Forwarded-For are both trusted,
// ignore the value of X-Forwarded-For, since they conflict
"X-Forwarded-For": []string{"123.45.67.89,10.10.10.10,17.18.19.20"},
"X-Forwarded-Proto": []string{"https"},
},
ExpectedRemoteAddr: "123.45.67.89",
},
{
Name: "trusted-real-ip-and-forwarded-same",
Config: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{
{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
},
TrustedHeaders: []string{
"X-Forwarded-For",
"X-Real-Ip",
},
},
Header: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
// X-Real-Ip and X-Forwarded-For are both trusted, and since
// they match, append the proxy address to X-Forwarded-For
"X-Forwarded-For": []string{"99.88.77.66,123.45.67.89,10.10.10.10"},
"X-Forwarded-Proto": []string{"https"},
},
RemoteAddr: "17.18.19.20",
TLS: false,
ExpectedHeader: http.Header{
"X-Real-Ip": []string{"99.88.77.66"},
"X-Forwarded-For": []string{"99.88.77.66,123.45.67.89,10.10.10.10,17.18.19.20"},
"X-Forwarded-Proto": []string{"https"},
},
ExpectedRemoteAddr: "99.88.77.66",
},
}
for _, test := range tests {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header = test.Header
req.RemoteAddr = test.RemoteAddr
if test.TLS {
req.TLS = &tls.ConnectionState{}
} else {
req.TLS = nil
}
middleware := httpmw.ExtractRealIP(test.Config)
handlerCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
require.Equal(t, test.ExpectedRemoteAddr, req.RemoteAddr, "remote address should match")
httpmw.FilterUntrustedOriginHeaders(test.Config, req)
err := httpmw.EnsureXForwardedForHeader(req)
require.NoError(t, err, "ensure X-Forwarded-For should be successful")
require.Equal(t, test.ExpectedHeader, req.Header, "filtered headers should match")
handlerCalled = true
})
middleware(nextHandler).ServeHTTP(httptest.NewRecorder(), req)
require.True(t, handlerCalled, "expected handler to be invoked")
})
}
}