Google Cloud HTTP(S) load balancer does not cancel connection with backend - go

I have a Google Kubernetes Engine cluster, inside several pods with NodePorts, and all is exposed via an Ingress, which creates an HTTP load balancer (LB). I am using custom domain with Google managed SSL certificate for the LB.
My backend is an HTTP server written in Go, using its "net/http" package. It is using self-signed certificate for mTLS with LB (Google's HTTP LB accepts any certificate for mTLS).
Everything works fine, except for one case, and that is when a client creates an HTTP 1.1 connection with the LB and then cancels the request. This cancels the connection between the client and the LB, but LB holds an open connection with my backend until server's timeout.
My use-case requires requests to be opened even for hours, so my server has huge timeout values. Business logic inside the request is correctly using the request's Context and takes into account if the request is canceled by the client.
Everything works as expected if the client makes an HTTP2 request and cancels it i.e. the whole connection down to my backend is canceled.
Here is an example Go handler that simulates a cancelable long-running task:
func handleLongRunningTask(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
t := time.Now()
select {
case <-ctx.Done():
log.Println("request canceled")
case <-time.After(30 * time.Second):
log.Println("request finished")
}
log.Printf("here after: %v\n", time.Since(t))
w.WriteHeader(http.StatusOK)
}
The case <-ctx.Done(): is never called for canceled HTTP 1.1 requests.
For easy testing I am using curl and Ctrl+C; this works:
curl -v --http2 'https://example.com/long_running_task'
and this does not:
curl -v --http1.1 'https://example.com/long_running_task'
It does not matter if the NodePort is HTTPS or HTTP2, the LB has exactly the same behaviour regarding requests canceled by clients.
I tried compiling the server with Go 1.14.4 and 1.13.12 and the results are the same.
Is this a bug in Kubernetes, Ingress, Google Kubernetes Engine, Google's HTTP Load Balancer, Go's HTTP server? Or is it something with HTTP 1.1 that I am missing? What can be wrong and how can I fix this?
...it is not possible to know the HTTP version in the backend, so I could reject all HTTP 1.1 requests. LB is always using the same HTTP version when communicating with its backends, no matter the client's HTTP version.

From your description it looks like the issue might be between the GFE and the backends, since GFE might hold the connections for reuse.
My take is that you're seeing this variation between protocol version because how both handle connection persistence.
For HTTP2, the connection will be open until one of the parties send a termination signal and the earliest takes preference. But for HTTP1.1, it might be prolonged until an explicit connection header is sent, specifying the termination:
An HTTP/1.1 server MAY assume that a HTTP/1.1 client intends to maintain a persistent connection unless a Connection header including the connection-token "close" was sent in the request. If the server chooses to close the connection immediately after sending the response, it SHOULD send a Connection header including the connection-token close.
This might explain why HTTP1.1 follows the same timeout configuration as the LB and HTTP2 doesn't.
I'd recommend trying to actively send termination headers whenever you want to terminate a connection. An example taken from Github:
func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("HTTP request from %s", r.RemoteAddr)
// Add this header to force to close the connection after serving the request.
w.Header().Add("Connection", "close")
fmt.Fprintf(w, "%s", m.hostname)
}
Additionally, there seem to be some success stories switching your cluster to be VPC Native, as it takes out of the equation the kube-proxy connection management.
Finally, it might be that you're in a very specific situation that is worth being evaluated separately. You might want to try to send some replication steps to the GKE team using Issue Tracker.
I hope this helps.

Related

Establishing a new TLS Server with incoming HTTP2 Requests

To comply with company guidelines of utilizing only HTTP2, how can I establish a new TLS with the underlying connection of the HTTP2 request?
I have successfully used the Hijack method on the http.ResponseWriter to retrieve the conn object from an HTTP1.1 request, and with the help of the conn object and TLS configuration, I have established a TLS server. My goal is to establish a TLS server in a similar manner, but with incoming HTTP2 requests.
Code snippet for HTTP1.1 request
hijacker, ok := w.(http.Hijacker)
if !ok {
return nil, errors.New("hijacking not supported")
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
return nil, errors.New("hijacking failed")
}
tlsConn := tls.Server(clientConn, &tlsConfig)
tlsConn.Write([]byte("hello there"))
We are trying to set up a secure server that can handle different certificates based on the hostname. To do this, Service A sends information about the certificate to Service B through HTTP 1.1. Service B uses the certificate information to create a new secure server (TLS). However, if the incoming request is using HTTP2, this process is not possible as HTTP2 does not support retrieving the underlying connection.
The reason for this setup is to allow Service A, which acts as a proxy, to communicate with Service B in a secure manner. The client's original request to Service A may not trust Service B's certificate, so Service A sends the host information to Service B first, allowing B to create a secure server using the proper certificate. Service A then forwards the client's request to this new secure server which is still using the same underlying connection.

How to make the client of proxy server keep alive?

