Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable PROXY protocol for specific CIDRs in HAProxy #711

Merged
merged 33 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
624d8bb
Add property and validation
Dariquest Sep 9, 2024
a3e0658
Property spec description
Dariquest Sep 9, 2024
2cf95e1
Adjust haproxy config to read the cidr list from a file
Dariquest Sep 9, 2024
5f02a92
Correct file name to be conform with other lists
Dariquest Sep 9, 2024
7529ab6
Correct desc and default value
Dariquest Sep 9, 2024
352f0c2
Correct desc and default value
Dariquest Sep 9, 2024
8e6b8cc
Spec and templates
Dariquest Sep 9, 2024
36ab94c
Spec extension
Dariquest Sep 10, 2024
fb72417
IPv6 addresses from the doc space
Dariquest Sep 10, 2024
a8f1e90
IPv6 addresses from the doc space
Dariquest Sep 10, 2024
ae21bf9
Property validation
Dariquest Sep 10, 2024
0620360
unit test for proxies_cidr.txt.erb
Mrizwanshaik Sep 10, 2024
a265106
First acceptance test & remove the AWS IPs
Dariquest Sep 10, 2024
88aed5b
frontend tests
Dariquest Sep 11, 2024
fc656e1
implement ruby spec tests
Mrizwanshaik Sep 12, 2024
f283e58
implement ruby spec tests
Dariquest Sep 13, 2024
3a3c165
implement ruby spec tests
Dariquest Sep 13, 2024
6c92038
implement ruby spec tests
Dariquest Sep 13, 2024
9c1df9f
add acceptance test for expect_proxy protocol
Mrizwanshaik Sep 20, 2024
5cddacc
fix: proxy_protocol_test.go
a18e Sep 24, 2024
75c4f38
Unnecessary changes
Dariquest Sep 24, 2024
9116297
Revert unnecessary changes
Dariquest Sep 24, 2024
160c7de
Add new line
Dariquest Sep 24, 2024
0592c43
fix linter errors
a18e Sep 25, 2024
ffa1156
improve test comments
a18e Sep 25, 2024
cef5323
Rename expect_proxy to expect_proxy_cidrs
Dariquest Sep 25, 2024
c09f269
Rename expect_proxy to expect_proxy_cidrs
Dariquest Sep 25, 2024
e9658d9
Rename expect_proxy to expect_proxy_cidrs
Dariquest Sep 25, 2024
a373c25
fix: expect proxy protocol also for health check
a18e Sep 25, 2024
26ac7ad
fix: indentation
a18e Sep 27, 2024
4906e2d
Changed the unit test to change expectation
Dariquest Oct 1, 2024
ce6929c
One more unit test and correct config
Dariquest Oct 1, 2024
b30b5a1
Extend spec description
Dariquest Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 86 additions & 38 deletions acceptance-tests/proxy_protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
)

