Merge pull request #46126 from timstclair/forward-ip

Automatic merge from submit-queue (batch tested with PRs 42042, 46139, 46126, 46258, 46312)

Append X-Forwarded-For in proxy handler

Append the request sender's IP to the `X-Forwarded-For` header chain when proxying requests. This is important for audit logging (https://github.com/kubernetes/features/issues/22) in order to capture the client IP (specifically in the case of federation or kube-aggregator).

/cc @liggitt @deads2k @ericchiang @ihmccreery @soltysh
pull/6/head
Kubernetes Submit Queue 2017-05-23 19:43:01 -07:00 committed by GitHub
commit 2b1b7f92ce
7 changed files with 69 additions and 43 deletions

View File

@ -209,6 +209,21 @@ func GetClientIP(req *http.Request) net.IP {
return ips[0] return ips[0]
} }
// Prepares the X-Forwarded-For header for another forwarding hop by appending the previous sender's
// IP address to the X-Forwarded-For chain.
func AppendForwardedForHeader(req *http.Request) {
// Copied from net/http/httputil/reverseproxy.go:
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := req.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
}
}
var defaultProxyFuncPointer = fmt.Sprintf("%p", http.ProxyFromEnvironment) var defaultProxyFuncPointer = fmt.Sprintf("%p", http.ProxyFromEnvironment)
// isDefault checks to see if the transportProxierFunc is pointing to the default one // isDefault checks to see if the transportProxierFunc is pointing to the default one

View File

@ -108,6 +108,32 @@ func TestGetClientIP(t *testing.T) {
} }
} }
func TestAppendForwardedForHeader(t *testing.T) {
testCases := []struct {
addr, forwarded, expected string
}{
{"1.2.3.4:8000", "", "1.2.3.4"},
{"1.2.3.4:8000", "8.8.8.8", "8.8.8.8, 1.2.3.4"},
{"1.2.3.4:8000", "8.8.8.8, 1.2.3.4", "8.8.8.8, 1.2.3.4, 1.2.3.4"},
{"1.2.3.4:8000", "foo,bar", "foo,bar, 1.2.3.4"},
}
for i, test := range testCases {
req := &http.Request{
RemoteAddr: test.addr,
Header: make(http.Header),
}
if test.forwarded != "" {
req.Header.Set("X-Forwarded-For", test.forwarded)
}
AppendForwardedForHeader(req)
actual := req.Header.Get("X-Forwarded-For")
if actual != test.expected {
t.Errorf("[%d] Expected %q, Got %q", i, test.expected, actual)
}
}
}
func TestProxierWithNoProxyCIDR(t *testing.T) { func TestProxierWithNoProxyCIDR(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string

View File

@ -17,6 +17,7 @@ limitations under the License.
package handlers package handlers
import ( import (
"context"
"errors" "errors"
"io" "io"
"math/rand" "math/rand"
@ -156,17 +157,10 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
location.RawQuery = values.Encode() location.RawQuery = values.Encode()
newReq, err := http.NewRequest(req.Method, location.String(), req.Body) // WithContext creates a shallow clone of the request with the new context.
if err != nil { newReq := req.WithContext(context.Background())
httpCode = responsewriters.ErrorNegotiated(ctx, err, r.Serializer, gv, w, req) newReq.Header = net.CloneHeader(req.Header)
return newReq.URL = location
}
httpCode = http.StatusOK
newReq.Header = req.Header
newReq.ContentLength = req.ContentLength
// Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and
// it can determine the TransferEncoding based on ContentLength and the Body.
newReq.TransferEncoding = req.TransferEncoding
// TODO convert this entire proxy to an UpgradeAwareProxy similar to // TODO convert this entire proxy to an UpgradeAwareProxy similar to
// https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go. // https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go.
@ -224,6 +218,10 @@ func (r *ProxyHandler) tryUpgrade(ctx request.Context, w http.ResponseWriter, re
if !httpstream.IsUpgradeRequest(req) { if !httpstream.IsUpgradeRequest(req) {
return false return false
} }
// Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy
// handles this in the non-upgrade path.
net.AppendForwardedForHeader(newReq)
backendConn, err := proxyutil.DialURL(location, transport) backendConn, err := proxyutil.DialURL(location, transport)
if err != nil { if err != nil {
responsewriters.ErrorNegotiated(ctx, err, r.Serializer, gv, w, req) responsewriters.ErrorNegotiated(ctx, err, r.Serializer, gv, w, req)

View File

@ -17,6 +17,7 @@ limitations under the License.
package rest package rest
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -116,16 +117,10 @@ func (h *UpgradeAwareProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Re
h.Transport = h.defaultProxyTransport(req.URL, h.Transport) h.Transport = h.defaultProxyTransport(req.URL, h.Transport)
} }
newReq, err := http.NewRequest(req.Method, loc.String(), req.Body) // WithContext creates a shallow clone of the request with the new context.
if err != nil { newReq := req.WithContext(context.Background())
h.Responder.Error(err) newReq.Header = utilnet.CloneHeader(req.Header)
return newReq.URL = &loc
}
newReq.Header = req.Header
newReq.ContentLength = req.ContentLength
// Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and
// it can determine the TransferEncoding based on ContentLength and the Body.
newReq.TransferEncoding = req.TransferEncoding
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host}) proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host})
proxy.Transport = h.Transport proxy.Transport = h.Transport
@ -145,10 +140,13 @@ func (h *UpgradeAwareProxyHandler) tryUpgrade(w http.ResponseWriter, req *http.R
err error err error
) )
clone := utilnet.CloneRequest(req)
// Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy
// handles this in the non-upgrade path.
utilnet.AppendForwardedForHeader(clone)
if h.InterceptRedirects && utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects) { if h.InterceptRedirects && utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StreamingProxyRedirects) {
backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, h.Location, req.Header, req.Body, h) backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, h.Location, clone.Header, req.Body, h)
} else { } else {
clone := utilnet.CloneRequest(req)
clone.URL = h.Location clone.URL = h.Location
backendConn, err = h.Dial(clone) backendConn, err = h.Dial(clone)
} }

