Mercurial > 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 |
| parents | |
| children | 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 }); |