I want to make the client of proxy server keepAlive. Thus, I don't want the proxy client to make a tcp close handshake everytime.
Please look at this example in netty.
Adding the keepAlive option to this example doesn't seem to work properly. Because it makes a client and connect everytime the server get request and close the client when the response is arrived.
Then how can I make my proxy client keepAlive? Is there any reference/example for it?
Using SO_KEEPALIVE socket option doesn't mean that the server (or the other peer in the connection) should ignore an explicit request to close the connection. It helps in cases like
Idle sessions timing-out/getting killed by the other end due to non-activity
Idle or long-running requests being disconnected by a firewall in-between after a certain time passes (e.g. 1 hour, for resource clean-up purposes).
If the client's logic is not to re-use the same socket for different requests (i.e. if its application logic uses a new socket for each request), there's nothing you can do about that on your proxy.
The same argument is valid for the "back-end" side of your proxy as well. If the server you're proxying to doesn't allow the socket to be re-used, and explicitly closes a connection after a request is served, that wouldn't work as you wanted either.
If you are not closing the connection on your side then the proxy is. Different proxy servers will behave in different ways.
Try sending Connection: Keep-Alive as a header.
If that doesn't work, try also sending Proxy-Connection: Keep-Alive as a header.

how to make golang grpc clients follow redirects

I have a server that redirects to server:443 when connecting to server:80. I have a grpc client that is connecting to server:80 with
clientConn, err = grpc.Dial("server:80", grpc.WithTransportCredentials(credentials.NewTLS(config)))
Its throwing a "tls: first record does not look like a TLS handshake" error. Is there a way to make the client follow the redirects?
gRPC client does not handle 302 redirects. See this https://github.com/grpc/grpc-java/issues/5330 - this is for Java but also applies to Golang.

How is a proxy connection made with URLSession?

I've been looking at Apple's WWWDC 2015 Session 711: "Networking with NSURLSession". Towards the end, the speaker mentions URLSessionStreamTask, which can be used for direct socket I/O. And he mentions how a (HTTP) proxy connection can be transitioned to a stream-task.
A slide contains:
NSURLSessionStreamTask
DataTask conversion
NSURLSessionDataTask may be converted to a stream task
• Use NSURLSession to get through HTTP proxies
Conversion can occur when response is received
And the next slide has partial sample code:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,
didReceiveResponse response: NSURLResponse,
completionHandler: (NSURLSessionResponseDisposition) -> Void) {
completionHandler(.BecomeStream)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,
didBecomeStreamTask streamTask: NSURLSessionStreamTask) {
}
I want to know how to create the proxy connection in the first place. And using the data from the "System Preferences" : Network : Advanced : Proxies panel if possible. (Not just the "HTTP" proxy, but any of the other 4 with the same format.)
And since they usually use HTTP instead of HTTPS, do such connections trigger ATS (App Transport Security)?
What they're talking about is support for the WebSocket protocol, which allows you to open a connection to a web server, then upgrade that connection to a web socket, thus allowing you to communicate directly with whatever script or CGI program is on the other end without having to conform to the traditional request-response style.
What I think they were trying to say was that if you are sending HTTP (ick) requests via a proxy that actually supports WebSocket communications, iOS knows how to ask the proxies to upgrade the connection properly. This same upgrade mechanism is often used for making an HTTPS request through an HTTP proxy, where supported, and if you make an HTTPS connection, the subsequent WebSockets upgrade should work in spite of the proxy no matter what, because the proxy is already just passing bits back and forth by that point.

Http GET requests over one tls connection using Go

Currently, I am using http GET requests to connect to a server.
Get request is calling from inside a thread. For each Get request one thread is being used but problem is for each Get request, one connection is established. Thus if there are 10000 Get request then 10000 connections will be established. However, I want first to establishe a TLS connection between me and the server, then create a thread and from that thread I want to send Get over that already established connection.
e.g.
for {
1. establish a tls connection
2. create thread go func()
}
func() {
resp, err := http.Get(url) // should be over already established tls connection
}
Firstly, just to clarify, Go has goroutines, not threads. Goroutines are co-operative light-weight processes. The Go runtime time-slices a number of goroutines onto the available operating system threads. This thread abstraction is an important feature of Go.
On the question of making many concurrent GET requests using goroutines, it is best initially to let the standard API handle multiplexing of requests onto connections for you. There may be a large number of requests and a smaller number of supporting connections. Provided you use keep-alive connections, you should not need to care about the details.
For control over proxies, TLS configuration, keep-alives, compression, and other settings, create a Transport
HTTP/1.1 handles keep-alive connections with both http and https. This is a benefit when many requests are made to the same server. As long as you don't force it to close each connection after each request, you will get a lot of benefit from keep-alive connections in your case.
The usual mantra applies: don't optimise prematurely. Describe what you need to do as clearly as possible. Benchmark it (Go has a useful micro-benchmark tool). Do this before you decide whether or how to optimise the performance.

Resources