Software /
code /
prosody-modules
Comparison
mod_net_proxy/mod_net_proxy.lua @ 2930:9a62780e7ee2
mod_net_proxy: New module implementing PROXY protocol versions 1 and 2
author | Pascal Mathis <mail@pascalmathis.com> |
---|---|
date | Thu, 15 Mar 2018 15:26:30 +0100 |
child | 2931:e79b9a55aa2e |
comparison
equal
deleted
inserted
replaced
2929:3a104a900af1 | 2930:9a62780e7ee2 |
---|---|
1 -- mod_net_proxy.lua | |
2 -- Copyright (C) 2018 Pascal Mathis <mail@pascalmathis.com> | |
3 -- | |
4 -- Implementation of PROXY protocol versions 1 and 2 | |
5 -- Specifications: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt | |
6 | |
7 module:set_global(); | |
8 | |
9 -- Imports | |
10 local softreq = require "util.dependencies".softreq; | |
11 local bit = assert(softreq "bit" or softreq "bit32", "No bit module found. See https://prosody.im/doc/depends#bitop"); | |
12 local hex = require "util.hex"; | |
13 local ip = require "util.ip"; | |
14 local net = require "util.net"; | |
15 local portmanager = require "core.portmanager"; | |
16 | |
17 -- Utility Functions | |
18 local function _table_invert(input) | |
19 local output = {}; | |
20 for key, value in pairs(input) do | |
21 output[value] = key; | |
22 end | |
23 return output; | |
24 end | |
25 | |
26 -- Constants | |
27 local ADDR_FAMILY = { UNSPEC = 0x0, INET = 0x1, INET6 = 0x2, UNIX = 0x3 }; | |
28 local ADDR_FAMILY_STR = _table_invert(ADDR_FAMILY); | |
29 local TRANSPORT = { UNSPEC = 0x0, STREAM = 0x1, DGRAM = 0x2 }; | |
30 local TRANSPORT_STR = _table_invert(TRANSPORT); | |
31 | |
32 local PROTO_MAX_HEADER_LENGTH = 256; | |
33 local PROTO_HANDLERS = { | |
34 PROXYv1 = { signature = hex.from("50524F5859"), callback = nil }, | |
35 PROXYv2 = { signature = hex.from("0D0A0D0A000D0A515549540A"), callback = nil } | |
36 }; | |
37 local PROTO_HANDLER_STATUS = { SUCCESS = 0, POSTPONE = 1, FAILURE = 2 }; | |
38 | |
39 -- Persistent In-Memory Storage | |
40 local sessions = {}; | |
41 local mappings = {}; | |
42 | |
43 -- Proxy Data Methods | |
44 local proxy_data_mt = {}; proxy_data_mt.__index = proxy_data_mt; | |
45 | |
46 function proxy_data_mt:describe() | |
47 return string.format("proto=%s/%s src=%s:%d dst=%s:%d", | |
48 self:addr_family_str(), self:transport_str(), self:src_addr(), self:src_port(), self:dst_addr(), self:dst_port()); | |
49 end | |
50 | |
51 function proxy_data_mt:addr_family_str() | |
52 return ADDR_FAMILY_STR[self._addr_family] or ADDR_FAMILY_STR[ADDR_FAMILY.UNSPEC]; | |
53 end | |
54 | |
55 function proxy_data_mt:transport_str() | |
56 return TRANSPORT_STR[self._transport] or TRANSPORT_STR[TRANSPORT.UNSPEC]; | |
57 end | |
58 | |
59 function proxy_data_mt:version() | |
60 return self._version; | |
61 end | |
62 | |
63 function proxy_data_mt:addr_family() | |
64 return self._addr_family; | |
65 end | |
66 | |
67 function proxy_data_mt:transport() | |
68 return self._transport; | |
69 end | |
70 | |
71 function proxy_data_mt:src_addr() | |
72 return self._src_addr; | |
73 end | |
74 | |
75 function proxy_data_mt:src_port() | |
76 return self._src_port; | |
77 end | |
78 | |
79 function proxy_data_mt:dst_addr() | |
80 return self._dst_addr; | |
81 end | |
82 | |
83 function proxy_data_mt:dst_port() | |
84 return self._dst_port; | |
85 end | |
86 | |
87 -- Protocol Handler Functions | |
88 PROTO_HANDLERS["PROXYv1"].callback = function(conn, session) | |
89 local addr_family_mappings = { TCP4 = ADDR_FAMILY.INET, TCP6 = ADDR_FAMILY.INET6 }; | |
90 | |
91 -- Postpone processing if CRLF (PROXYv1 header terminator) does not exist within buffer | |
92 if session.buffer:find("\r\n") == nil then | |
93 return PROTO_HANDLER_STATUS.POSTPONE, nil; | |
94 end | |
95 | |
96 -- Declare header pattern and match current buffer against pattern | |
97 local header_pattern = "^PROXY (%S+) (%S+) (%S+) (%d+) (%d+)\r\n"; | |
98 local addr_family, src_addr, dst_addr, src_port, dst_port = session.buffer:match(header_pattern); | |
99 src_port, dst_port = tonumber(src_port), tonumber(dst_port); | |
100 | |
101 -- Ensure that header was successfully parsed and contains a valid address family | |
102 if addr_family == nil or src_addr == nil or dst_addr == nil or src_port == nil or dst_port == nil then | |
103 module:log("warn", "Received unparseable PROXYv1 header from %s", conn:ip()); | |
104 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
105 end | |
106 if addr_family_mappings[addr_family] == nil then | |
107 module:log("warn", "Received invalid PROXYv1 address family from %s: %s", conn:ip(), addr_family); | |
108 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
109 end | |
110 | |
111 -- Ensure that received source and destination ports are within 1 and 65535 (0xFFFF) | |
112 if src_port <= 0 or src_port >= 0xFFFF then | |
113 module:log("warn", "Received invalid PROXYv1 source port from %s: %d", conn:ip(), src_port); | |
114 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
115 end | |
116 if dst_port <= 0 or dst_port >= 0xFFFF then | |
117 module:log("warn", "Received invalid PROXYv1 destination port from %s: %d", conn:ip(), dst_port); | |
118 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
119 end | |
120 | |
121 -- Ensure that received source and destination address can be parsed | |
122 local _, err = ip.new_ip(src_addr); | |
123 if err ~= nil then | |
124 module:log("warn", "Received unparseable PROXYv1 source address from %s: %s", conn:ip(), src_addr); | |
125 end | |
126 _, err = ip.new_ip(dst_addr); | |
127 if err ~= nil then | |
128 module:log("warn", "Received unparseable PROXYv1 destination address from %s: %s", conn:ip(), dst_addr); | |
129 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
130 end | |
131 | |
132 -- Strip parsed header from session buffer and build proxy data | |
133 session.buffer = session.buffer:gsub(header_pattern, ""); | |
134 | |
135 local proxy_data = { | |
136 _version = 1, | |
137 _addr_family = addr_family, _transport = TRANSPORT.STREAM, | |
138 _src_addr = src_addr, _src_port = src_port, | |
139 _dst_addr = dst_addr, _dst_port = dst_port | |
140 }; | |
141 setmetatable(proxy_data, proxy_data_mt); | |
142 | |
143 -- Return successful response with gathered proxy data | |
144 return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; | |
145 end | |
146 | |
147 PROTO_HANDLERS["PROXYv2"].callback = function(conn, session) | |
148 -- Postpone processing if less than 16 bytes are available | |
149 if #session.buffer < 16 then | |
150 return PROTO_HANDLER_STATUS.POSTPONE, nil; | |
151 end | |
152 | |
153 -- Parse first 16 bytes of protocol header | |
154 local version = bit.rshift(bit.band(session.buffer:byte(13), 0xF0), 4); | |
155 local command = bit.band(session.buffer:byte(13), 0x0F); | |
156 local addr_family = bit.rshift(bit.band(session.buffer:byte(14), 0xF0), 4); | |
157 local transport = bit.band(session.buffer:byte(14), 0x0F); | |
158 local length = bit.bor(session.buffer:byte(16), bit.lshift(session.buffer:byte(15), 8)); | |
159 | |
160 -- Postpone processing if less than 16+<length> bytes are available | |
161 if #session.buffer < 16 + length then | |
162 return PROTO_HANDLER_STATUS.POSTPONE, nil; | |
163 end | |
164 | |
165 -- Ensure that version number is correct | |
166 if version ~= 0x2 then | |
167 module:log("error", "Received unsupported PROXYv2 version from %s: %d", conn:ip(), version); | |
168 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
169 end | |
170 | |
171 local payload = session.buffer:sub(17); | |
172 if command == 0x0 then | |
173 -- Gather source/destination addresses and ports from local socket | |
174 local src_addr, src_port = conn:socket():getpeername(); | |
175 local dst_addr, dst_port = conn:socket():getsockname(); | |
176 | |
177 -- Build proxy data based on real connection information | |
178 local proxy_data = { | |
179 _version = version, | |
180 _addr_family = addr_family, _transport = transport, | |
181 _src_addr = src_addr, _src_port = src_port, | |
182 _dst_addr = dst_addr, _dst_port = dst_port | |
183 }; | |
184 setmetatable(proxy_data, proxy_data_mt); | |
185 | |
186 -- Return successful response with gathered proxy data | |
187 return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; | |
188 elseif command == 0x1 then | |
189 local offset = 1; | |
190 local src_addr, src_port, dst_addr, dst_port; | |
191 | |
192 -- Verify transport protocol is either STREAM or DGRAM | |
193 if transport ~= TRANSPORT.STREAM and transport ~= TRANSPORT.DGRAM then | |
194 module:log("warn", "Received unsupported PROXYv2 transport from %s: 0x%02X", conn:ip(), transport); | |
195 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
196 end | |
197 | |
198 -- Parse source and destination addresses | |
199 if addr_family == ADDR_FAMILY.INET then | |
200 src_addr = net.ntop(payload:sub(offset, offset + 3)); offset = offset + 4; | |
201 dst_addr = net.ntop(payload:sub(offset, offset + 3)); offset = offset + 4; | |
202 elseif addr_family == ADDR_FAMILY.INET6 then | |
203 src_addr = net.ntop(payload:sub(offset, offset + 15)); offset = offset + 16; | |
204 dst_addr = net.ntop(payload:sub(offset, offset + 15)); offset = offset + 16; | |
205 elseif addr_family == ADDR_FAMILY.UNIX then | |
206 src_addr = payload:sub(offset, offset + 107); offset = offset + 108; | |
207 dst_addr = payload:sub(offset, offset + 107); offset = offset + 108; | |
208 end | |
209 | |
210 -- Parse source and destination ports | |
211 if addr_family == ADDR_FAMILY.INET or addr_family == ADDR_FAMILY.INET6 then | |
212 src_port = bit.bor(payload:byte(offset + 1), bit.lshift(payload:byte(offset), 8)); offset = offset + 2; | |
213 -- luacheck: ignore 311 | |
214 dst_port = bit.bor(payload:byte(offset + 1), bit.lshift(payload:byte(offset), 8)); offset = offset + 2; | |
215 end | |
216 | |
217 -- Strip parsed header from session buffer and build proxy data | |
218 session.buffer = session.buffer:sub(17 + length); | |
219 | |
220 local proxy_data = { | |
221 _version = version, | |
222 _addr_family = addr_family, _transport = transport, | |
223 _src_addr = src_addr, _src_port = src_port, | |
224 _dst_addr = dst_addr, _dst_port = dst_port | |
225 }; | |
226 setmetatable(proxy_data, proxy_data_mt); | |
227 | |
228 -- Return successful response with gathered proxy data | |
229 return PROTO_HANDLER_STATUS.SUCCESS, proxy_data; | |
230 else | |
231 module:log("error", "Received unsupported PROXYv2 command from %s: 0x%02X", conn:ip(), command); | |
232 return PROTO_HANDLER_STATUS.FAILURE, nil; | |
233 end | |
234 end | |
235 | |
236 -- Wrap an existing connection with the provided proxy data. This will override several methods of the 'conn' object to | |
237 -- return the proxied source instead of the source which initiated the TCP connection. Afterwards, the listener of the | |
238 -- connection gets set according to the globally defined port<>service mappings and the methods 'onconnect' and | |
239 -- 'onincoming' are being called manually with the current session buffer. | |
240 local function wrap_proxy_connection(conn, session, proxy_data) | |
241 -- Override and add functions of 'conn' object when source information has been collected | |
242 conn.proxyip, conn.proxyport = conn.ip, conn.port; | |
243 if proxy_data:src_addr() ~= nil and proxy_data:src_port() ~= nil then | |
244 conn.ip = function() | |
245 return proxy_data:src_addr(); | |
246 end | |
247 conn.port = function() | |
248 return proxy_data:src_port(); | |
249 end | |
250 conn.clientport = conn.port; | |
251 end | |
252 | |
253 -- Attempt to find service by processing port<>service mappings | |
254 local mapping = mappings[conn:serverport()]; | |
255 if mapping == nil then | |
256 conn:close(); | |
257 module:log("error", "Connection %s@%s terminated: Could not find mapping for port %d", | |
258 conn:ip(), conn:proxyip(), conn:serverport()); | |
259 return; | |
260 end | |
261 | |
262 if mapping.service == nil then | |
263 local service = portmanager.get_service(mapping.service_name); | |
264 | |
265 if service ~= nil then | |
266 mapping.service = service; | |
267 else | |
268 conn:close(); | |
269 module:log("error", "Connection %s@%s terminated: Could not process mapping for unknown service %s", | |
270 conn:ip(), conn:proxyip(), mapping.service_name); | |
271 return; | |
272 end | |
273 end | |
274 | |
275 -- Pass connection to actual service listener and simulate onconnect/onincoming callbacks | |
276 local service_listener = mapping.service.listener; | |
277 | |
278 module:log("info", "Passing proxied connection %s:%d to service %s", conn:ip(), conn:port(), mapping.service_name); | |
279 conn:setlistener(service_listener); | |
280 if service_listener.onconnect then | |
281 service_listener.onconnect(conn); | |
282 end | |
283 return service_listener.onincoming(conn, session.buffer); | |
284 end | |
285 | |
286 -- Network Listener Methods | |
287 local listener = {}; | |
288 | |
289 function listener.onconnect(conn) | |
290 sessions[conn] = { | |
291 handler = nil; | |
292 buffer = nil; | |
293 }; | |
294 end | |
295 | |
296 function listener.onincoming(conn, data) | |
297 -- Abort processing if no data has been received | |
298 if not data then | |
299 return; | |
300 end | |
301 | |
302 -- Lookup session for connection and append received data to buffer | |
303 local session = sessions[conn]; | |
304 session.buffer = session.buffer and session.buffer .. data or data; | |
305 | |
306 -- Attempt to determine protocol handler if not done previously | |
307 if session.handler == nil then | |
308 -- Match current session buffer against all known protocol signatures to determine protocol handler | |
309 for handler_name, handler in pairs(PROTO_HANDLERS) do | |
310 if session.buffer:find("^" .. handler.signature) ~= nil then | |
311 session.handler = handler.callback; | |
312 module:log("debug", "Detected %s connection from %s:%d", handler_name, conn:ip(), conn:port()); | |
313 break; | |
314 end | |
315 end | |
316 | |
317 -- Decide between waiting for a complete header signature or terminating the connection when no handler has been found | |
318 if session.handler == nil then | |
319 -- Terminate connection if buffer size has exceeded tolerable maximum size | |
320 if #session.buffer > PROTO_MAX_HEADER_LENGTH then | |
321 conn:close(); | |
322 module:log("warn", "Connection %s:%d terminated: No valid PROXY header within %d bytes", | |
323 conn:ip(), conn:port(), PROTO_MAX_HEADER_LENGTH); | |
324 end | |
325 | |
326 -- Skip further processing without a valid protocol handler | |
327 module:log("debug", "No valid header signature detected from %s:%d, waiting for more data...", | |
328 conn:ip(), conn:port()); | |
329 return; | |
330 end | |
331 end | |
332 | |
333 -- Execute proxy protocol handler and process response | |
334 local response, proxy_data = session.handler(conn, session); | |
335 if response == PROTO_HANDLER_STATUS.SUCCESS then | |
336 module:log("info", "Received PROXY header from %s: %s", conn:ip(), proxy_data:describe()); | |
337 return wrap_proxy_connection(conn, session, proxy_data); | |
338 elseif response == PROTO_HANDLER_STATUS.POSTPONE then | |
339 module:log("debug", "Postponed parsing of incomplete PROXY header received from %s", conn:ip()); | |
340 return; | |
341 elseif response == PROTO_HANDLER_STATUS.FAILURE then | |
342 conn:close(); | |
343 module:log("warn", "Connection %s terminated: Could not process PROXY header from client, " + | |
344 "see previous log messages.", conn:ip()); | |
345 return; | |
346 else | |
347 -- This code should be never reached, but is included for completeness | |
348 conn:close(); | |
349 module:log("error", "Connection terminated: Received invalid protocol handler response with code %d", response); | |
350 return; | |
351 end | |
352 end | |
353 | |
354 function listener.ondisconnect(conn) | |
355 sessions[conn] = nil; | |
356 end | |
357 | |
358 listener.ondetach = listener.ondisconnect; | |
359 | |
360 -- Initialize the module by processing all configured port mappings | |
361 local config_ports = module:get_option_set("proxy_ports", {}); | |
362 local config_mappings = module:get_option("proxy_port_mappings", {}); | |
363 for port in config_ports do | |
364 if config_mappings[port] ~= nil then | |
365 mappings[port] = { | |
366 service_name = config_mappings[port], | |
367 service = nil | |
368 }; | |
369 else | |
370 module:log("warn", "No port<>service mapping found for port: %d", port); | |
371 end | |
372 end | |
373 | |
374 -- Register the previously declared network listener | |
375 module:provides("net", { | |
376 name = "proxy"; | |
377 listener = listener; | |
378 }); |