Skip to content

Commit d5f2771

Browse files
committed
Add unit tests for PROXY Protocol v1 parser
1 parent d80f726 commit d5f2771

File tree

4 files changed

+224
-13
lines changed

4 files changed

+224
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ lib/perl/lib/Apache/TS.pm
9494

9595
iocore/net/test_certlookup
9696
iocore/net/test_UDPNet
97+
iocore/net/test_libinknet
9798
iocore/net/quic/test_QUIC*
9899
iocore/aio/test_AIO
99100
iocore/eventsystem/test_IOBuffer

iocore/net/Makefile.am

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ AM_CPPFLAGS += \
3737

3838
TESTS = $(check_PROGRAMS)
3939

40-
check_PROGRAMS = test_certlookup test_UDPNet
40+
check_PROGRAMS = test_certlookup test_UDPNet test_libinknet
4141
noinst_LIBRARIES = libinknet.a
4242

4343
test_certlookup_LDFLAGS = \
@@ -85,6 +85,36 @@ test_UDPNet_SOURCES = \
8585
libinknet_stub.cc \
8686
test_I_UDPNet.cc
8787

88+
test_libinknet_SOURCES = \
89+
unit_tests/test_ProxyProtocol.cc
90+
91+
test_libinknet_CPPFLAGS = \
92+
$(AM_CPPFLAGS) \
93+
$(iocore_include_dirs) \
94+
-I$(abs_top_srcdir)/tests/include \
95+
-I$(abs_top_srcdir)/proxy \
96+
-I$(abs_top_srcdir)/proxy/hdrs \
97+
-I$(abs_top_srcdir)/proxy/http \
98+
-I$(abs_top_srcdir)/proxy/logging \
99+
-I$(abs_top_srcdir)/mgmt \
100+
-I$(abs_top_srcdir)/mgmt/utils \
101+
@OPENSSL_INCLUDES@
102+
103+
test_libinknet_LDFLAGS = \
104+
@AM_LDFLAGS@ \
105+
@OPENSSL_LDFLAGS@ \
106+
@YAMLCPP_LDFLAGS@
107+
108+
test_libinknet_LDADD = \
109+
libinknet.a \
110+
$(top_builddir)/iocore/eventsystem/libinkevent.a \
111+
$(top_builddir)/mgmt/libmgmt_p.la \
112+
$(top_builddir)/lib/records/librecords_p.a \
113+
$(top_builddir)/src/tscore/libtscore.la \
114+
$(top_builddir)/src/tscpp/util/libtscpputil.la \
115+
$(top_builddir)/proxy/ParentSelectionStrategy.o \
116+
@HWLOC_LIBS@ @OPENSSL_LIBS@ @LIBPCRE@ @YAMLCPP_LIBS@
117+
88118
libinknet_a_SOURCES = \
89119
ALPNSupport.cc \
90120
BIO_fastopen.cc \

iocore/net/ProxyProtocol.cc

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ constexpr ts::TextView PPv2_CONNECTION_PREFACE = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x
3939
constexpr size_t PPv1_CONNECTION_HEADER_LEN_MIN = 15;
4040
constexpr size_t PPv2_CONNECTION_HEADER_LEN_MIN = 16;
4141

