From c9061d55ed415baa5cccff3ef0a798836fc04e59 Mon Sep 17 00:00:00 2001 From: Peter Jones Date: Wed, 27 Oct 2021 09:21:56 +0100 Subject: [PATCH 1/3] improve enable_http acceptance tests --- acceptance-tests/https_frontend_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acceptance-tests/https_frontend_test.go b/acceptance-tests/https_frontend_test.go index fed269f7..fee107d8 100644 --- a/acceptance-tests/https_frontend_test.go +++ b/acceptance-tests/https_frontend_test.go @@ -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()) From 4ee3a0a5603bab41ca5cf76a939fac94e4253705 Mon Sep 17 00:00:00 2001 From: Peter Jones Date: Tue, 26 Oct 2021 17:05:19 +0100 Subject: [PATCH 2/3] add new property backend_match_http_protocol --- .../acceptance_tests_suite_test.go | 2 +- .../backend_match_http_protocol_test.go | 162 ++++++++++++++ acceptance-tests/http.go | 12 +- acceptance-tests/xfcc_test.go | 2 +- jobs/haproxy/spec | 11 +- jobs/haproxy/templates/haproxy.config.erb | 132 +++++++----- .../haproxy_config/backend_http_spec.rb | 198 +++++++++++------- .../haproxy_config/backend_wss_spec.rb | 31 +-- .../haproxy_config/frontend_http_spec.rb | 35 +++- .../haproxy_config/frontend_https_spec.rb | 63 +++++- .../haproxy_config/frontend_wss_spec.rb | 63 +++++- .../healthcheck_listener_spec.rb | 24 ++- 12 files changed, 561 insertions(+), 174 deletions(-) create mode 100644 acceptance-tests/backend_match_http_protocol_test.go diff --git a/acceptance-tests/acceptance_tests_suite_test.go b/acceptance-tests/acceptance_tests_suite_test.go index e22c7957..c92e830c 100644 --- a/acceptance-tests/acceptance_tests_suite_test.go +++ b/acceptance-tests/acceptance_tests_suite_test.go @@ -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") diff --git a/acceptance-tests/backend_match_http_protocol_test.go b/acceptance-tests/backend_match_http_protocol_test.go new file mode 100644 index 00000000..7f084769 --- /dev/null +++ b/acceptance-tests/backend_match_http_protocol_test.go @@ -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() + } + }) +}) diff --git a/acceptance-tests/http.go b/acceptance-tests/http.go index 750d23ea..58354637 100644 --- a/acceptance-tests/http.go +++ b/acceptance-tests/http.go @@ -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 @@ -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 diff --git a/acceptance-tests/xfcc_test.go b/acceptance-tests/xfcc_test.go index 21ff6975..ac2ad989 100644 --- a/acceptance-tests/xfcc_test.go +++ b/acceptance-tests/xfcc_test.go @@ -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")) diff --git a/jobs/haproxy/spec b/jobs/haproxy/spec index 4f1ac050..4f097bd7 100644 --- a/jobs/haproxy/spec +++ b/jobs/haproxy/spec @@ -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: | @@ -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" @@ -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." @@ -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 diff --git a/jobs/haproxy/templates/haproxy.config.erb b/jobs/haproxy/templates/haproxy.config.erb index ec60e47f..d9753170 100644 --- a/jobs/haproxy/templates/haproxy.config.erb +++ b/jobs/haproxy/templates/haproxy.config.erb @@ -131,9 +131,9 @@ end # }}} # ALPN Option {{{ -alpn_config = "" +default_alpn_config = "" if p("ha_proxy.enable_http2") - alpn_config = "alpn h2,http/1.1 " + default_alpn_config = "alpn h2,http/1.1 " end # }}} @@ -170,6 +170,53 @@ end if p("ha_proxy.enable_4443") && !ssl_enabled abort "Conflicting configuration: if enable_4443 is true, you must provide a valid SSL config via ssl_pem or crt_list" end + + backend_servers = [] + backend_servers_local = [] + backend_port = nil + if_link("http_backend") do |backend| + backend_servers = backend.instances.map(&:address) + backend_port = backend.p("port", p("ha_proxy.backend_port")) + + if p("ha_proxy.backend_prefer_local_az") + backend_servers_local = backend.instances.select{ |n| n.az == spec.az }.map(&:address) + end + end.else_if_p("ha_proxy.backend_servers") do |servers| + backend_servers = servers + backend_port = p("ha_proxy.backend_port") + end + resolvers = "" + if_p("ha_proxy.resolvers") do + resolvers = "resolvers default " + end + backend_crt = "" + if_p("ha_proxy.backend_crt") do + backend_crt = "crt /var/vcap/jobs/haproxy/config/backend-crt.pem " + end + + backend_ssl = "" + if p("ha_proxy.backend_ssl").downcase == "verify" + backend_ssl = "ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem " + if_p("ha_proxy.backend_ssl_verifyhost") do | verify_hostname | + backend_ssl += "verifyhost #{verify_hostname} " + end + elsif p("ha_proxy.backend_ssl").downcase == "noverify" + backend_ssl = "ssl verify none " + end + + backends = [] + disable_backend_http2_websockets = p("ha_proxy.disable_backend_http2_websockets") + enable_http2 = p("ha_proxy.enable_http2") + backend_match_http_protocol = p("ha_proxy.backend_match_http_protocol") + + if disable_backend_http2_websockets || (!enable_http2) || backend_match_http_protocol || backend_ssl == "" + alpn = backend_ssl != "" ? "alpn http/1.1 " : "" + backends += [{ name: "http-routers-http1", backend_ssl: backend_ssl, alpn: alpn }] + end + + if backend_ssl != "" && (enable_http2 || backend_match_http_protocol) + backends += [{ name: "http-routers-http2", backend_ssl: backend_ssl, alpn: "alpn h2,http/1.1 " }] + end -%> global @@ -223,6 +270,9 @@ global <%- if_p("ha_proxy.ssl_ciphersuites") do -%> ssl-default-server-ciphersuites <%= p("ha_proxy.ssl_ciphersuites") %> <%- end -%> + <%- if backend_match_http_protocol && backends.length == 2 -%> + set-var proc.h2_alpn_tag str(h2) + <%- end -%> defaults log global @@ -270,7 +320,7 @@ listen health_check_http_url bind :<%= p("ha_proxy.health_check_port") %> mode http monitor-uri /health - acl http-routers_down nbsrv(http-routers) eq 0 + acl http-routers_down nbsrv(<%= backends.first[:name] %>) eq 0 monitor fail if http-routers_down <% end -%> @@ -307,7 +357,7 @@ frontend http-in tcp-request content reject <%- end -%> capture request header Host len 256 - default_backend http-routers + default_backend <%= backends.last[:name] %> <%- if_p("ha_proxy.http_request_deny_conditions") do |conditions| -%> <%- conditions.each do |condition| -%> <%- acl_names="" -%> @@ -357,9 +407,10 @@ frontend http-in <%- if p("ha_proxy.disable_backend_http2_websockets") -%> # Send websockets to a backend that forces HTTP/1.1. This avoids bugs in Go & Gorouter's HTTP/2 websocket support # https://github.com/cloudfoundry/routing-release/issues/230 + # Note: first matching backend will be used when there are multiple use_backend directives acl is_websocket hdr(Upgrade) -i WebSocket acl is_websocket hdr_beg(Host) -i ws - use_backend http-routers-ws-http1 if is_websocket + use_backend http-routers-http1 if is_websocket <%- end -%> # }}} <% end -%> @@ -371,7 +422,7 @@ frontend https-in <% if p("ha_proxy.drain_enable") -%> grace <%= (p("ha_proxy.drain_frontend_grace_time").to_f * 1000).to_i %> <%- end -%> - bind <%= p("ha_proxy.binding_ip") %>:443 <%= accept_proxy %> <%= tls_bind_options %> <%= v4v6 %> <%= alpn_config %> + bind <%= p("ha_proxy.binding_ip") %>:443 <%= accept_proxy %> <%= tls_bind_options %> <%= v4v6 %> <%= default_alpn_config %> <%- if disable_domain_fronting -%> # Check whether the client is attempting domain fronting. # Ensure a host header exists to check against @@ -456,7 +507,7 @@ frontend https-in http-response set-header Strict-Transport-Security max-age=<%= p("ha_proxy.hsts_max_age").to_i %>;<% if p("ha_proxy.hsts_include_subdomains") %>\ includeSubDomains;<% end %><% if p("ha_proxy.hsts_preload") %>\ preload;<% end %> <%- end -%> capture request header Host len 256 - default_backend http-routers + default_backend <%= backends.last[:name] %> <%- if_p("ha_proxy.http_request_deny_conditions") do |conditions| -%> <%- conditions.each do |condition| -%> <%- acl_names="" -%> @@ -499,9 +550,18 @@ frontend https-in <%- if p("ha_proxy.disable_backend_http2_websockets") -%> # Send websockets to a backend that forces HTTP/1.1. This avoids bugs in Go & Gorouter's HTTP/2 websocket support # https://github.com/cloudfoundry/routing-release/issues/230 + # Note: first matching backend will be used when there are multiple use_backend directives acl is_websocket hdr(Upgrade) -i WebSocket acl is_websocket hdr_beg(Host) -i ws - use_backend http-routers-ws-http1 if is_websocket + use_backend http-routers-http1 if is_websocket + <%- end -%> + + <%- if backend_match_http_protocol && backends.length == 2 -%> + # Ensure that backend protocol matches frontend protocol + # Note: first matching backend will be used when there are multiple use_backend directives + acl is_http2 ssl_fc_alpn,lower,strcmp(proc.h2_alpn_tag) eq 0 + use_backend http-routers-http1 if ! is_http2 + use_backend http-routers-http2 if is_http2 <%- end -%> # }}} <% end -%> @@ -598,7 +658,7 @@ frontend wss-in http-response set-header Strict-Transport-Security max-age=<%= p("ha_proxy.hsts_max_age").to_i %>;<% if p("ha_proxy.hsts_include_subdomains") %>\ includeSubDomains;<% end %><% if p("ha_proxy.hsts_preload") %>\ preload;<% end %> <%- end -%> capture request header Host len 256 - default_backend http-routers + default_backend <%= backends.last[:name] %> <%- if_p("ha_proxy.http_request_deny_conditions") do |conditions| -%> <%- conditions.each do |condition| -%> <%- acl_names="" -%> @@ -643,50 +703,20 @@ frontend wss-in # https://github.com/cloudfoundry/routing-release/issues/230 acl is_websocket hdr(Upgrade) -i WebSocket acl is_websocket hdr_beg(Host) -i ws - use_backend http-routers-ws-http1 if is_websocket + use_backend http-routers-http1 if is_websocket + <%- end -%> + + <%- if backend_match_http_protocol && backends.length == 2 -%> + # Ensure that backend protocol matches frontend protocol + # Note: first matching backend will be used when there are multiple use_backend directives + acl is_http2 ssl_fc_alpn,lower,strcmp(proc.h2_alpn_tag) eq 0 + use_backend http-routers-http1 if ! is_http2 + use_backend http-routers-http2 if is_http2 <%- end -%> # }}} <% end -%> <%- - backend_servers = [] - backend_servers_local = [] - backend_port = nil - if_link("http_backend") do |backend| - backend_servers = backend.instances.map(&:address) - backend_port = backend.p("port", p("ha_proxy.backend_port")) - - if p("ha_proxy.backend_prefer_local_az") - backend_servers_local = backend.instances.select{ |n| n.az == spec.az }.map(&:address) - end - end.else_if_p("ha_proxy.backend_servers") do |servers| - backend_servers = servers - backend_port = p("ha_proxy.backend_port") - end - resolvers = "" - if_p("ha_proxy.resolvers") do - resolvers = "resolvers default " - end - backend_crt = "" - if_p("ha_proxy.backend_crt") do - backend_crt = "crt /var/vcap/jobs/haproxy/config/backend-crt.pem " - end - - backend_ssl = "" - if p("ha_proxy.backend_ssl").downcase == "verify" - backend_ssl = "ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem " - if_p("ha_proxy.backend_ssl_verifyhost") do | verify_hostname | - backend_ssl += "verifyhost #{verify_hostname} " - end - elsif p("ha_proxy.backend_ssl").downcase == "noverify" - backend_ssl = "ssl verify none " - end - - backends = [{ name: "http-routers", backend_ssl: (backend_ssl != "" ? backend_ssl + alpn_config : "") }] - if p("ha_proxy.disable_backend_http2_websockets") - backends += [{ name: "http-routers-ws-http1", backend_ssl: backend_ssl + "alpn http/1.1 " }] - end - backends.each do |backend| -%> # Backend <%= backend[:name] %> {{{ @@ -710,7 +740,7 @@ backend <%= backend[:name] %> <%- health_check_options = "port " + p("ha_proxy.backend_http_health_port").to_s -%> <%- end -%> <% backend_servers.each_with_index do |ip, index| %> - server node<%= index %> <%= ip %>:<%= backend_port -%> <%= resolvers -%><%= backend_crt -%>check inter 1000 <%= health_check_options %> <%= backend[:backend_ssl] %><%- if !backend_servers_local.empty? && !backend_servers_local.include?(ip) -%> backup<%- end -%> + server node<%= index %> <%= ip %>:<%= backend_port -%> <%= resolvers -%><%= backend_crt -%>check inter 1000 <%= health_check_options %> <%= backend[:backend_ssl] %><%= backend[:alpn] %><%- if !backend_servers_local.empty? && !backend_servers_local.include?(ip) -%> backup<%- end -%> <% end %> # }}} <%- end %> @@ -748,10 +778,10 @@ backend http-routed-backend-<%= prefix_hash %> if data["backend_verifyhost"] backend_ssl += "verifyhost #{data["backend_verifyhost"]} " end - backend_ssl += alpn_config + backend_ssl += default_alpn_config elsif data["backend_ssl"].downcase == "noverify" backend_ssl = "ssl verify none " - backend_ssl += alpn_config + backend_ssl += default_alpn_config end end -%> diff --git a/spec/haproxy/templates/haproxy_config/backend_http_spec.rb b/spec/haproxy/templates/haproxy_config/backend_http_spec.rb index e6226b26..f8450aa0 100644 --- a/spec/haproxy/templates/haproxy_config/backend_http_spec.rb +++ b/spec/haproxy/templates/haproxy_config/backend_http_spec.rb @@ -8,22 +8,23 @@ end let(:properties) { {} } - let(:backend_http_routers) { haproxy_conf['backend http-routers'] } + let(:backend_http1) { haproxy_conf['backend http-routers-http1'] } + let(:backend_http2) { haproxy_conf['backend http-routers-http2'] } it 'has the correct mode' do - expect(backend_http_routers).to include('mode http') + expect(backend_http1).to include('mode http') end it 'uses round-robin load balancing' do - expect(backend_http_routers).to include('balance roundrobin') + expect(backend_http1).to include('balance roundrobin') end context 'when ha_proxy.compress_types are provided' do let(:properties) { { 'compress_types' => 'text/html text/plain text/css' } } it 'configures the compression type and algorithm' do - expect(backend_http_routers).to include('compression algo gzip') - expect(backend_http_routers).to include('compression type text/html text/plain text/css') + expect(backend_http1).to include('compression algo gzip') + expect(backend_http1).to include('compression type text/html text/plain text/css') end end @@ -35,7 +36,7 @@ end it 'includes the config' do - expect(backend_http_routers).to include('custom backend config') + expect(backend_http1).to include('custom backend config') end end @@ -49,7 +50,7 @@ end it 'includes the errorfiles' do - expect(backend_http_routers).to include('errorfile 503 /var/vcap/jobs/haproxy/errorfiles/custom503.http') + expect(backend_http1).to include('errorfile 503 /var/vcap/jobs/haproxy/errorfiles/custom503.http') end end @@ -62,12 +63,12 @@ end it 'configures the healthcheck' do - expect(backend_http_routers).to include('option httpchk GET /health') + expect(backend_http1).to include('option httpchk GET /health') end it 'adds the healthcheck to the server config' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') end context 'when backend_http_health_uri is provided' do @@ -80,12 +81,12 @@ end it 'configures the healthcheck' do - expect(backend_http_routers).to include('option httpchk GET 1.2.3.5/health') + expect(backend_http1).to include('option httpchk GET 1.2.3.5/health') end it 'adds the healthcheck to the server config' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 port 8080') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 port 8080') end end @@ -99,12 +100,12 @@ end it 'configures the healthcheck' do - expect(backend_http_routers).to include('option httpchk GET /health') + expect(backend_http1).to include('option httpchk GET /health') end it 'adds the healthcheck to the server config' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 port 8081') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 port 8081') + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 port 8081') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 port 8081') end end end @@ -117,8 +118,8 @@ end it 'configures the servers' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000') + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000') end end @@ -131,8 +132,8 @@ end it 'configures the server to use the provided certificate' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') + expect(backend_http1).to include('server node0 10.0.0.1:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:80 crt /var/vcap/jobs/haproxy/config/backend-crt.pem check inter 1000') end end @@ -144,9 +145,9 @@ } end - it 'configures the server to use ssl: verify' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem') + it 'configures the server to use ssl: verify and adds the alpn config' do + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn http/1.1') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn http/1.1') end context 'when ha_proxy.backend_ssl_verifyhost is provided' do @@ -158,9 +159,9 @@ } end - it 'configures the server to use ssl: verify with verifyhost for the provided host name' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com') + it 'configures the server to use ssl: verify with verifyhost for the provided host name and adds the alpn config' do + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com alpn http/1.1') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem verifyhost backend.com alpn http/1.1') end context 'when ha_proxy.backend_ssl is not verify' do @@ -174,24 +175,57 @@ it 'aborts with a meaningful error message' do expect do - backend_http_routers + backend_http1 end.to raise_error /Conflicting configuration: backend_ssl must be 'verify' to use backend_ssl_verifyhost/ end end end - context 'when ha_proxy.enable_http2 is true' do - let(:properties) do - { - 'backend_servers' => ['10.0.0.1', '10.0.0.2'], - 'backend_ssl' => 'verify', - 'enable_http2' => true - } - end - - it 'enables h2 ALPN negotiation with backends' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn h2,http/1.1') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn h2,http/1.1') + context 'when backend protocol-modifying properties are set' do + [ + { disable_backend_http2_websockets: false, enable_http2: false, backend_match_http_protocol: false, expect_h1: true, expect_h2: false }, + { disable_backend_http2_websockets: true, enable_http2: false, backend_match_http_protocol: false, expect_h1: true, expect_h2: false }, + { disable_backend_http2_websockets: false, enable_http2: true, backend_match_http_protocol: false, expect_h1: false, expect_h2: true }, + { disable_backend_http2_websockets: true, enable_http2: true, backend_match_http_protocol: false, expect_h1: true, expect_h2: true }, + { disable_backend_http2_websockets: false, enable_http2: true, backend_match_http_protocol: true, expect_h1: true, expect_h2: true }, + { disable_backend_http2_websockets: true, enable_http2: true, backend_match_http_protocol: true, expect_h1: true, expect_h2: true }, + { disable_backend_http2_websockets: false, enable_http2: false, backend_match_http_protocol: true, expect_h1: true, expect_h2: true }, + { disable_backend_http2_websockets: true, enable_http2: false, backend_match_http_protocol: true, expect_h1: true, expect_h2: true } + + ].each do |test_case| + context "when disable_backend_http2_websockets is #{test_case[:disable_backend_http2_websockets]}, enable_http2 is #{test_case[:enable_http2]}, and backend_match_http_protocol is #{test_case[:backend_match_http_protocol]}" do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'verify', + 'disable_backend_http2_websockets' => test_case[:disable_backend_http2_websockets], + 'enable_http2' => test_case[:enable_http2], + 'backend_match_http_protocol' => test_case[:backend_match_http_protocol] + } + end + + if test_case[:expect_h1] + it 'creates an HTTP1.1 backend' do + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn http/1.1') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn http/1.1') + end + else + it 'does not create an HTTP1.1 backend' do + expect(backend_http1).to be_nil + end + end + + if test_case[:expect_h2] + it 'creates an HTTP2 backend' do + expect(backend_http2).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn h2,http/1.1') + expect(backend_http2).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify required ca-file /var/vcap/jobs/haproxy/config/backend-ca-certs.pem alpn h2,http/1.1') + end + else + it 'does not create an HTTP2 backend' do + expect(backend_http2).to be_nil + end + end + end end end end @@ -205,23 +239,8 @@ end it 'configures the server to use ssl: verify none' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify none') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify none') - end - - context 'when ha_proxy.enable_http2 is true' do - let(:properties) do - { - 'backend_servers' => ['10.0.0.1', '10.0.0.2'], - 'backend_ssl' => 'noverify', - 'enable_http2' => true - } - end - - it 'enables h2 ALPN negotiation with backends' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify none alpn h2,http/1.1') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify none alpn h2,http/1.1') - end + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000 ssl verify none alpn http/1.1') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000 ssl verify none alpn http/1.1') end end @@ -234,22 +253,41 @@ end it 'configures the server to not use ssl' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000') - end - - context 'when ha_proxy.enable_http2 is true' do - let(:properties) do - { - 'backend_servers' => ['10.0.0.1', '10.0.0.2'], - 'backend_ssl' => 'off', - 'enable_http2' => true - } - end - - it 'does not include ALPN configuration' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 check inter 1000') + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000') + end + + context 'when backend protocol-modifying properties are set' do + [ + { disable_backend_http2_websockets: false, enable_http2: false, backend_match_http_protocol: false }, + { disable_backend_http2_websockets: true, enable_http2: false, backend_match_http_protocol: false }, + { disable_backend_http2_websockets: false, enable_http2: true, backend_match_http_protocol: false }, + { disable_backend_http2_websockets: true, enable_http2: true, backend_match_http_protocol: false }, + { disable_backend_http2_websockets: false, enable_http2: true, backend_match_http_protocol: true }, + { disable_backend_http2_websockets: true, enable_http2: true, backend_match_http_protocol: true }, + { disable_backend_http2_websockets: false, enable_http2: false, backend_match_http_protocol: true }, + { disable_backend_http2_websockets: true, enable_http2: false, backend_match_http_protocol: true } + ].each do |test_case| + context "when disable_backend_http2_websockets is #{test_case[:disable_backend_http2_websockets]}, enable_http2 is #{test_case[:enable_http2]}, and backend_match_http_protocol is #{test_case[:backend_match_http_protocol]}" do + let(:properties) do + { + 'backend_servers' => ['10.0.0.1', '10.0.0.2'], + 'backend_ssl' => 'off', + 'disable_backend_http2_websockets' => test_case[:disable_backend_http2_websockets], + 'enable_http2' => test_case[:enable_http2], + 'backend_match_http_protocol' => test_case[:backend_match_http_protocol] + } + end + + it 'does not include ALPN configuration' do + expect(backend_http1).to include('server node0 10.0.0.1:80 check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:80 check inter 1000') + end + + it 'does not create an HTTP2 backend' do + expect(backend_http2).to be_nil + end + end end end end @@ -263,8 +301,8 @@ end it 'overrides the default port' do - expect(backend_http_routers).to include('server node0 10.0.0.1:7000 check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:7000 check inter 1000') + expect(backend_http1).to include('server node0 10.0.0.1:7000 check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:7000 check inter 1000') end end @@ -277,8 +315,8 @@ end it 'sets the resolver on the server configuration' do - expect(backend_http_routers).to include('server node0 10.0.0.1:80 resolvers default check inter 1000') - expect(backend_http_routers).to include('server node1 10.0.0.2:80 resolvers default check inter 1000') + expect(backend_http1).to include('server node0 10.0.0.1:80 resolvers default check inter 1000') + expect(backend_http1).to include('server node1 10.0.0.2:80 resolvers default check inter 1000') end end @@ -301,8 +339,8 @@ end it 'correctly configures the servers' do - expect(backend_http_routers).to include('server node0 backend.az1.internal:80 check inter 1000') - expect(backend_http_routers).to include('server node1 backend.az2.internal:80 check inter 1000') + expect(backend_http1).to include('server node0 backend.az1.internal:80 check inter 1000') + expect(backend_http1).to include('server node1 backend.az2.internal:80 check inter 1000') end context 'when ha_proxy.backend_prefer_local_az is true' do @@ -311,8 +349,8 @@ end it 'configures servers in other azs as backup servers' do - expect(backend_http_routers).to include('server node0 backend.az1.internal:80 check inter 1000') - expect(backend_http_routers).to include('server node1 backend.az2.internal:80 check inter 1000 backup') + expect(backend_http1).to include('server node0 backend.az1.internal:80 check inter 1000') + expect(backend_http1).to include('server node1 backend.az2.internal:80 check inter 1000 backup') end end end diff --git a/spec/haproxy/templates/haproxy_config/backend_wss_spec.rb b/spec/haproxy/templates/haproxy_config/backend_wss_spec.rb index 160e6de9..16f95958 100644 --- a/spec/haproxy/templates/haproxy_config/backend_wss_spec.rb +++ b/spec/haproxy/templates/haproxy_config/backend_wss_spec.rb @@ -10,41 +10,18 @@ let(:properties) do { 'backend_servers' => ['10.0.0.1', '10.0.0.2'] } end - let(:backend_http_routers) { haproxy_conf['backend http-routers'] } - let(:backend_http_routers_ws) { haproxy_conf['backend http-routers-ws-http1'] } + let(:frontend_http) { haproxy_conf['frontend http-in'] } let(:frontend_https) { haproxy_conf['frontend https-in'] } let(:frontend_wss_in) { haproxy_conf['frontend wss-in'] } - it 'does not exist by default' do - expect(backend_http_routers_ws).to be_nil - end - context 'when ha_proxy.disable_backend_http2_websockets is configured' do let(:properties) do super().merge({ 'disable_backend_http2_websockets' => true }) end - it 'exists' do - expect(backend_http_routers_ws).not_to be_nil - end - - it 'is the same as the normal http-routers backend except for different server settings' do - http_non_server_entries = backend_http_routers.reject { |l| l =~ /^server / } - http_ws_non_server_entries = backend_http_routers_ws.reject { |l| l =~ /^server / } - expect(http_non_server_entries).to eq(http_ws_non_server_entries) - end - - it 'uses an upstream ALPN of HTTP/1.1 only' do - server_entries = backend_http_routers_ws.select { |l| l =~ /^server / } - expect(server_entries).to contain_exactly( - 'server node0 10.0.0.1:80 check inter 1000 alpn http/1.1', - 'server node1 10.0.0.2:80 check inter 1000 alpn http/1.1' - ) - end - it 'receives websocket traffic from http-in' do - expect(frontend_http).to include('use_backend http-routers-ws-http1 if is_websocket') + expect(frontend_http).to include('use_backend http-routers-http1 if is_websocket') end context 'when https is enabled' do @@ -53,7 +30,7 @@ end it 'receives websocket traffic from https-in' do - expect(frontend_http).to include('use_backend http-routers-ws-http1 if is_websocket') + expect(frontend_http).to include('use_backend http-routers-http1 if is_websocket') end context 'when the port 4443 websocket frontend is enabled' do @@ -62,7 +39,7 @@ end it 'receives websocket traffic from wss-in' do - expect(frontend_wss_in).to include('use_backend http-routers-ws-http1 if is_websocket') + expect(frontend_wss_in).to include('use_backend http-routers-http1 if is_websocket') end end end diff --git a/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb index f65bd3ff..c994c0bb 100644 --- a/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb +++ b/spec/haproxy/templates/haproxy_config/frontend_http_spec.rb @@ -122,8 +122,39 @@ expect(frontend_http).to include('capture request header Host len 256') end - it 'has the correct default backend' do - expect(frontend_http).to include('default_backend http-routers') + context 'when HTTP1 backend servers are available' do + it 'has the uses the HTTP1 backend default backend' do + expect(frontend_http).to include('default_backend http-routers-http1') + end + end + + context 'when only HTTP1 and HTTP2 backend servers are available' do + let(:properties) do + { + 'disable_backend_http2_websockets' => true, + 'enable_http2' => true, + 'backend_ssl' => 'verify' + } + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_http).to include('default_backend http-routers-http2') + end + end + + context 'when only HTTP2 backend servers are available' do + let(:properties) do + { + 'disable_backend_http2_websockets' => false, + 'enable_http2' => true, + 'backend_match_http_protocol' => false, + 'backend_ssl' => 'verify' + } + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_http).to include('default_backend http-routers-http2') + end end context 'when ha_proxy.http_request_deny_conditions are provided' do diff --git a/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb index 994486fe..ed9d9da4 100644 --- a/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb +++ b/spec/haproxy/templates/haproxy_config/frontend_https_spec.rb @@ -459,8 +459,39 @@ expect(frontend_https).to include('capture request header Host len 256') end - it 'has the correct default backend' do - expect(frontend_https).to include('default_backend http-routers') + context 'when only HTTP1 backend servers are available' do + it 'has the uses the HTTP1 backend default backend' do + expect(frontend_https).to include('default_backend http-routers-http1') + end + end + + context 'when HTTP1 and HTTP2 backend servers are available' do + let(:properties) do + default_properties.merge({ + 'disable_backend_http2_websockets' => true, + 'enable_http2' => true, + 'backend_ssl' => 'verify' + }) + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_https).to include('default_backend http-routers-http2') + end + end + + context 'when only HTTP2 backend servers are available' do + let(:properties) do + default_properties.merge({ + 'disable_backend_http2_websockets' => false, + 'enable_http2' => true, + 'backend_match_http_protocol' => false, + 'backend_ssl' => 'verify' + }) + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_https).to include('default_backend http-routers-http2') + end end context 'when ha_proxy.http_request_deny_conditions are provided' do @@ -596,6 +627,34 @@ end end + context 'when backend_match_http_protocol is true' do + let(:properties) do + default_properties.merge({ + 'backend_match_http_protocol' => true, + 'backend_ssl' => 'verify' + }) + end + + it 'enables config to match the protocol' do + expect(frontend_https).to include('acl is_http2 ssl_fc_alpn,lower,strcmp(proc.h2_alpn_tag) eq 0') + expect(frontend_https).to include('use_backend http-routers-http1 if ! is_http2') + expect(frontend_https).to include('use_backend http-routers-http2 if is_http2') + end + + context('when backend_ssl is off') do + let(:properties) do + default_properties.merge({ + 'backend_match_http_protocol' => true, + 'backend_ssl' => 'off' + }) + end + + it 'does not override the default backend' do + expect(frontend_https).not_to include(/use_backend/) + end + end + end + context 'when no ssl options are provided' do let(:properties) { {} } diff --git a/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb b/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb index 6717e7d9..24989acf 100644 --- a/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb +++ b/spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb @@ -457,8 +457,67 @@ expect(frontend_wss).to include('capture request header Host len 256') end - it 'has the correct default backend' do - expect(frontend_wss).to include('default_backend http-routers') + context 'when only HTTP1 backend servers are available' do + it 'has the uses the HTTP1 backend default backend' do + expect(frontend_wss).to include('default_backend http-routers-http1') + end + end + + context 'when HTTP1 and HTTP2 backend servers are available' do + let(:properties) do + default_properties.merge({ + 'disable_backend_http2_websockets' => true, + 'enable_http2' => true, + 'backend_ssl' => 'verify' + }) + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_wss).to include('default_backend http-routers-http2') + end + end + + context 'when only HTTP2 backend servers are available' do + let(:properties) do + default_properties.merge({ + 'disable_backend_http2_websockets' => false, + 'enable_http2' => true, + 'backend_match_http_protocol' => false, + 'backend_ssl' => 'verify' + }) + end + + it 'uses the HTTP2 backend default backend' do + expect(frontend_wss).to include('default_backend http-routers-http2') + end + end + + context 'when backend_match_http_protocol is true' do + let(:properties) do + default_properties.merge({ + 'backend_match_http_protocol' => true, + 'backend_ssl' => 'verify' + }) + end + + it 'enables config to match the protocol' do + expect(frontend_wss).to include('acl is_http2 ssl_fc_alpn,lower,strcmp(proc.h2_alpn_tag) eq 0') + expect(frontend_wss).to include('use_backend http-routers-http1 if ! is_http2') + expect(frontend_wss).to include('use_backend http-routers-http2 if is_http2') + end + + context('when backend_ssl is off') do + let(:properties) do + default_properties.merge({ + 'backend_match_http_protocol' => true, + 'backend_ssl' => 'off' + }) + end + + it 'does not override the default backend' do + expect(frontend_wss).not_to include(/use_backend/) + end + end end context 'when ha_proxy.http_request_deny_conditions are provided' do diff --git a/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb b/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb index 9647de2a..595ce163 100644 --- a/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb +++ b/spec/haproxy/templates/haproxy_config/healthcheck_listener_spec.rb @@ -16,14 +16,34 @@ } end - it 'adds a health check listener for the http-routers' do + it 'adds a health check listener for the http-routers-http1' do expect(healthcheck_listener).to include('bind :8080') expect(healthcheck_listener).to include('mode http') expect(healthcheck_listener).to include('monitor-uri /health') - expect(healthcheck_listener).to include('acl http-routers_down nbsrv(http-routers) eq 0') + expect(healthcheck_listener).to include('acl http-routers_down nbsrv(http-routers-http1) eq 0') expect(healthcheck_listener).to include('monitor fail if http-routers_down') end + context 'when only http2 backend servers are available' do + let(:properties) do + { + 'enable_health_check_http' => true, + 'disable_backend_http2_websockets' => false, + 'enable_http2' => true, + 'backend_match_http_protocol' => false, + 'backend_ssl' => 'verify' + } + end + + it 'adds a health check listener for the http-routers-http2' do + expect(healthcheck_listener).to include('bind :8080') + expect(healthcheck_listener).to include('mode http') + expect(healthcheck_listener).to include('monitor-uri /health') + expect(healthcheck_listener).to include('acl http-routers_down nbsrv(http-routers-http2) eq 0') + expect(healthcheck_listener).to include('monitor fail if http-routers_down') + end + end + context 'when health_check_port is not the default' do let(:properties) do { From 04b2424a2b1cf76b2d2c08499ddf8aed28c2d721 Mon Sep 17 00:00:00 2001 From: Peter Jones Date: Thu, 28 Oct 2021 10:05:28 +0100 Subject: [PATCH 3/3] add release notes --- ci/release_notes.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 ci/release_notes.md diff --git a/ci/release_notes.md b/ci/release_notes.md new file mode 100644 index 00000000..a611558d --- /dev/null +++ b/ci/release_notes.md @@ -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