var _ = Describe("Proxy Protocol", func() {
opsfileProxyProtocol := `---

Context("accept_proxy", func() {
opsfileProxyProtocol := `---
# Enable Proxy Protocol
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/accept_proxy?
Expand All @@ -23,44 +25,90 @@ var _ = Describe("Proxy Protocol", func() {
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/disable_health_check_proxy?
value: false
`
It("Correctly proxies Proxy Protocol requests", func() {
haproxyBackendPort := 12000
haproxyInfo, _ := deployHAProxy(baseManifestVars{
haproxyBackendPort: haproxyBackendPort,
haproxyBackendServers: []string{"127.0.0.1"},
deploymentName: deploymentNameForTestNode(),
}, []string{opsfileProxyProtocol}, map[string]interface{}{}, false)

closeLocalServer, localPort := startDefaultTestServer()
defer closeLocalServer()

closeTunnel := setupTunnelFromHaproxyToTestServer(haproxyInfo, haproxyBackendPort, localPort)
defer closeTunnel()

By("Waiting for monit to report that HAProxy is healthy")
// Since the backend is now listening, HAProxy healthcheck should start returning healthy
// and monit should in turn start reporting a healthy process
// We will wait up to one minute for the status to stabilise
Eventually(func() string {
return boshInstances(deploymentNameForTestNode())[0].ProcessState
}, time.Minute, time.Second).Should(Equal("running"))

By("Sending a request with Proxy Protocol Header to HAProxy traffic port")
err := performProxyProtocolRequest(haproxyInfo.PublicIP, 80, "/")
Expect(err).NotTo(HaveOccurred())

By("Sending a request without Proxy Protocol Header to HAProxy")
_, err = http.Get(fmt.Sprintf("http://%s", haproxyInfo.PublicIP))
expectConnectionResetErr(err)

By("Sending a request with Proxy Protocol Header to HAProxy health check port")
err = performProxyProtocolRequest(haproxyInfo.PublicIP, 8080, "/health")
Expect(err).NotTo(HaveOccurred())

By("Sending a request without Proxy Protocol Header to HAProxy health check port")
_, err = http.Get(fmt.Sprintf("http://%s:8080/health", haproxyInfo.PublicIP))
expectConnectionResetErr(err)

})
})

It("Correctly proxies Proxy Protocol requests", func() {
haproxyBackendPort := 12000
haproxyInfo, _ := deployHAProxy(baseManifestVars{
haproxyBackendPort: haproxyBackendPort,
haproxyBackendServers: []string{"127.0.0.1"},
deploymentName: deploymentNameForTestNode(),
}, []string{opsfileProxyProtocol}, map[string]interface{}{}, false)

closeLocalServer, localPort := startDefaultTestServer()
defer closeLocalServer()

closeTunnel := setupTunnelFromHaproxyToTestServer(haproxyInfo, haproxyBackendPort, localPort)
defer closeTunnel()

By("Waiting for monit to report that HAProxy is healthy")
// Since the backend is now listening, HAProxy healthcheck should start returning healthy
// and monit should in turn start reporting a healthy process
// We will wait up to one minute for the status to stabilise
Eventually(func() string {
return boshInstances(deploymentNameForTestNode())[0].ProcessState
}, time.Minute, time.Second).Should(Equal("running"))

By("Sending a request with Proxy Protocol Header to HAProxy traffic port")
err := performProxyProtocolRequest(haproxyInfo.PublicIP, 80, "/")
Expect(err).NotTo(HaveOccurred())

By("Sending a request without Proxy Protocol Header to HAProxy")
_, err = http.Get(fmt.Sprintf("http://%s", haproxyInfo.PublicIP))
expectConnectionResetErr(err)

By("Sending a request with Proxy Protocol Header to HAProxy health check port")
err = performProxyProtocolRequest(haproxyInfo.PublicIP, 8080, "/health")
Expect(err).NotTo(HaveOccurred())

By("Sending a request without Proxy Protocol Header to HAProxy health check port")
_, err = http.Get(fmt.Sprintf("http://%s:8080/health", haproxyInfo.PublicIP))
expectConnectionResetErr(err)
Context("expect_proxy_cidrs", func() {
opsfileExpectProxyProtocol := `---
# Enable Proxy Protocol
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/accept_proxy?
value: false
- type: replace
path: /instance_groups/name=haproxy/jobs/name=haproxy/properties/ha_proxy/expect_proxy_cidrs?
value:
- 10.0.0.0/8 # Bosh Network CIDR
`
It("Correctly proxies request with Expect Proxy CIDRs list", func() {
haproxyBackendPort := 12000
haproxyInfo, _ := deployHAProxy(baseManifestVars{
haproxyBackendPort: haproxyBackendPort,
haproxyBackendServers: []string{"127.0.0.1"},
deploymentName: deploymentNameForTestNode(),
}, []string{opsfileExpectProxyProtocol}, map[string]interface{}{}, true)

closeLocalServer, localPort := startDefaultTestServer()
defer closeLocalServer()

closeBackendTunnel := setupTunnelFromHaproxyToTestServer(haproxyInfo, haproxyBackendPort, localPort)
defer closeBackendTunnel()

By("Checking that Proxy Protocol is expected for requests from IPs in the list")
// Requests without Proxy Protocol Header should be rejected
_, err := http.Get(fmt.Sprintf("http://%s", haproxyInfo.PublicIP))
expectConnectionResetErr(err)

// Requests with Proxy Protocol Header should succeed
err = performProxyProtocolRequest(haproxyInfo.PublicIP, 80, "/")
Expect(err).NotTo(HaveOccurred())
peanball marked this conversation as resolved.
Show resolved Hide resolved

By("Checking that Proxy Protocol is NOT expected for requests from IPs NOT in the list")
// Set up a tunnel so that requests to localhost:11000 appear to come from localhost
// on the HAProxy VM as Proxy Protocol is not expected for requests from localhost (see ops file above)
closeFrontendTunnel := setupTunnelFromLocalMachineToHAProxy(haproxyInfo, 11000, 80)
defer closeFrontendTunnel()

// Requests without Proxy Protocol Header should succeed
expectTestServer200(http.Get("http://127.0.0.1:11000"))
})
})
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to have an edge test case with empty except_proxy_cidr and check that the call without proxy will be successful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an empty except_proxy_cidrs is equivalent to the property being off.

Expand Down
8 changes: 8 additions & 0 deletions jobs/haproxy/spec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ templates:
client-revocation-list.erb: config/client-revocation-list.pem
blacklist_cidrs.txt.erb: config/blacklist_cidrs.txt
whitelist_cidrs.txt.erb: config/whitelist_cidrs.txt
expect_proxy_cidrs.txt.erb: config/expect_proxy_cidrs.txt
trusted_domain_cidrs.txt.erb: config/trusted_domain_cidrs.txt

consumes:
Expand Down Expand Up @@ -580,6 +581,13 @@ properties:
cidr_whitelist:
- 172.168.4.1/32
- 10.2.0.0/16
ha_proxy.expect_proxy_cidrs:
description: "List of CIDRs to enable proxy protocol for. This enables forwarding of the client source IP for hyperscalers not supporting IP dual stack (v4 & v6)"
default: ~
example:
expect_proxy_cidrs:
- 10.6.7.8/27
- 2001:db8::/32
ha_proxy.block_all:
description: "Optionally block all incoming traffic to http(s). Use in conjunction with whitelist."
default: false
Expand Down
14 changes: 14 additions & 0 deletions jobs/haproxy/templates/expect_proxy_cidrs.txt.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%
require 'zlib'
require 'stringio'

if_p("ha_proxy.expect_proxy_cidrs") do |cidrs|
-%>
# generated from expect_proxy_cidrs.txt.erb

# BEGIN expect_proxy_cidrs
<%= cidrs.join("\n") %>
# END expect_proxy_cidrs
<%
end
%>
19 changes: 18 additions & 1 deletion jobs/haproxy/templates/haproxy.config.erb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ end
# }}}

# Error checking
if p("ha_proxy.accept_proxy",false) && p("ha_proxy.expect_proxy_cidrs",nil)
abort "Conflicting configuration: accept_proxy and expect_proxy_cidrs are mutually exclusive"
end
if !p("ha_proxy.drain_enable", false) && p("ha_proxy.drain_frontend_grace_time") > 0
abort "Conflicting configuration: drain_enable must be true to use drain_frontend_grace_time"
end
Expand Down Expand Up @@ -359,9 +362,14 @@ listen health_check_http_url
mode http
option httpclose
monitor-uri /health
<% if p("ha_proxy.accept_proxy") && !p("ha_proxy.disable_health_check_proxy") -%>
<%- if p("ha_proxy.accept_proxy") && !p("ha_proxy.disable_health_check_proxy") -%>
tcp-request connection expect-proxy layer4 unless LOCALHOST
<%- end -%>
<%- if_p("ha_proxy.expect_proxy_cidrs") do -%>
<%- if !p("ha_proxy.disable_health_check_proxy") -%>
tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }
<%- end -%>
<%- end -%>
acl http-routers_down nbsrv(<%= backends.first[:name] %>) eq 0
monitor fail if http-routers_down
<% end -%>
Expand Down Expand Up @@ -410,6 +418,9 @@ frontend http-in
<%- end -%>
<%- end -%>
<%- end -%>
<%- if_p("ha_proxy.expect_proxy_cidrs") do -%>
tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }
<%- end -%>
<%- if_p("ha_proxy.cidr_whitelist") do -%>
acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt
tcp-request content accept if whitelist
Expand Down Expand Up @@ -528,6 +539,9 @@ frontend https-in
<%- end -%>
<%- end -%>
<%- end -%>
<%- if_p("ha_proxy.expect_proxy_cidrs") do -%>
tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }
<%- end -%>
<%- if_p("ha_proxy.cidr_whitelist") do -%>
acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt
tcp-request content accept if whitelist
Expand Down Expand Up @@ -691,6 +705,9 @@ frontend wss-in
<%- if properties.ha_proxy.frontend_config -%>
<%= format_indented_multiline_config(p("ha_proxy.frontend_config")) %>
<%- end -%>
<% if_p("ha_proxy.expect_proxy_cidrs") do -%>
tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }
<%- end -%>
<%- if_p("ha_proxy.cidr_whitelist") do -%>
acl whitelist src -f /var/vcap/jobs/haproxy/config/whitelist_cidrs.txt
tcp-request content accept if whitelist
Expand Down
33 changes: 33 additions & 0 deletions spec/haproxy/templates/expect_proxy_cidrs.txt_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require 'rspec'

describe 'config/expect_proxy_cidrs.txt' do
let(:template) { haproxy_job.template('config/expect_proxy_cidrs.txt') }

context 'when ha_proxy.expect_proxy_cidrs' do
context 'when a list of cidrs is provided' do
it 'has the correct contents' do
expect(template.render({
'ha_proxy' => {
'expect_proxy_cidrs' => ['10.5.6.7/27',
'2001:db8::/32']
}
})).to eq(<<~EXPECTED)
# generated from expect_proxy_cidrs.txt.erb

# BEGIN expect_proxy_cidrs
10.5.6.7/27
2001:db8::/32
# END expect_proxy_cidrs
EXPECTED
end
end
end

context 'when ha_proxy.expect_proxy_cidrs is not provided' do
it 'is empty' do
expect(template.render({})).to be_a_blank_string
end
end
end
35 changes: 35 additions & 0 deletions spec/haproxy/templates/haproxy_config/frontend_http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,41 @@
expect(frontend_http).to include('bind :80 accept-proxy')
end
end

context 'when ha_proxy.expect_proxy_cidrs is not empty/nil and ha_proxy.accept_proxy is false' do
let(:properties) do
{ 'accept_proxy' => false,
'expect_proxy_cidrs' => ['127.0.0.1/8'] }
b1tamara marked this conversation as resolved.
Show resolved Hide resolved
end

it 'sets expect-proxy of tcp connection to the file proxies_cidrs.txt contents' do
expect(frontend_http).to include('tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }')
end
end

context 'when ha_proxy.expect_proxy_cidrs is empty and not nil and ha_proxy.accept_proxy is false' do
let(:properties) do
{ 'accept_proxy' => false,
'expect_proxy_cidrs' => [] }
end

it 'does not contain expect-proxy of tcp connection directive' do
expect(frontend_http).not_to include('tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }')
end
end

context 'when ha_proxy.accept_proxy is true and ha_proxy.expect_proxy_cidrs is not empty/nil' do
let(:properties) do
{ 'accept_proxy' => true,
'expect_proxy_cidrs' => ['127.0.0.1/8'] }
end

it 'aborts with a meaningful error message' do
expect do
frontend_http
end.to raise_error(/Conflicting configuration: accept_proxy and expect_proxy_cidrs are mutually exclusive/)
end
end
end

context 'when a custom ha_proxy.frontend_config is provided' do
Expand Down
24 changes: 24 additions & 0 deletions spec/haproxy/templates/haproxy_config/frontend_https_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@
expect(frontend_https).to include('bind :443 accept-proxy ssl crt /var/vcap/jobs/haproxy/config/ssl')
end
end

context 'when ha_proxy.expect_proxy_cidrs is not empty/nil and ha_proxy.accept_proxy is false' do
let(:properties) do
default_properties.merge({ 'accept_proxy' => false,
'expect_proxy_cidrs' => ['127.0.0.1/8'] })
end

it 'sets expect-proxy of tcp connection to the file proxies_cidrs.txt contents' do
expect(frontend_https).to include('tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }')
end
end

context 'when ha_proxy.accept_proxy is true and ha_proxy.expect_proxy_cidrs is not empty/nil' do
let(:properties) do
default_properties.merge({ 'accept_proxy' => true,
'expect_proxy_cidrs' => ['127.0.0.1/8'] })
end

it 'aborts with a meaningful error message' do
expect do
frontend_https
end.to raise_error(/Conflicting configuration: accept_proxy and expect_proxy_cidrs are mutually exclusive/)
end
end
end

context 'when ha_proxy.disable_domain_fronting is true' do
Expand Down
24 changes: 24 additions & 0 deletions spec/haproxy/templates/haproxy_config/frontend_wss_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@
expect(frontend_wss).to include('bind :4443 accept-proxy ssl crt /var/vcap/jobs/haproxy/config/ssl')
end
end

context 'when ha_proxy.expect_proxy_cidrs is not empty/nil and ha_proxy.accept_proxy is false' do
let(:properties) do
default_properties.merge({ 'accept_proxy' => false,
'expect_proxy_cidrs' => ['127.0.0.1/8'] })
end

it 'sets expect-proxy of tcp connection to the file proxies_cidrs.txt contents' do
expect(frontend_wss).to include('tcp-request connection expect-proxy layer4 if { src -f /var/vcap/jobs/haproxy/config/expect_proxy_cidrs.txt }')
end
end

context 'when ha_proxy.accept_proxy is true and ha_proxy.expect_proxy_cidrs is not empty/nil' do
let(:properties) do
default_properties.merge({ 'accept_proxy' => true,
'expect_proxy_cidrs' => ['127.0.0.1/8'] })
end

it 'aborts with a meaningful error message' do
expect do
frontend_wss
end.to raise_error(/Conflicting configuration: accept_proxy and expect_proxy_cidrs are mutually exclusive/)
end
end
end

context 'when ha_proxy.disable_domain_fronting is true' do
Expand Down