42+
constexpr ts::TextView PPv1_PROTO_UNKNOWN = "UNKNOWN"sv;
43+
constexpr ts::TextView PPv1_PROTO_TCP4 = "TCP4"sv;
44+
constexpr ts::TextView PPv1_PROTO_TCP6 = "TCP6"sv;
45+
4246
/**
4347
PROXY Protocol v1 Parser
4448
@@ -47,15 +51,21 @@ constexpr size_t PPv2_CONNECTION_HEADER_LEN_MIN = 16;
4751
size_t
4852
proxy_protocol_v1_parse(ProxyProtocol *pp_info, ts::TextView hdr)
4953
{
50-
// Find the terminating newline
54+
ink_release_assert(hdr.size() >= PPv1_CONNECTION_HEADER_LEN_MIN);
55+
56+
// Find the terminating newline
5157
ts::TextView::size_type pos = hdr.find('\n');
5258
if (pos == hdr.npos) {
53-
Debug("proxyprotocol_v1", "ssl_has_proxy_v1: newline not found");
59+
Debug("proxyprotocol_v1", "ssl_has_proxy_v1: LF not found");
60+
return 0;
61+
}
62+
63+
if (hdr[pos - 1] != '\r') {
64+
Debug("proxyprotocol_v1", "ssl_has_proxy_v1: CR not found");
5465
return 0;
5566
}
5667

5768
ts::TextView token;
58-
in_port_t port;
5969

6070
// All the cases are special and sequence, might as well unroll them.
6171

@@ -69,8 +79,28 @@ proxy_protocol_v1_parse(ProxyProtocol *pp_info, ts::TextView hdr)
6979
Debug("proxyprotocol_v1", "proxy_protov1_parse: [%.*s] = PREFACE", static_cast<int>(token.size()), token.data());
7080

7181
// The INET protocol family - TCP4, TCP6 or UNKNOWN
72-
token = hdr.split_prefix_at(' ');
73-
if (0 == token.size()) {
82+
if (PPv1_PROTO_UNKNOWN.isPrefixOf(hdr)) {
83+
Debug("proxyprotocol_v1", "proxy_protov1_parse: [UNKNOWN] = INET Family");
84+
85+
// Ignore anything presented before the CRLF
86+
pp_info->version = ProxyProtocolVersion::V1;
87+
88+
return pos + 1;
89+
} else if (PPv1_PROTO_TCP4.isPrefixOf(hdr)) {
90+
token = hdr.split_prefix_at(' ');
91+
if (0 == token.size()) {
92+
return 0;
93+
}
94+
95+
pp_info->ip_family = AF_INET;
96+
} else if (PPv1_PROTO_TCP6.isPrefixOf(hdr)) {
97+
token = hdr.split_prefix_at(' ');
98+
if (0 == token.size()) {
99+
return 0;
100+
}
101+
102+
pp_info->ip_family = AF_INET6;
103+
} else {
74104
return 0;
75105
}
76106
Debug("proxyprotocol_v1", "proxy_protov1_parse: [%.*s] = INET Family", static_cast<int>(token.size()), token.data());
@@ -104,26 +134,29 @@ proxy_protocol_v1_parse(ProxyProtocol *pp_info, ts::TextView hdr)
104134
}
105135
Debug("proxyprotocol_v1", "proxy_protov1_parse: [%.*s] = Source Port", static_cast<int>(token.size()), token.data());
106136

107-
if (0 == (port = ts::svtoi(token))) {
108-
Debug("proxyprotocol_v1", "proxy_protov1_parse: src port [%d] token [%.*s] failed to parse", port,
137+
in_port_t src_port;
138+
if (0 == (src_port = ts::svtoi(token))) {
139+
Debug("proxyprotocol_v1", "proxy_protov1_parse: src port [%d] token [%.*s] failed to parse", src_port,
109140
static_cast<int>(token.size()), token.data());
110141
return 0;
111142
}
112-
pp_info->src_addr.port() = htons(port);
143+
pp_info->src_addr.port() = htons(src_port);
113144

114145
// Next is the TCP destination port represented as a decimal number in the range of [0..65535] inclusive.
115146
// Final trailer is CR LF so split at CR.
116147
token = hdr.split_prefix_at('\r');
117-
if (0 == token.size()) {
148+
if (0 == token.size() || token.find(0x20) != token.npos) {
118149
return 0;
119150
}
120151
Debug("proxyprotocol_v1", "proxy_protov1_parse: [%.*s] = Destination Port", static_cast<int>(token.size()), token.data());
121-
if (0 == (port = ts::svtoi(token))) {
122-
Debug("proxyprotocol_v1", "proxy_protov1_parse: dst port [%d] token [%.*s] failed to parse", port,
152+
153+
in_port_t dst_port;
154+
if (0 == (dst_port = ts::svtoi(token))) {
155+
Debug("proxyprotocol_v1", "proxy_protov1_parse: dst port [%d] token [%.*s] failed to parse", dst_port,
123156
static_cast<int>(token.size()), token.data());
124157
return 0;
125158
}
126-
pp_info->dst_addr.port() = htons(port);
159+
pp_info->dst_addr.port() = htons(dst_port);
127160

128161
pp_info->version = ProxyProtocolVersion::V1;
129162

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/** @file
2+
3+
Catch based unit tests for PROXY Protocol
4+
5+
@section license License
6+
7+
Licensed to the Apache Software Foundation (ASF) under one
8+
or more contributor license agreements. See the NOTICE file
9+
distributed with this work for additional information
10+
regarding copyright ownership. The ASF licenses this file
11+
to you under the Apache License, Version 2.0 (the
12+
"License"); you may not use this file except in compliance
13+
with the License. You may obtain a copy of the License at
14+
15+
http://www.apache.org/licenses/LICENSE-2.0
16+
17+
Unless required by applicable law or agreed to in writing, software
18+
distributed under the License is distributed on an "AS IS" BASIS,
19+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
See the License for the specific language governing permissions and
21+
limitations under the License.
22+
*/
23+
24+
#define CATCH_CONFIG_MAIN
25+
#include "catch.hpp"
26+
27+
#include "ProxyProtocol.h"
28+
29+
using namespace std::literals;
30+
31+
TEST_CASE("PROXY Protocol v1 Parser", "[ProxyProtocol][ProxyProtocolv1]")
32+
{
33+
IpEndpoint src_addr;
34+
IpEndpoint dst_addr;
35+
36+
SECTION("TCP over IPv4")
37+
{
38+
ts::TextView raw_data = "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv;
39+
40+
ProxyProtocol pp_info;
41+
REQUIRE(proxy_protocol_parse(&pp_info, raw_data) == raw_data.size());
42+
43+
REQUIRE(ats_ip_pton("192.0.2.1:50000", src_addr) == 0);
44+
REQUIRE(ats_ip_pton("198.51.100.1:443", dst_addr) == 0);
45+
46+
CHECK(pp_info.version == ProxyProtocolVersion::V1);
47+
CHECK(pp_info.ip_family == AF_INET);
48+
CHECK(pp_info.src_addr == src_addr);
49+
CHECK(pp_info.dst_addr == dst_addr);
50+
}
51+
52+
SECTION("TCP over IPv6")
53+
{
54+
ts::TextView raw_data = "PROXY TCP6 2001:0DB8:0:0:0:0:0:1 2001:0DB8:0:0:0:0:0:2 50000 443\r\n"sv;
55+
56+
ProxyProtocol pp_info;
57+
REQUIRE(proxy_protocol_parse(&pp_info, raw_data) == raw_data.size());
58+
59+
REQUIRE(ats_ip_pton("[2001:0DB8:0:0:0:0:0:1]:50000", src_addr) == 0);
60+
REQUIRE(ats_ip_pton("[2001:0DB8:0:0:0:0:0:2]:443", dst_addr) == 0);
61+
62+
CHECK(pp_info.version == ProxyProtocolVersion::V1);
63+
CHECK(pp_info.ip_family == AF_INET6);
64+
CHECK(pp_info.src_addr == src_addr);
65+
CHECK(pp_info.dst_addr == dst_addr);
66+
}
67+
68+
SECTION("UNKNOWN connection (short form)")
69+
{
70+
ts::TextView raw_data = "PROXY UNKNOWN\r\n"sv;
71+
72+
ProxyProtocol pp_info;
73+
REQUIRE(proxy_protocol_parse(&pp_info, raw_data) == raw_data.size());
74+
75+
CHECK(pp_info.version == ProxyProtocolVersion::V1);
76+
CHECK(pp_info.ip_family == AF_UNSPEC);
77+
}
78+
79+
SECTION("UNKNOWN connection (worst case)")
80+
{
81+
ts::TextView raw_data =
82+
"PROXY UNKNOWN ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n"sv;
83+
84+
ProxyProtocol pp_info;
85+
REQUIRE(proxy_protocol_parse(&pp_info, raw_data) == raw_data.size());
86+
87+
CHECK(pp_info.version == ProxyProtocolVersion::V1);
88+
CHECK(pp_info.ip_family == AF_UNSPEC);
89+
}
90+
91+
SECTION("Malformed Headers")
92+
{
93+
ProxyProtocol pp_info;
94+
95+
// lack of some fields
96+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4"sv) == 0);
97+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1"sv) == 0);
98+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1\r\n"sv) == 0);
99+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1\r\n"sv) == 0);
100+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 \r\n"sv) == 0);
101+
102+
// invalid preface
103+
CHECK(proxy_protocol_parse(&pp_info, "PROX TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
104+
CHECK(proxy_protocol_parse(&pp_info, "PROXZ TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
105+
106+
// invalid transport protocol & address family
107+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP1 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
108+
CHECK(proxy_protocol_parse(&pp_info, "PROXY UDP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
109+
110+
// extra space
111+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
112+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
113+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
114+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
115+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r\n"sv) == 0);
116+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443 \r\n"sv) == 0);
117+
118+
// invalid CRLF
119+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443"sv) == 0);
120+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\n"sv) == 0);
121+
CHECK(proxy_protocol_parse(&pp_info, "PROXY TCP4 192.0.2.1 198.51.100.1 50000 443\r"sv) == 0);
122+
}
123+
}
124+
125+
TEST_CASE("PROXY Protocol v2 Parser", "[ProxyProtocol][ProxyProtocolv2]")
126+
{
127+
SECTION("TCP over IPv4")
128+
{
129+
uint8_t raw_data[] = {
130+
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, ///< sig
131+
0x55, 0x49, 0x54, 0x0A, ///<
132+
0x02, ///< ver_vmd
133+
0x11, ///< fam
134+
0x00, 0x0C, ///< len
135+
0xC0, 0x00, 0x02, 0x01, ///< src_addr
136+
0xC6, 0x33, 0x64, 0x01, ///< dst_addr
137+
0xC3, 0x50, ///< src_port
138+
0x01, 0xBB, ///< dst_port
139+
};
140+
141+
ts::TextView tv(reinterpret_cast<char *>(raw_data), sizeof(raw_data));
142+
143+
ProxyProtocol pp_info;
144+
// TODO: add test when implemented. Just checking this doesn't crash for now
145+
REQUIRE(proxy_protocol_parse(&pp_info, tv) == 0);
146+
}
147+
}

0 commit comments

Comments
 (0)