View File

@ -54,6 +54,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",

View File

@ -17,6 +17,7 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"context"
"net/http" "net/http"
"net/url" "net/url"
"sync/atomic" "sync/atomic"
@ -24,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy" "k8s.io/apimachinery/pkg/util/httpstream/spdy"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features" genericfeatures "k8s.io/apiserver/pkg/features"
@ -110,21 +112,14 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
location.Path = req.URL.Path location.Path = req.URL.Path
location.RawQuery = req.URL.Query().Encode() location.RawQuery = req.URL.Query().Encode()
// make a new request object with the updated location and the body we already have // WithContext creates a shallow clone of the request with the new context.
newReq, err := http.NewRequest(req.Method, location.String(), req.Body) newReq := req.WithContext(context.Background())
if err != nil { newReq.Header = utilnet.CloneHeader(req.Header)
http.Error(w, err.Error(), http.StatusInternalServerError) newReq.URL = location
return
}
mergeHeader(newReq.Header, req.Header)
newReq.ContentLength = req.ContentLength
// Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and
// it can determine the TransferEncoding based on ContentLength and the Body.
newReq.TransferEncoding = req.TransferEncoding
upgrade := false upgrade := false
// we need to wrap the roundtripper in another roundtripper which will apply the front proxy headers // we need to wrap the roundtripper in another roundtripper which will apply the front proxy headers
proxyRoundTripper, upgrade, err = maybeWrapForConnectionUpgrades(handlingInfo.restConfig, proxyRoundTripper, req) proxyRoundTripper, upgrade, err := maybeWrapForConnectionUpgrades(handlingInfo.restConfig, proxyRoundTripper, req)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -163,14 +158,6 @@ func maybeWrapForConnectionUpgrades(restConfig *restclient.Config, rt http.Round
return wrappedRT, true, nil return wrappedRT, true, nil
} }
func mergeHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
// responder implements rest.Responder for assisting a connector in writing objects or errors. // responder implements rest.Responder for assisting a connector in writing objects or errors.
type responder struct { type responder struct {
w http.ResponseWriter w http.ResponseWriter

View File

@ -128,6 +128,7 @@ func TestProxyHandler(t *testing.T) {
expectedHeaders: map[string][]string{ expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"}, "X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"}, "X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"}, "X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"}, "User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"}, "Accept-Encoding": {"gzip"},