mod_net_proxy/mod_net_proxy.lua
changeset 2934 9a62780e7ee2
child 2935 e79b9a55aa2e
equal deleted inserted replaced
2933:3a104a900af1 2934: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 });