• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / esockd / 303

pending completion
303

Pull #178

github

Pull Request #178: Fix receiving proxy protocol information

21 of 21 new or added lines in 1 file covered. (100.0%)

723 of 1028 relevant lines covered (70.33%)

59.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.71
/src/esockd_proxy_protocol.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
3
%%
4
%% Licensed under the Apache License, Version 2.0 (the "License");
5
%% you may not use this file except in compliance with the License.
6
%% You may obtain a copy of the License at
7
%%
8
%%     http://www.apache.org/licenses/LICENSE-2.0
9
%%
10
%% Unless required by applicable law or agreed to in writing, software
11
%% distributed under the License is distributed on an "AS IS" BASIS,
12
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
%% See the License for the specific language governing permissions and
14
%% limitations under the License.
15
%%--------------------------------------------------------------------
16

17
%% @doc [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
18
-module(esockd_proxy_protocol).
19

20
-include("esockd.hrl").
21

22
-export([recv/3]).
23

24
-ifdef(TEST).
25
-export([get_proxy_attrs/1]).
26
-export([parse_v1/2, parse_v2/4, parse_pp2_tlv/2, parse_pp2_ssl/1]).
27
-endif.
28

29
%% Protocol Command
30
-define(LOCAL, 16#0).
31
-define(PROXY, 16#1).
32

33
%% Address families
34
-define(UNSPEC, 16#0).
35
-define(INET,   16#1).
36
-define(INET6,  16#2).
37
-define(UNIX,   16#3).
38

39
-define(STREAM, 16#1).
40
-define(DGRAM,  16#2).
41

42
-define(SPACE, 16#20).
43

44
-define(MAX_TIMEOUT, 17000).
45

46
%% Proxy Protocol Additional Fields
47
-define(PP2_TYPE_ALPN,           16#01).
48
-define(PP2_TYPE_AUTHORITY,      16#02).
49
-define(PP2_TYPE_CRC32C,         16#03).
50
-define(PP2_TYPE_NOOP,           16#04).
51
-define(PP2_TYPE_SSL,            16#20).
52
-define(PP2_SUBTYPE_SSL_VERSION, 16#21).
53
-define(PP2_SUBTYPE_SSL_CN,      16#22).
54
-define(PP2_SUBTYPE_SSL_CIPHER,  16#23).
55
-define(PP2_SUBTYPE_SSL_SIG_ALG, 16#24).
56
-define(PP2_SUBTYPE_SSL_KEY_ALG, 16#25).
57
-define(PP2_TYPE_NETNS,          16#30).
58

59
%% Protocol signature:
60
%% 16#0D,16#0A,16#00,16#0D,16#0A,16#51,16#55,16#49,16#54,16#0A
61
-define(SIG, "\r\n\0\r\nQUIT\n").
62

63
-type maybe_proxy_socket() :: #proxy_socket{} | inet:socket() | #ssl_socket{}.
19✔
64

19✔
65
-spec recv
19✔
66
    (esockd_socket, socket:socket(), timeout()) ->
19✔
67
        {ok, socket:socket()} | {error, term()};
68
    (module(), inet:socket() | #ssl_socket{}, timeout()) ->
69
        {ok, maybe_proxy_socket()} | {error, term()}.
4✔
70
recv(esockd_socket, Sock, Timeout) ->
4✔
71
    recv_esockd_socket(Sock, Timeout);
72
recv(Transport, Sock, Timeout) ->
73
    {ok, OriginalOpts} = Transport:getopts(Sock, [mode, active, packet]),
2✔
74
    case do_recv(Transport, Sock, Timeout) of
2✔
75
        {ok, _} = OkResult ->
76
            Transport:setopts(Sock, OriginalOpts),
77
            OkResult;
8✔
78
        {error, _} = Error ->
79
            Error
1✔
80
    end.
81

82
do_recv(Transport, Sock, Timeout) ->
83
    Deadline = deadline(Timeout),
1✔
84
    ok = Transport:setopts(Sock, [binary, {active, once}, {packet, line}]),
85
    receive
2✔
86
        %% V1 TCP
87
        {_, _Sock, <<"PROXY TCP", Proto, ?SPACE, ProxyInfo/binary>>} ->
88
            parse_v1(ProxyInfo, #proxy_socket{inet = inet_family(Proto), socket = Sock});
1✔
89
        %% V1 Unknown
90
        {_, _Sock, <<"PROXY UNKNOWN", _ProxyInfo/binary>>} ->
91
            {ok, Sock};
92
        %% V2 TCP
8✔
93
        {_, _Sock, <<"\r\n">>} ->
8✔
94
            Transport:setopts(Sock, [{active, false}, {packet, raw}]),
8✔
95
            recv_v2(Transport, Sock, Deadline);
96
        {tcp_error, _Sock, Reason} ->
7✔
97
            {error, {recv_proxy_info_error, Reason}};
7✔
98
        {tcp_closed, _Sock} ->
99
            %% socket closed before any data is received
6✔
100
            %% return an atom here to avoid error level logging
6✔
101
            {error, proxy_proto_close};
102
        {_, _Sock, ProxyInfo} ->
×
103
            {error, {invalid_proxy_info, ProxyInfo}}
104
    after
1✔
105
        Timeout ->
106
            {error, proxy_proto_timeout}
107
    end.
108

×
109
recv_esockd_socket(Sock, Timeout) ->
110
    Deadline = deadline(Timeout),
×
111
    case socket:recv(Sock, 2, [], Timeout) of
112
        {ok, <<"\r\n">>} ->
1✔
113
            recv_v2_esockd_socket(Sock, Deadline);
114
        {ok, <<"PR">>} ->
115
            recv_v1_esockd_socket(Sock, Deadline);
116
        {ok, Header} ->
117
            {error, {invalid_proxy_info, Header}};
118
        {error, Reason} ->
6✔
119
            map_tcpsocket_error(Reason)
6✔
120
    end.
6✔
121

6✔
122
recv_v1_esockd_socket(Sock, Deadline) ->
6✔
123
    case socket_recvline(Sock, _MaxLine = 108, Deadline) of
6✔
124
        %% NOTE: "PR" was already received.
125
        {ok, <<"OXY TCP", Proto, ?SPACE, ProxyInfo/binary>>} ->
126
            {ok, ProxySock} = parse_v1(ProxyInfo,
127
                                       #proxy_socket{inet = inet_family(Proto), socket = Sock}),
×
128
            ok = set_socket_meta(ProxySock),
129
            {ok, Sock};
130
        {ok, <<"OXY UNKNOWN", _ProxyInfo/binary>>} ->
131
            {ok, Sock};
8✔
132
        {ok, Header} ->
8✔
133
            {error, {invalid_proxy_info, <<"PR", Header/binary>>}};
134
        {error, Reason} ->
135
            map_tcpsocket_error(Reason)
136
    end.
137

138
recv_v2_esockd_socket(Sock, Deadline) ->
139
    case recv_v2(socket, Sock, Deadline) of
1✔
140
        {ok, ProxySock} ->
1✔
141
            ok = set_socket_meta(ProxySock),
142
            {ok, Sock};
143
        {error, Reason} ->
144
            {error, Reason}
145
    end.
×
146

147
socket_recvline(Sock, MaxLine, Deadline) ->
148
    case socket:recv(Sock, 0, [peek], timeout_left(Deadline)) of
8✔
149
        {ok, Bytes} ->
150
            MatchOpts = case byte_size(Bytes) of
1✔
151
                N when N > MaxLine -> [{scope, {0, MaxLine}}];
1✔
152
                _                  -> []
1✔
153
            end,
154
            case binary:match(Bytes, <<"\r\n">>, MatchOpts) of
155
                {Pos, _} ->
3✔
156
                    _ = socket:recv(Sock, Pos + 2, [], timeout_left(Deadline)),
157
                    {ok, binary:part(Bytes, {0, Pos})};
4✔
158
                nomatch when byte_size(Bytes) < MaxLine ->
159
                    socket_recvline(Sock, MaxLine, Deadline);
160
                nomatch ->
1✔
161
                    {error, {invalid_proxy_info, Bytes}}
162
            end;
1✔
163
        {error, Reason} ->
164
            map_tcpsocket_error(Reason)
×
165
    end.
166

1✔
167
map_tcpsocket_error(closed) ->
168
    {error, proxy_proto_close};
1✔
169
map_tcpsocket_error(timeout) ->
170
    {error, proxy_proto_timeout};
×
171
map_tcpsocket_error({Reason, _}) ->
172
    {error, {recv_proxy_info_error, Reason}};
173
map_tcpsocket_error(Reason) ->
174
    {error, {recv_proxy_info_error, Reason}}.
2✔
175

176
set_socket_meta(ProxySocket = #proxy_socket{socket = Sock}) ->
177
    socket:setopt(Sock, {otp, meta}, mk_proxy_attrs(ProxySocket)).
178

179
recv_v2(Transport, Sock, Deadline) ->
180
    with_remaining_timeout(Deadline, fun(HeaderTimeout) ->
181
        case Transport:recv(Sock, 14, HeaderTimeout) of
182
            {ok, <<?SIG, 2:4, Cmd:4, AF:4, Trans:4, Len:16>>} ->
183
                with_remaining_timeout(Deadline, fun(ProxyInfoTimeout) ->
184
                    case Transport:recv(Sock, Len, ProxyInfoTimeout) of
185
                        {ok, ProxyInfo} ->
186
                            parse_v2(Cmd, Trans, ProxyInfo, #proxy_socket{inet = inet_family(AF), socket = Sock});
187
                        {error, closed} ->
188
                            {error, proxy_proto_close};
189
                        {error, Reason} ->
190
                            {error, {recv_proxy_info_error, Reason}}
191
                    end
192
                end);
193
            {ok, UnknownHeader} ->
194
                {error, {invalid_proxy_info, UnknownHeader}};
195
            {error, closed} ->
196
                {error, proxy_proto_close};
×
197
            {error, Reason} ->
198
                {error, {recv_proxy_info_error, Reason}}
199
        end
200
    end).
201

202
mk_proxy_attrs(#proxy_socket{inet = Protocol,
×
203
                           src_addr = SrcAddr, dst_addr = DstAddr,
204
                           src_port = SrcPort, dst_port = DstPort,
×
205
                           pp2_additional_info = PP2Info}) ->
206
    #{proxy_protocol => Protocol,
×
207
      proxy_src_addr => SrcAddr, proxy_dst_addr => DstAddr,
208
      proxy_src_port => SrcPort, proxy_dst_port => DstPort,
×
209
      proxy_pp2_info => PP2Info}.
210

4✔
211
-ifdef(TEST).
212

2✔
213
-spec get_proxy_attrs(maybe_proxy_socket() | socket:socket()) -> map().
×
214
get_proxy_attrs(ProxySocket = #proxy_socket{}) ->
215
    mk_proxy_attrs(ProxySocket);
216
get_proxy_attrs(Socket) when element(1, Socket) =:= '$socket' ->
3✔
217
    case socket:getopt(Socket, {otp, meta}) of
1✔
218
        {ok, Meta = #{}} ->
219
            Meta;
220
        _Otherwise ->
×
221
            #{}
6✔
222
    end;
×
223
get_proxy_attrs(_Socket) ->
×
224
    #{}.
225

4✔
226
-endif.
2✔
227

228
parse_v1(ProxyInfo, ProxySock) ->
229
    [SrcAddrBin, DstAddrBin, SrcPortBin, DstPortBin]
19✔
230
        = binary:split(ProxyInfo, [<<" ">>, <<"\r\n">>], [global, trim]),
231
    {ok, SrcAddr} = inet:parse_address(binary_to_list(SrcAddrBin)),
232
    {ok, DstAddr} = inet:parse_address(binary_to_list(DstAddrBin)),
2✔
233
    SrcPort = list_to_integer(binary_to_list(SrcPortBin)),
234
    DstPort = list_to_integer(binary_to_list(DstPortBin)),
13✔
235
    {ok, ProxySock#proxy_socket{src_addr = SrcAddr, dst_addr = DstAddr,
236
                                src_port = SrcPort, dst_port = DstPort}}.
237

15✔
238
parse_v2(?LOCAL, _Trans, _ProxyInfo, #proxy_socket{socket = Sock}) ->
239
    {ok, Sock};
2✔
240

241
parse_v2(?PROXY, ?STREAM, ProxyInfo, ProxySock = #proxy_socket{inet = inet4}) ->
13✔
242
    <<A:8, B:8, C:8, D:8, W:8, X:8, Y:8, Z:8,
243
      SrcPort:16, DstPort:16, AdditionalBytes/binary>> = ProxyInfo,
×
244
    parse_pp2_additional(AdditionalBytes, ProxySock#proxy_socket{
245
        src_addr = {A, B, C, D}, src_port = SrcPort,
246
        dst_addr = {W, X, Y, Z}, dst_port = DstPort});
247

248
parse_v2(?PROXY, ?STREAM, ProxyInfo, ProxySock = #proxy_socket{inet = inet6}) ->
249
    <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16,
250
      R:16, S:16, T:16, U:16, V:16, W:16, X:16, Y:16,
251
      SrcPort:16, DstPort:16, AdditionalBytes/binary>> = ProxyInfo,
252
    parse_pp2_additional(AdditionalBytes, ProxySock#proxy_socket{
253
        src_addr = {A, B, C, D, E, F, G, H}, src_port = SrcPort,
254
        dst_addr = {R, S, T, U, V, W, X, Y}, dst_port = DstPort});
255

256
parse_v2(_, _, _, #proxy_socket{socket = _Sock}) ->
257
    {error, unsupported_proto_v2}.
258

259
parse_pp2_additional(<<>>, ProxySock) ->
260
    {ok, ProxySock};
261
parse_pp2_additional(Bytes, ProxySock) when is_binary(Bytes) ->
262
    IgnoreGuard = fun(?PP2_TYPE_NOOP) -> false; (_Type) -> true end,
263
    AdditionalInfo = parse_pp2_tlv(fun pp2_additional_field/1, Bytes, IgnoreGuard),
264
    {ok, ProxySock#proxy_socket{pp2_additional_info = AdditionalInfo}}.
265

266
parse_pp2_tlv(Fun, Bytes) ->
267
    parse_pp2_tlv(Fun, Bytes, fun(_Any) -> true end).
268
parse_pp2_tlv(Fun, Bytes, Guard) ->
269
    [Fun({Type, Val}) || <<Type:8, Len:16, Val:Len/binary>> <= Bytes, Guard(Type)].
270

271
pp2_additional_field({?PP2_TYPE_ALPN, PP2_ALPN}) ->
272
    {pp2_alpn, PP2_ALPN};
273
pp2_additional_field({?PP2_TYPE_AUTHORITY, PP2_AUTHORITY}) ->
274
    {pp2_authority, PP2_AUTHORITY};
275
pp2_additional_field({?PP2_TYPE_CRC32C, PP2_CRC32C}) ->
276
    {pp2_crc32c, PP2_CRC32C};
277
pp2_additional_field({?PP2_TYPE_NETNS, PP2_NETNS}) ->
278
    {pp2_netns, PP2_NETNS};
279
pp2_additional_field({?PP2_TYPE_SSL, PP2_SSL}) ->
280
    {pp2_ssl, parse_pp2_ssl(PP2_SSL)};
281
pp2_additional_field({Field, Value}) ->
282
    {{pp2_raw, Field}, Value}.
283

284
parse_pp2_ssl(<<_Unused:5, PP2_CLIENT_CERT_SESS:1, PP2_CLIENT_CERT_CONN:1, PP2_CLIENT_SSL:1,
285
                PP2_SSL_VERIFY:32, SubFields/bitstring>>) ->
286
    [
287
     %% The PP2_CLIENT_SSL flag indicates that the client connected over SSL/TLS. When
288
     %% this field is present, the US-ASCII string representation of the TLS version is
289
     %% appended at the end of the field in the TLV format using the type PP2_SUBTYPE_SSL_VERSION.
290
     {pp2_ssl_client, bool(PP2_CLIENT_SSL)},
291

292
     %% PP2_CLIENT_CERT_CONN indicates that the client provided a certificate over the
293
     %% current connection.
294
     {pp2_ssl_client_cert_conn, bool(PP2_CLIENT_CERT_CONN)},
295

296
     %% PP2_CLIENT_CERT_SESS indicates that the client provided a
297
     %% certificate at least once over the TLS session this connection belongs to.
298
     {pp2_ssl_client_cert_sess, bool(PP2_CLIENT_CERT_SESS)},
299

300
     %% The <verify> field will be zero if the client presented a certificate
301
     %% and it was successfully verified, and non-zero otherwise.
302
     {pp2_ssl_verify, ssl_certificate_verified(PP2_SSL_VERIFY)}
303

304
     | parse_pp2_tlv(fun pp2_additional_ssl_field/1, SubFields)
305
    ].
306

307
pp2_additional_ssl_field({?PP2_SUBTYPE_SSL_VERSION, PP2_SSL_VERSION}) ->
308
    {pp2_ssl_version, PP2_SSL_VERSION};
309

310
%% In all cases, the string representation (in UTF8) of the Common Name field
311
%% (OID: 2.5.4.3) of the client certificate's Distinguished Name, is appended
312
%% using the TLV format and the type PP2_SUBTYPE_SSL_CN. E.g. "example.com".
313
pp2_additional_ssl_field({?PP2_SUBTYPE_SSL_CN, PP2_SSL_CN}) ->
314
    {pp2_ssl_cn, PP2_SSL_CN};
315
pp2_additional_ssl_field({?PP2_SUBTYPE_SSL_CIPHER, PP2_SSL_CIPHER}) ->
316
    {pp2_ssl_cipher, PP2_SSL_CIPHER};
317
pp2_additional_ssl_field({?PP2_SUBTYPE_SSL_SIG_ALG, PP2_SSL_SIG_ALG}) ->
318
    {pp2_ssl_sig_alg, PP2_SSL_SIG_ALG};
319
pp2_additional_ssl_field({?PP2_SUBTYPE_SSL_KEY_ALG, PP2_SSL_KEY_ALG}) ->
320
    {pp2_ssl_key_alg, PP2_SSL_KEY_ALG};
321
pp2_additional_ssl_field({Field, Val}) ->
322
    {{pp2_ssl_raw, Field}, Val}.
323

324
ssl_certificate_verified(0) -> success;
325
ssl_certificate_verified(_) -> failed.
326

327
%% V1
328
inet_family($4) -> inet4;
329
inet_family($6) -> inet6;
330

331
%% V2
332
inet_family(?UNSPEC) -> unspec;
333
inet_family(?INET)   -> inet4;
334
inet_family(?INET6)  -> inet6;
335
inet_family(?UNIX)   -> unix.
336

337
bool(1) -> true;
338
bool(_) -> false.
339

340
maybe_limit_timeout(infinity) -> ?MAX_TIMEOUT;
341
maybe_limit_timeout(Timeout) when is_integer(Timeout) andalso Timeout > ?MAX_TIMEOUT ->
342
    ?MAX_TIMEOUT;
343
maybe_limit_timeout(Timeout) when is_integer(Timeout) andalso Timeout > 0 ->
344
    Timeout.
345

346
deadline(Timeout) ->
347
    erlang:monotonic_time(millisecond) + maybe_limit_timeout(Timeout).
348

349
timeout_left(Deadline) ->
350
    Deadline - erlang:monotonic_time(millisecond).
351

352
with_remaining_timeout(Timer, Fun) ->
353
    case timeout_left(Timer) of
354
        Timeout when Timeout > 0 ->
355
            Fun(Timeout);
356
        _ ->
357
            {error, proxy_proto_timeout}
358
    end.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc