registry.k8s.io/pkg/net/clientip/clientip.go

80 lines
2.9 KiB
Go

/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientip
import (
"fmt"
"net"
"net/http"
"net/netip"
"strings"
)
// Get gets the client IP for an http.Request
//
// NOTE: currently only two scenarios are supported:
// 1. no loadbalancer, local testing
// 2. behind Google Cloud LoadBalancer (as in cloudrun)
//
// Note that in particular we do not support hitting the CloudRun endpoint
// directly (though we could easily do so here). Cloud Armor is on the GCLB,
// so directly accessing the CloudRun endpoint would bypass that.
//
// At this time we have no need to complicate it further.
func Get(r *http.Request) (netip.Addr, error) {
// Upstream docs:
// https://cloud.google.com/load-balancing/docs/https#x-forwarded-for_header
//
// If there is no X-Forwarded-For header on the incoming request,
// these two IP addresses are the entire header value:
// X-Forwarded-For: <client-ip>,<load-balancer-ip>
//
// If the request includes an X-Forwarded-For header, the load balancer
// preserves the supplied value before the <client-ip>,<load-balancer-ip>:
// X-Forwarded-For: [<supplied-value>,]<client-ip>,<load-balancer-ip>
//
// Caution: The load balancer does not verify any IP addresses that
// precede <client-ip>,<load-balancer-ip> in this header.
// The preceding IP addresses might contain other characters, including spaces.
rawXFwdFor := r.Header.Get("X-Forwarded-For")
// clearly we are not in cloud if this header is not set, we can use
// r.RemoteAddr in that case to support local testing
// Go http server will always set this value for us
if rawXFwdFor == "" {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return netip.Addr{}, err
}
return netip.ParseAddr(host)
}
// assume we are in cloud run, get <client-ip> from load balancer header
// local tests with direct connection to the server can also fake this
// header which is fine + useful
//
// we want the contents between the second to last comma and the last comma
// or if only one comma between the start of the string and the last comma
keys := strings.FieldsFunc(rawXFwdFor, func(r rune) bool {
return r == ',' || r == ' '
})
// there should be at least two values: <client-ip>,<load-balancer-ip>
if len(keys) < 2 {
return netip.Addr{}, fmt.Errorf("invalid X-Forwarded-For value: %s", rawXFwdFor)
}
// normal case, we expect the client-ip to be 2 from the end
return netip.ParseAddr(keys[len(keys)-2])
}