Skip to content

Commit

Permalink
Merge pull request #265 from cloudfoundry-incubator/add-backend_match…
Browse files Browse the repository at this point in the history
…_http_protocol-property

add new property backend_match_http_protocol
  • Loading branch information
peterellisjones authored Oct 28, 2021
2 parents d0a1ad5 + 04b2424 commit a0e1b6c
Show file tree
Hide file tree
Showing 14 changed files with 573 additions and 174 deletions.
2 changes: 1 addition & 1 deletion acceptance-tests/acceptance_tests_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func startDefaultTestServer() (func(), int) {
var upgrader = websocket.Upgrader{}

By("Starting a local websocket server to act as a backend")
closeLocalServer, localPort, err := startLocalHTTPServer(func(w http.ResponseWriter, r *http.Request) {
closeLocalServer, localPort, err := startLocalHTTPServer(nil, func(w http.ResponseWriter, r *http.Request) {
// if no upgrade requested, act like a normal HTTP server
if strings.ToLower(r.Header.Get("Upgrade")) != "websocket" {
fmt.Fprintln(w, "Hello cloud foundry")
Expand Down
162 changes: 162 additions & 0 deletions acceptance-tests/backend_match_http_protocol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package acceptance_tests

import (
"crypto/tls"
"fmt"
"net/http"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Backend match HTTP protocol", func() {
var haproxyInfo haproxyInfo
var closeTunnel func()
var closeLocalServer func()
var http1Client *http.Client
var http2Client *http.Client

haproxyBackendPort := 12000
opsfileHTTPS := `---
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/backend_ssl?
value: verify
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/backend_ca_file?
value: ((https_backend.ca))
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/backend_match_http_protocol?
value: true
# Configure CA and cert chain
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/crt_list?/-
value:
snifilter:
- haproxy.internal
ssl_pem:
cert_chain: ((https_frontend.certificate))((default_ca.certificate))
private_key: ((https_frontend.private_key))
alpn: ['h2', 'http/1.1']
# Declare certs
- type: replace
path: /variables?/-
value:
name: default_ca
type: certificate
options:
is_ca: true
common_name: bosh
- type: replace
path: /variables?/-
value:
name: https_frontend
type: certificate
options:
ca: default_ca
common_name: haproxy.internal
alternative_names: [haproxy.internal]
- type: replace
path: /variables?/-
value:
name: https_backend
type: certificate
options:
ca: default_ca
common_name: 127.0.0.1
alternative_names: [127.0.0.1]
`

var creds struct {
HTTPSFrontend struct {
Certificate string `yaml:"certificate"`
PrivateKey string `yaml:"private_key"`
CA string `yaml:"ca"`
} `yaml:"https_frontend"`
HTTPSBackend struct {
Certificate string `yaml:"certificate"`
PrivateKey string `yaml:"private_key"`
CA string `yaml:"ca"`
} `yaml:"https_backend"`
}

JustBeforeEach(func() {
var varsStoreReader varsStoreReader
haproxyInfo, varsStoreReader = deployHAProxy(baseManifestVars{
haproxyBackendPort: haproxyBackendPort,
haproxyBackendServers: []string{"127.0.0.1"},
deploymentName: defaultDeploymentName,
}, []string{opsfileHTTPS}, map[string]interface{}{}, true)

err := varsStoreReader(&creds)
Expect(err).NotTo(HaveOccurred())

// Build backend server that supports HTTP2 and HTTP1.1
backendTLSCert, err := tls.X509KeyPair([]byte(creds.HTTPSBackend.Certificate), []byte(creds.HTTPSBackend.PrivateKey))
Expect(err).NotTo(HaveOccurred())

backendTLSConfig := &tls.Config{
Certificates: []tls.Certificate{backendTLSCert},
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
NextProtos: []string{"h2", "http/1.1"},
}

var localPort int
closeLocalServer, localPort, err = startLocalHTTPServer(backendTLSConfig, func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Backend server handling incoming request")
protocolHeaderValue := "none"
if r.TLS != nil {
protocolHeaderValue = r.TLS.NegotiatedProtocol
}
w.Header().Add("X-BACKEND-ALPN-PROTOCOL", protocolHeaderValue)
w.Header().Add("X-BACKEND-PROTO", r.Proto)
_, _ = w.Write([]byte("OK"))
})
Expect(err).NotTo(HaveOccurred())
closeTunnel = setupTunnelFromHaproxyToTestServer(haproxyInfo, haproxyBackendPort, localPort)

addresses := map[string]string{
"haproxy.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP),
}

http1Client = buildHTTPClient([]string{creds.HTTPSFrontend.CA}, addresses, []tls.Certificate{}, "")
http2Client = buildHTTP2Client([]string{creds.HTTPSFrontend.CA}, addresses, []tls.Certificate{})
})

Context("When backend_match_http_protocol is true", func() {
It("uses the same backend protocol as was used for the frontend connection", func() {
resp, err := http1Client.Get("https://haproxy.internal:443")
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(200))

// Frontend request HTTP1.1
Expect(resp.Proto).To(Equal("HTTP/1.1"))
Expect(resp.TLS.NegotiatedProtocol).To(Equal(""))

// Backend request HTTP1.1
Expect(resp.Header.Get("X-BACKEND-PROTO")).To((Equal("HTTP/1.1")))
Expect(resp.Header.Get("X-BACKEND-ALPN-PROTOCOL")).To((Equal("http/1.1")))

resp, err = http2Client.Get("https://haproxy.internal:443")
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(200))

// Frontend request HTTP2
Expect(resp.Proto).To(Equal("HTTP/2.0"))
Expect(resp.TLS.NegotiatedProtocol).To(Equal("h2"))

// Backend request HTTP2
Expect(resp.Header.Get("X-BACKEND-PROTO")).To((Equal("HTTP/2.0")))
Expect(resp.Header.Get("X-BACKEND-ALPN-PROTOCOL")).To(Equal("h2"))
})
})

AfterEach(func() {
if closeLocalServer != nil {
defer closeLocalServer()
}
if closeTunnel != nil {
defer closeTunnel()
}
})
})
12 changes: 10 additions & 2 deletions acceptance-tests/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import (

// starts a local http server handling the provided handler
// returns a close function to stop the server and the port the server is listening on
func startLocalHTTPServer(handler func(http.ResponseWriter, *http.Request)) (func(), int, error) {
server := httptest.NewServer(http.HandlerFunc(handler))
func startLocalHTTPServer(tlsConfig *tls.Config, handler func(http.ResponseWriter, *http.Request)) (func(), int, error) {
server := httptest.NewUnstartedServer(http.HandlerFunc(handler))
if tlsConfig != nil {
server.TLS = tlsConfig
server.StartTLS()
} else {
server.Start()
}

serverURL, err := url.Parse(server.URL)
if err != nil {
return nil, 0, err
Expand Down Expand Up @@ -78,6 +85,7 @@ func buildHTTP2Client(caCerts []string, addressMap map[string]string, clientCert

httpClient := buildHTTPClient(caCerts, addressMap, clientCerts, "")
transport := httpClient.Transport.(*http.Transport)

http2.ConfigureTransport(transport)

// force HTTP2-only
Expand Down
6 changes: 6 additions & 0 deletions acceptance-tests/https_frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ var _ = Describe("HTTPS Frontend", func() {
})

It("Allows clients to use HTTP2 as well as HTTP1.1", func() {
By("Negotiating the correct ALPN protocol")
// H2 endpoint should negotiate H2 if the client supports it
alpnProto, err := connectTLSALPNNegotiatedProtocol([]string{"http/1.1", "h2"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.internal")
Expect(err).NotTo(HaveOccurred())
Expect(alpnProto).To(Equal("h2"))

By("Sending a request to HAProxy using HTTP 1.1")
resp, err := http1Client.Get("https://haproxy.internal:443")
Expect(err).NotTo(HaveOccurred())
Expand Down
2 changes: 1 addition & 1 deletion acceptance-tests/xfcc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ var _ = Describe("forwarded_client_cert", func() {

By("Starting a local http server to act as a backend")
var localPort int
closeLocalServer, localPort, err = startLocalHTTPServer(func(w http.ResponseWriter, r *http.Request) {
closeLocalServer, localPort, err = startLocalHTTPServer(nil, func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Backend server handling incoming request")
recordedHeaders = r.Header
_, _ = w.Write([]byte("OK"))
Expand Down
6 changes: 6 additions & 0 deletions ci/release_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# New Features
- `ha_proxy.backend_match_http_protocol` This causes HAProxy to use the same HTTP protocol for backend connections that was used for frontend connections. Note that this property ignores the value of ha_proxy.enable_http2, and requires that ha_proxy.backend_ssl is not off for HTTP2 support

# Acknowledgements

Thanks @peterellisjones, @Rob-rls, @Mrizwanshaik for the PR
11 changes: 7 additions & 4 deletions jobs/haproxy/spec
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ properties:
ssl_ciphersuites: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl_min_version: TLSv1.2
ssl_max_version: TLSv1.3
alpn:
alpn:
- h2
- http/1.1
client_revocation_list: |
Expand Down Expand Up @@ -248,9 +248,12 @@ properties:
ha_proxy.disable_tls_13:
default: false
description: "Disable TLS 1.3 in HA Proxy"
ha_proxy.backend_match_http_protocol:
default: false
description: Uses the same version of HTTP for backend connections that was used for frontend connections (ie HTTP 1.1 or HTTP 2). Ignores the value of enable_http2. HTTP2 backend connections require that `ha_proxy.backend_ssl` is not `off`.
ha_proxy.disable_backend_http2_websockets:
default: false
description: "Forward websockets to the backend servers using HTTP/1.1, never HTTP/2. Does not apply to custom routed_backend_servers. Works around https://github.com/cloudfoundry/routing-release/issues/230"
description: "Forward websockets to the backend servers using HTTP/1.1, never HTTP/2. Does not apply to custom routed_backend_servers. Works around https://github.com/cloudfoundry/routing-release/issues/230. Overrides backend_match_http_protocol for websockets."

ha_proxy.connect_timeout:
description: "Timeout (in floating point seconds) used on connections from haproxy to a backend, while waiting for the TCP handshake to complete + connection to establish"
Expand Down Expand Up @@ -302,7 +305,7 @@ properties:
description: "Array of the router IPs acting as the HTTP/TCP backends (should include servers all Availability Zones being used)"
default: []
ha_proxy.backend_ssl:
description: "Optionally enable SSL verification for backend servers, one of `verify`, `noverify`, any other value assumes no ssl backend. Setting `verify` requires `ha_proxy.backend_ca_file` key to be set."
description: "Optionally enable SSL verification for backend servers, one of `verify`, `noverify`, any other value assumes no ssl backend. Setting `verify` requires `ha_proxy.backend_ca_file` key to be set. Note that `off` will disable all backend HTTP2 support regardless of other properties."
default: "off"
ha_proxy.backend_ssl_verifyhost:
description: "Optional hostname to verify in the x509 certificate subject for SSL-enabled backend servers. Requires `ha_proxy.backend_ssl` is set to `verify` when using this."
Expand Down Expand Up @@ -663,5 +666,5 @@ properties:
Prefer backend servers which are located on the same availability zone. Note that this only affects servers provided via the http_backend link property. Servers provided via the tcp backend_link will automatically prefer the local AZ.
default: false
ha_proxy.enable_http2:
description: Enables ingress and egress HTTP/2 ALPN negotiation
description: Enables ingress (frontend) and egress (backend) HTTP/2 ALPN negotiation. Egress (backend) HTTP protocol version may be overriden by `ha_proxy.backend_ssl`, `ha_proxy.disable_backend_http2_websockets` and `ha_proxy.backend_match_http_protocol`.
default: false
Loading

0 comments on commit a0e1b6c

Please sign in to comment.