-
Notifications
You must be signed in to change notification settings - Fork 5.5k
proxy protocol: new feature auto-detect proxy protocol #18951
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
Changes from 59 commits
278ce7e
121e87d
2d43ae3
de6b7f2
3a36695
409f94c
ae19e1f
ed4fb54
bcc7aef
8c87bcd
4aa520e
91c9a1b
c45ecc7
4259f1a
2b4a89a
f105c3d
6a0d49b
f47232f
fa9746a
a37ad43
6f19883
0927000
b5cbfc1
a7ef68f
9495853
ffb56d1
ac2982f
2a66a01
c9e086f
af6f88d
7f17660
59206da
b416242
4e4638a
354a365
86ad288
899f199
887d0bc
a23ac5f
6075b67
4d6f75f
2cb4526
1667461
a0c977f
85706e6
c86edef
6031151
a09bb1b
cfeda5a
5a660a0
e92a8a4
84cdc32
f3d65e9
b2d467e
45e9c24
056f74f
39e7b50
392124e
307df1c
5c50e23
36bbfa2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,7 +46,8 @@ namespace ProxyProtocol { | |
| Config::Config( | ||
| Stats::Scope& scope, | ||
| const envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol& proto_config) | ||
| : stats_{ALL_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))} { | ||
| : stats_{ALL_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))}, | ||
| allow_requests_without_proxy_protocol_(proto_config.allow_requests_without_proxy_protocol()) { | ||
| for (const auto& rule : proto_config.rules()) { | ||
| tlv_types_[0xFF & rule.tlv_type()] = rule.on_tlv_present(); | ||
| } | ||
|
|
@@ -63,6 +64,10 @@ const KeyValuePair* Config::isTlvTypeNeeded(uint8_t type) const { | |
|
|
||
| size_t Config::numberOfNeededTlvTypes() const { return tlv_types_.size(); } | ||
|
|
||
| bool Config::allowRequestsWithoutProxyProtocol() const { | ||
| return allow_requests_without_proxy_protocol_; | ||
| } | ||
|
|
||
| Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) { | ||
| ENVOY_LOG(debug, "proxy_protocol: New connection accepted"); | ||
| cb_ = &cb; | ||
|
|
@@ -72,12 +77,17 @@ Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) { | |
|
|
||
| Network::FilterStatus Filter::onData(Network::ListenerFilterBuffer& buffer) { | ||
| const ReadOrParseState read_state = parseBuffer(buffer); | ||
| if (read_state == ReadOrParseState::Error) { | ||
| switch (read_state) { | ||
| case ReadOrParseState::Error: | ||
| config_->stats_.downstream_cx_proxy_proto_error_.inc(); | ||
| cb_->socket().ioHandle().close(); | ||
| return Network::FilterStatus::StopIteration; | ||
| } else if (read_state == ReadOrParseState::TryAgainLater) { | ||
| case ReadOrParseState::TryAgainLater: | ||
| return Network::FilterStatus::StopIteration; | ||
| case ReadOrParseState::SkipFilter: | ||
| return Network::FilterStatus::Continue; | ||
| default: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the default case. Then the compiler will warn/error if there are missing enum cases. I think it's possible that you won't need the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed in 5c50e23 unfortunately the compiler isn't smart enough to tell that every case is implemented and returns; thus we need the ghost "default" case at the end of the function after the switch statement source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc: In member function 'virtual Envoy::Network::FilterStatus Envoy::Extensions::ListenerFilters::ProxyProtocol::Filter::onData(Envoy::Network::ListenerFilterBuffer&)':
source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc:93:1: error: control reaches end of non-void function [-Werror=return-type]
93 | }
| ^I removed the default case regardless and confirmed that compiler will fail if any case is not implemented |
||
| return Network::FilterStatus::Continue; | ||
| } | ||
| return Network::FilterStatus::Continue; | ||
| } | ||
|
|
@@ -399,6 +409,19 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer) | |
| auto raw_slice = buffer.rawSlice(); | ||
| const char* buf = static_cast<const char*>(raw_slice.mem_); | ||
|
|
||
| if (config_.get()->allowRequestsWithoutProxyProtocol()) { | ||
| auto matchv2 = !memcmp(buf, PROXY_PROTO_V2_SIGNATURE, | ||
| std::min<size_t>(PROXY_PROTO_V2_SIGNATURE_LEN, raw_slice.len_)); | ||
| auto matchv1 = !memcmp(buf, PROXY_PROTO_V1_SIGNATURE, | ||
| std::min<size_t>(PROXY_PROTO_V1_SIGNATURE_LEN, raw_slice.len_)); | ||
|
ggreenway marked this conversation as resolved.
|
||
| if (!matchv2 && !matchv1) { | ||
| // The bytes we have seen so far do not match v1 or v2 proxy protocol, so we can safely | ||
| // short-circuit | ||
| ENVOY_LOG(trace, "request does not use v1 or v2 proxy protocol, forwarding as is"); | ||
| return ReadOrParseState::SkipFilter; | ||
| } | ||
| } | ||
|
|
||
| if (raw_slice.len_ >= PROXY_PROTO_V2_HEADER_LEN) { | ||
| const char* sig = PROXY_PROTO_V2_SIGNATURE; | ||
| if (!memcmp(buf, sig, PROXY_PROTO_V2_SIGNATURE_LEN)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,8 @@ | |
| #include "gmock/gmock.h" | ||
| #include "gtest/gtest.h" | ||
|
|
||
| using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V1_SIGNATURE_LEN; | ||
| using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V2_SIGNATURE_LEN; | ||
| using testing::_; | ||
| using testing::AnyNumber; | ||
| using testing::AtLeast; | ||
|
|
@@ -230,6 +232,78 @@ TEST_P(ProxyProtocolTest, V1Basic) { | |
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocol) { | ||
| // Allows a small request (less bytes than v1/v2 signature) through even though it doesn't use | ||
| // proxy protocol | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| std::string msg = "data"; | ||
| ASSERT_GT(PROXY_PROTO_V1_SIGNATURE_LEN, | ||
| msg.length()); // Ensure we attempt parsing byte by byte using `search_index_` | ||
| ASSERT_GT(PROXY_PROTO_V2_SIGNATURE_LEN, msg.length()); | ||
|
|
||
| write(msg); | ||
| expectData(msg); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV1First) { | ||
| // Allows a small request (less bytes than v1/v2 signature) through even though it doesn't use | ||
| // proxy protocol v1/v2 (but it does match parts of both signatures) | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| // First two bytes are proxy protocol v1, second two bytes are proxy protocol v2. | ||
| // This ensures our byte by byte parsing (`search_index_`) has persistence built-in to | ||
| // remember whether the previous bytes were also valid for the signature | ||
| std::string msg = "PR\r\n"; | ||
| ASSERT_GT(PROXY_PROTO_V1_SIGNATURE_LEN, msg.length()); | ||
| ASSERT_GT(PROXY_PROTO_V2_SIGNATURE_LEN, msg.length()); | ||
|
|
||
| write(msg); | ||
| expectData(msg); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV2First) { | ||
| // Allows a small request (less bytes than v1/v2 signature) through even though it doesn't use | ||
| // proxy protocol v1/v2 (but it does match parts of both signatures) | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| // First two bytes are proxy protocol v2, second two bytes are proxy protocol v1. | ||
| // This ensures our byte by byte parsing (`search_index_`) has persistence built-in to | ||
| // remember whether the previous bytes were also valid for the signature | ||
| std::string msg = "\r\nOX"; | ||
| ASSERT_GT(PROXY_PROTO_V1_SIGNATURE_LEN, msg.length()); | ||
| ASSERT_GT(PROXY_PROTO_V2_SIGNATURE_LEN, msg.length()); | ||
|
|
||
| write(msg); | ||
| expectData(msg); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, AllowLargeNoProxyProtocol) { | ||
| // Allows a large request (more bytes than v1/v2 signature) through even though it doesn't use | ||
| // proxy protocol | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| std::string msg = "more data more data more data"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can test the v2 case here, otherwise, I didn't see a difference between the previous test case, both of them are going to skip the header in the first bytes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we can, this test is for separate logic (it ensures we have this check) while the test prior ensures that we have this check |
||
| ASSERT_GT(msg.length(), | ||
| PROXY_PROTO_V2_HEADER_LEN); // Ensure we attempt parsing as v2 proxy protocol up front | ||
| // rather than parsing byte by byte using `search_index_` | ||
|
|
||
| write(msg); | ||
| expectData(msg); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V1Minimal) { | ||
| connect(); | ||
| write("PROXY UNKNOWN\r\nmore data"); | ||
|
|
@@ -526,6 +600,19 @@ TEST_P(ProxyProtocolTest, V2ShortV4) { | |
| expectProxyProtoError(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V2ShortV4WithAllowNoProxyProtocol) { | ||
| // An ipv4/tcp PROXY header that has incorrect addr-len encoded | ||
| constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, | ||
| 0x54, 0x0a, 0x21, 0x21, 0x00, 0x04, 0x00, 0x08, 0x00, 0x02, | ||
| 'm', 'o', 'r', 'e', ' ', 'd', 'a', 't', 'a'}; | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(false, &proto_config); | ||
|
|
||
| write(buffer, sizeof(buffer)); | ||
| expectProxyProtoError(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V2ShortAddrV4) { | ||
| // An ipv4/tcp connection that has insufficient header-length encoded | ||
| constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, | ||
|
|
@@ -609,6 +696,18 @@ TEST_P(ProxyProtocolTest, V1TooLong) { | |
| expectProxyProtoError(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V1TooLongWithAllowNoProxyProtocol) { | ||
| constexpr uint8_t buffer[] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}; | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(false, &proto_config); | ||
| write("PROXY TCP4 1.2.3.4 2.3.4.5 100 100"); | ||
| for (size_t i = 0; i < 256; i += sizeof(buffer)) { | ||
| write(buffer, sizeof(buffer)); | ||
| } | ||
| expectProxyProtoError(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V2ParseExtensions) { | ||
| // A well-formed ipv4/tcp with a pair of TLV extensions is accepted | ||
| constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, | ||
|
|
@@ -1035,6 +1134,52 @@ TEST_P(ProxyProtocolTest, PartialRead) { | |
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, PartialV1ReadWithAllowNoProxyProtocol) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also worth testing a case that we have correct v1 or v2 header signature, but invalid data in the rest of header, then ensure we disconnect the connection, and not let the connection passthrough.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed in 6031151 |
||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| write("PROXY TCP4"); // Intentionally larger than the size of v1 proxy protocol signature | ||
|
|
||
| dispatcher_->run(Event::Dispatcher::RunType::NonBlock); | ||
|
|
||
| write(" 254.254.2"); | ||
| write("54.254 1.2"); | ||
| write(".3.4 65535"); | ||
| write(" 1234\r\n..."); | ||
|
|
||
| expectData("..."); | ||
| EXPECT_EQ(server_connection_->connectionInfoProvider().remoteAddress()->ip()->addressAsString(), | ||
| "254.254.254.254"); | ||
| EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, TinyPartialV1ReadWithAllowNoProxyProtocol) { | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| write("PRO"); // Intentionally smaller than the size of v1 proxy protocol signature | ||
|
|
||
| dispatcher_->run(Event::Dispatcher::RunType::NonBlock); | ||
|
|
||
| write("XY TCP4 25"); | ||
|
|
||
| dispatcher_->run(Event::Dispatcher::RunType::NonBlock); | ||
|
|
||
| write("4.254.2"); | ||
| write("54.254 1.2"); | ||
| write(".3.4 65535"); | ||
| write(" 1234\r\n..."); | ||
|
|
||
| expectData("..."); | ||
| EXPECT_EQ(server_connection_->connectionInfoProvider().remoteAddress()->ip()->addressAsString(), | ||
| "254.254.254.254"); | ||
| EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, V2PartialRead) { | ||
| // A well-formed ipv4/tcp header, delivered with part of the signature, | ||
| // part of the header, rest of header + body | ||
|
|
@@ -1060,6 +1205,64 @@ TEST_P(ProxyProtocolTest, V2PartialRead) { | |
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, PartialV2ReadWithAllowNoProxyProtocol) { | ||
| // A well-formed ipv4/tcp header, delivered with part of the signature, | ||
| // part of the header, rest of header + body | ||
| constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, | ||
| 0x49, 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, | ||
| 0x03, 0x04, 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, | ||
| 0x02, 'm', 'o', 'r', 'e', 'd', 'a', 't', 'a'}; | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| // Using 18 intentionally as it is larger than v2 signature length and divides evenly into | ||
| // len(buffer) | ||
| auto buffer_incr_size = 18; | ||
| ASSERT_LT(PROXY_PROTO_V2_SIGNATURE_LEN, buffer_incr_size); | ||
| for (size_t i = 0; i < sizeof(buffer); i += buffer_incr_size) { | ||
| write(&buffer[i], buffer_incr_size); | ||
| if (i == 0) { | ||
| dispatcher_->run(Event::Dispatcher::RunType::NonBlock); | ||
| } | ||
| } | ||
|
|
||
| expectData("moredata"); | ||
| EXPECT_EQ(server_connection_->connectionInfoProvider().remoteAddress()->ip()->addressAsString(), | ||
| "1.2.3.4"); | ||
| EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); | ||
| disconnect(); | ||
| } | ||
|
|
||
| TEST_P(ProxyProtocolTest, TinyPartialV2ReadWithAllowNoProxyProtocol) { | ||
| // A well-formed ipv4/tcp header, delivered with part of the signature, | ||
| // part of the header, rest of header + body | ||
| constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, | ||
| 0x49, 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, | ||
| 0x03, 0x04, 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, | ||
| 0x02, 'm', 'o', 'r', 'e', 'd', 'a', 't', 'a'}; | ||
| envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; | ||
| proto_config.set_allow_requests_without_proxy_protocol(true); | ||
| connect(true, &proto_config); | ||
|
|
||
| // Using 3 intentionally as it is smaller than v2 signature length and divides evenly into | ||
| // len(buffer) | ||
| auto buffer_incr_size = 3; | ||
| ASSERT_GT(PROXY_PROTO_V2_SIGNATURE_LEN, buffer_incr_size); | ||
| for (size_t i = 0; i < sizeof(buffer); i += buffer_incr_size) { | ||
| write(&buffer[i], buffer_incr_size); | ||
| if (i == 0) { | ||
| dispatcher_->run(Event::Dispatcher::RunType::NonBlock); | ||
| } | ||
| } | ||
|
|
||
| expectData("moredata"); | ||
| EXPECT_EQ(server_connection_->connectionInfoProvider().remoteAddress()->ip()->addressAsString(), | ||
| "1.2.3.4"); | ||
| EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); | ||
| disconnect(); | ||
| } | ||
|
|
||
| const std::string ProxyProtocol = "envoy.filters.listener.proxy_protocol"; | ||
|
|
||
| TEST_P(ProxyProtocolTest, V2ParseExtensionsLargeThanInitMaxReadBytes) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.