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

Append X-Forwarded-For in proxy handler #46126

Merged
merged 1 commit into from
May 24, 2017
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
15 changes: 15 additions & 0 deletions staging/src/k8s.io/apimachinery/pkg/util/net/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ func GetClientIP(req *http.Request) net.IP {
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)

// isDefault checks to see if the transportProxierFunc is pointing to the default one
Expand Down
26 changes: 26 additions & 0 deletions staging/src/k8s.io/apimachinery/pkg/util/net/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
testCases := []struct {
name string
Expand Down
20 changes: 9 additions & 11 deletions staging/src/k8s.io/apiserver/pkg/endpoints/handlers/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package handlers

import (
"context"
"errors"
"io"
"math/rand"
Expand Down Expand Up @@ -156,17 +157,10 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
location.RawQuery = values.Encode()

newReq, err := http.NewRequest(req.Method, location.String(), req.Body)
if err != nil {
httpCode = responsewriters.ErrorNegotiated(ctx, err, r.Serializer, gv, w, req)
return
}
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
// WithContext creates a shallow clone of the request with the new context.
newReq := req.WithContext(context.Background())
newReq.Header = net.CloneHeader(req.Header)
newReq.URL = location

// TODO convert this entire proxy to an UpgradeAwareProxy similar to
// https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go.
Expand Down Expand Up @@ -224,6 +218,10 @@ func (r *ProxyHandler) tryUpgrade(ctx request.Context, w http.ResponseWriter, re
if !httpstream.IsUpgradeRequest(req) {
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)
if err != nil {
responsewriters.ErrorNegotiated(ctx, err, r.Serializer, gv, w, req)
Expand Down
22 changes: 10 additions & 12 deletions staging/src/k8s.io/apiserver/pkg/registry/generic/rest/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package rest

import (
"context"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -116,16 +117,10 @@ func (h *UpgradeAwareProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Re
h.Transport = h.defaultProxyTransport(req.URL, h.Transport)
}

newReq, err := http.NewRequest(req.Method, loc.String(), req.Body)
if err != nil {
h.Responder.Error(err)
return
}
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
// WithContext creates a shallow clone of the request with the new context.
newReq := req.WithContext(context.Background())
newReq.Header = utilnet.CloneHeader(req.Header)
newReq.URL = &loc

proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host})
proxy.Transport = h.Transport
Expand All @@ -145,10 +140,13 @@ func (h *UpgradeAwareProxyHandler) tryUpgrade(w http.ResponseWriter, req *http.R
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) {
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 {
clone := utilnet.CloneRequest(req)
clone.URL = h.Location
backendConn, err = h.Dial(clone)
}
Expand Down
1 change: 1 addition & 0 deletions staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ go_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/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/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
Expand Down
27 changes: 7 additions & 20 deletions staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ limitations under the License.
package apiserver

import (
"context"
"net/http"
"net/url"
"sync/atomic"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericfeatures "k8s.io/apiserver/pkg/features"
Expand Down Expand Up @@ -110,21 +112,14 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
location.Path = req.URL.Path
location.RawQuery = req.URL.Query().Encode()

// make a new request object with the updated location and the body we already have
newReq, err := http.NewRequest(req.Method, location.String(), req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
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
// WithContext creates a shallow clone of the request with the new context.
newReq := req.WithContext(context.Background())
newReq.Header = utilnet.CloneHeader(req.Header)
newReq.URL = location

upgrade := false
// 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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -163,14 +158,6 @@ func maybeWrapForConnectionUpgrades(restConfig *restclient.Config, rt http.Round
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.
type responder struct {
w http.ResponseWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ func TestProxyHandler(t *testing.T) {
expectedHeaders: map[string][]string{
"X-Forwarded-Proto": {"https"},
"X-Forwarded-Uri": {"/request/path"},
"X-Forwarded-For": {"127.0.0.1"},
"X-Remote-User": {"username"},
"User-Agent": {"Go-http-client/1.1"},
"Accept-Encoding": {"gzip"},
Expand Down