Skip to content

Commit efc272e

Browse files
Merge pull request #262 from b1tamara/master
Add alpn option to the crt-list
2 parents 739d426 + a4275bd commit efc272e

File tree

6 files changed

+168
-13
lines changed

6 files changed

+168
-13
lines changed

acceptance-tests/http.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"fmt"
78
"net"
89
"net/http"
910
"net/http/httptest"
@@ -84,3 +85,15 @@ func buildHTTP2Client(caCerts []string, addressMap map[string]string, clientCert
8485

8586
return &http.Client{Transport: transport}
8687
}
88+
89+
func connectTLSALPNNegotiatedProtocol(protos []string, publicIP string, ca string, sni string) (string, error) {
90+
config := buildTLSConfig([]string{ca}, []tls.Certificate{}, sni)
91+
config.NextProtos = protos
92+
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:443", publicIP), config)
93+
if err != nil {
94+
return "", err
95+
}
96+
defer conn.Close()
97+
98+
return conn.ConnectionState().NegotiatedProtocol, nil
99+
}

acceptance-tests/https_frontend_test.go

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var _ = Describe("HTTPS Frontend", func() {
1616
var closeLocalServer func()
1717
enableHTTP2 := false
1818
var http1Client *http.Client
19+
var http2Client *http.Client
1920

2021
haproxyBackendPort := 12000
2122
opsfileHTTPS := `---
@@ -32,6 +33,36 @@ var _ = Describe("HTTPS Frontend", func() {
3233
ssl_pem:
3334
cert_chain: ((https_frontend.certificate))((https_frontend_ca.certificate))
3435
private_key: ((https_frontend.private_key))
36+
# Configure CA and cert chain
37+
- type: replace
38+
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/crt_list?/-
39+
value:
40+
snifilter:
41+
- haproxy.h2.internal
42+
ssl_pem:
43+
cert_chain: ((https_frontend.certificate))((https_frontend_ca.certificate))
44+
private_key: ((https_frontend.private_key))
45+
alpn: ['h2']
46+
# Configure CA and cert chain
47+
- type: replace
48+
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/crt_list?/-
49+
value:
50+
snifilter:
51+
- haproxy.http11.internal
52+
ssl_pem:
53+
cert_chain: ((https_frontend.certificate))((https_frontend_ca.certificate))
54+
private_key: ((https_frontend.private_key))
55+
alpn: ['http/1.1']
56+
# Configure CA and cert chain
57+
- type: replace
58+
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/crt_list?/-
59+
value:
60+
snifilter:
61+
- haproxy.h2-http11.internal
62+
ssl_pem:
63+
cert_chain: ((https_frontend.certificate))((https_frontend_ca.certificate))
64+
private_key: ((https_frontend.private_key))
65+
alpn: ['h2', 'http/1.1']
3566
# Declare certs
3667
- type: replace
3768
path: /variables?/-
@@ -49,7 +80,7 @@ var _ = Describe("HTTPS Frontend", func() {
4980
options:
5081
ca: https_frontend_ca
5182
common_name: haproxy.internal
52-
alternative_names: [haproxy.internal]
83+
alternative_names: [haproxy.internal, haproxy.h2.internal, haproxy.http11.internal, haproxy.h2-http11.internal]
5384
`
5485

5586
var creds struct {
@@ -77,11 +108,14 @@ var _ = Describe("HTTPS Frontend", func() {
77108
closeLocalServer, localPort = startDefaultTestServer()
78109
closeTunnel = setupTunnelFromHaproxyToTestServer(haproxyInfo, haproxyBackendPort, localPort)
79110

80-
http1Client = buildHTTPClient(
81-
[]string{creds.HTTPSFrontend.CA},
82-
map[string]string{"haproxy.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP)},
83-
[]tls.Certificate{}, "",
84-
)
111+
addresses := map[string]string{
112+
"haproxy.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP),
113+
"haproxy.h2.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP),
114+
"haproxy.http11.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP),
115+
}
116+
117+
http1Client = buildHTTPClient([]string{creds.HTTPSFrontend.CA}, addresses, []tls.Certificate{}, "")
118+
http2Client = buildHTTP2Client([]string{creds.HTTPSFrontend.CA}, addresses, []tls.Certificate{})
85119
})
86120

87121
AfterEach(func() {
@@ -134,12 +168,6 @@ var _ = Describe("HTTPS Frontend", func() {
134168
Expect(resp.StatusCode).To(Equal(http.StatusOK))
135169
Eventually(gbytes.BufferReader(resp.Body)).Should(gbytes.Say("Hello cloud foundry"))
136170

137-
http2Client := buildHTTP2Client(
138-
[]string{creds.HTTPSFrontend.CA},
139-
map[string]string{"haproxy.internal:443": fmt.Sprintf("%s:443", haproxyInfo.PublicIP)},
140-
[]tls.Certificate{},
141-
)
142-
143171
By("Sending a request to HAProxy using HTTP 2")
144172
resp, err = http2Client.Get("https://haproxy.internal:443")
145173
Expect(err).NotTo(HaveOccurred())
@@ -150,4 +178,43 @@ var _ = Describe("HTTPS Frontend", func() {
150178
Eventually(gbytes.BufferReader(resp.Body)).Should(gbytes.Say("Hello cloud foundry"))
151179
})
152180
})
181+
182+
Context("ALPN Configuration via CRT list", func() {
183+
BeforeEach(func() {
184+
// Do not enable HTTP globally, since we are adding it via crt-list entries
185+
enableHTTP2 = false
186+
})
187+
188+
It("Negotiates the correct ALPN protocol", func() {
189+
// H2 endpoint should negotiate H2 if the client supports it
190+
alpnProto, err := connectTLSALPNNegotiatedProtocol([]string{"http/1.1", "h2"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.h2.internal")
191+
Expect(err).NotTo(HaveOccurred())
192+
Expect(alpnProto).To(Equal("h2"))
193+
194+
// HTTP/1.1 endpoint should negotiate HTTP/1.1 if the client supports it
195+
alpnProto, err = connectTLSALPNNegotiatedProtocol([]string{"h2", "http/1.1"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.http11.internal")
196+
Expect(err).NotTo(HaveOccurred())
197+
Expect(alpnProto).To(Equal("http/1.1"))
198+
199+
// H2+HTTP/1.1 endpoint should negotiate H2 if the client supports it
200+
alpnProto, err = connectTLSALPNNegotiatedProtocol([]string{"h2"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.h2-http11.internal")
201+
Expect(err).NotTo(HaveOccurred())
202+
Expect(alpnProto).To(Equal("h2"))
203+
204+
// H2+HTTP/1.1 endpoint should negotiate HTTP/1.1 if the client supports it
205+
alpnProto, err = connectTLSALPNNegotiatedProtocol([]string{"http/1.1"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.h2-http11.internal")
206+
Expect(err).NotTo(HaveOccurred())
207+
Expect(alpnProto).To(Equal("http/1.1"))
208+
209+
// H2 endpoint should not use ALPN if client does not support H2
210+
alpnProto, err = connectTLSALPNNegotiatedProtocol([]string{"http/1.1"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.h2.internal")
211+
Expect(err).NotTo(HaveOccurred())
212+
Expect(alpnProto).To(Equal(""))
213+
214+
// HTTP/1.1 endpoint should not use ALPN if client does not support HTTP/1.1
215+
alpnProto, err = connectTLSALPNNegotiatedProtocol([]string{"h2"}, haproxyInfo.PublicIP, creds.HTTPSFrontend.CA, "haproxy.http11.internal")
216+
Expect(err).NotTo(HaveOccurred())
217+
Expect(alpnProto).To(Equal(""))
218+
})
219+
})
153220
})

ci/release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# New Features
22
- New property `disable_backend_http2_websockets` to force backend websocket connections to use HTTP/1.1 (default `false`) #263 / #261
3+
- Support for `alpn` property in crt-list was added #262
4+
35

46
# Acknowledgements
57

6-
Thanks @46bit for the PR
8+
Thanks @46bit and @b1tamara!

jobs/haproxy/spec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ properties:
107107
Array of private keys and certificates used for TLS handshakes with downstream clients. Each element in the array is an object containing at least the field 'ssl_pem'.
108108
The field 'ssl_pem' itself is either an object containing fields 'cert_chain' and 'private_key', or a single string containing the cert chain and the private key.
109109
The following fields are optional:
110+
- 'alpn' (a optional array of strings). If both HTTP/2 and HTTP/1.1 are expected to be supported, both versions can be advertised, in order of preference
110111
- 'client_ca_file' (replaces ha_proxy.client_ca_file)
111112
- 'verify' (allowed values: [none|optional|required])
112113
- 'ssl_ciphers' (overrides ha_proxy.ssl_ciphers)
@@ -145,6 +146,9 @@ properties:
145146
ssl_ciphersuites: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
146147
ssl_min_version: TLSv1.2
147148
ssl_max_version: TLSv1.3
149+
alpn:
150+
- h2
151+
- http/1.1
148152
client_revocation_list: |
149153
-----BEGIN X509 CRL-----
150154
-----END X509 CRL-----

jobs/haproxy/templates/certs.ttar.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ if_p("ha_proxy.crt_list") do |crt_list|
5353
if list_entry.key?("ssl_max_version")
5454
sslbindconf += " ssl-max-ver " + list_entry["ssl_max_version"]
5555
end
56+
if list_entry.key?("alpn")
57+
sslbindconf += " alpn #{list_entry["alpn"].join(",")} "
58+
end
5659

5760
if sslbindconf != ""
5861
sslbindconf = " ["+sslbindconf.strip+"]"

spec/haproxy/templates/certs.ttar_spec.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,72 @@
413413
end
414414
end
415415

416+
describe 'ha_proxy.crt_list[].alpn only h2' do
417+
let(:ttar) do
418+
template.render({
419+
'ha_proxy' => {
420+
'crt_list' => [{
421+
'alpn' => ['h2'],
422+
'ssl_pem' => 'ssl_pem contents'
423+
}]
424+
}
425+
})
426+
end
427+
428+
it 'is included in the crt list' do
429+
expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED)
430+
431+
/var/vcap/jobs/haproxy/config/ssl/cert-0.pem [alpn h2]
432+
433+
434+
EXPECTED
435+
end
436+
end
437+
438+
describe 'ha_proxy.crt_list[].alpn h2 and http/1.1' do
439+
let(:ttar) do
440+
template.render({
441+
'ha_proxy' => {
442+
'crt_list' => [{
443+
'alpn' => ['h2', 'http/1.1'],
444+
'ssl_pem' => 'ssl_pem contents'
445+
}]
446+
}
447+
})
448+
end
449+
450+
it 'is included in the crt list' do
451+
expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED)
452+
453+
/var/vcap/jobs/haproxy/config/ssl/cert-0.pem [alpn h2,http/1.1]
454+
455+
456+
EXPECTED
457+
end
458+
end
459+
460+
describe 'ha_proxy.crt_list[].alpn prefer http/1.1 to h2' do
461+
let(:ttar) do
462+
template.render({
463+
'ha_proxy' => {
464+
'crt_list' => [{
465+
'alpn' => ['http/1.1', 'h2'],
466+
'ssl_pem' => 'ssl_pem contents'
467+
}]
468+
}
469+
})
470+
end
471+
472+
it 'is included in the crt list' do
473+
expect(ttar_entry(ttar, '/var/vcap/jobs/haproxy/config/ssl/crt-list')).to eq(<<~EXPECTED)
474+
475+
/var/vcap/jobs/haproxy/config/ssl/cert-0.pem [alpn http/1.1,h2]
476+
477+
478+
EXPECTED
479+
end
480+
end
481+
416482
describe 'ha_proxy.ext_crt_list' do
417483
context 'when there are no internal certificates' do
418484
let(:ttar) do

0 commit comments

Comments
 (0)