mod_onions / mod_onions.lua

module:set_global();

local wrapclient = require "net.server".wrapclient;
local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
local initialize_filters = require "util.filters".initialize;
local bit = require "bit32";
local st = require "util.stanza";
local portmanager = require "core.portmanager";
local initialize_filters = require "util.filters".initialize;

-- Configuration, todo: actual configuration
local proxy_ip = "127.0.0.1";
local proxy_port = "9050";

local sessions = module:shared("sessions");

-- The socks5listener handles connection while still connecting to the proxy,
-- then it hands them over to the normal listener (in mod_s2s)
local socks5listener = { default_port = 9050, default_mode = "*a", default_interface = "*" };

local function socks5_connect_sent(conn, data)
	
	local session = sessions[conn];

	if #data < 5 then
		module:log("debug", "Did not receive a full reply, waiting.");
		session.socks5_buffer = data
		return
	end

	request_status = string.byte(data, 2);

	if not request_status == 0x00 then
		module:log("debug", "Failed to connect to the SOCKS5 proxy. :(");
		return;
	end

	module:log("debug", "Succesfully connected over SOCKS5");
	
	local response = string.byte(data, 4);

	module:log("debug", "Response (2): "..response);

	-- see if we should connect somewhere else
	if response == 0x03 then

		-- this means the server tells us to connect on a hostname
		local len = string.byte(data, 5);

		if #data < 6+len+2 then
			-- let's try again when we have enough
			module:log("debug", "Did not receive a full hostname reply, waiting.");
			session.socks5_buffer = data
			return
		end

		local hostname = string.byte(data, 6, 6+len);
		local port = bit.band(string.byte(data, 6+len+1), bit.lshift(string.byte(data, 6+len+2), 8));
		module:log("debug", "Should connect to: "..hostname..":"..port);

		-- TODO: connect on the other host instead
	elseif response == 0x01 then

		if #data < 10 then
			-- let's try again when we have enough
			module:log("debug", "Did not receive a full IP address reply, waiting.");
			session.socks5_buffer = data
			return
		end

		-- this means the server tells us to connect on an IPv4 address
		local ip1 = string.byte(data, 5);
		local ip2 = string.byte(data, 6);
		local ip3 = string.byte(data, 7);
		local ip4 = string.byte(data, 8);
		local port = bit.band(string.byte(data, 9), bit.lshift(string.byte(data, 10), 8));
		module:log("debug", "Should connect to: "..ip1.."."..ip2.."."..ip3.."."..ip4..":"..port);

		if not (ip1 == 0 and ip2 == 0 and ip3 == 0 and ip4 == 0 and port == 0) then
			module:log("debug", "The SOCKS5 proxy tells us to connect to a different IP, don't know how. :(");
			return
		end

		-- Now the real s2s listener can take over the connection.
		local listener = portmanager.get_service("s2s").listener;

		module:log("debug", "SOCKS5 done, handing over listening to "..tostring(listener));

		conn.setlistener(conn, listener);

		listener.register_outgoing(conn, session);

		session.socks5_handler = nil;
		session.socks5_buffer = nil;

		initialize_filters(session);

		local w, log = conn.send, session.log;

		local filter = initialize_filters(session);

		session.sends2s = function (t)
			log("debug", "sending (socks5): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
			if t.name then
				t = filter("stanzas/out", t);
			end
			if t then
				t = filter("bytes/out", tostring(t));
				if t then
					return w(conn, tostring(t));
				end
			end
		end

		session.openstream = function ()
			session.sends2s(st.stanza("stream:stream", {
				xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
				["xmlns:stream"]='http://etherx.jabber.org/streams',
				from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
		end
		
		listener.onconnect(conn);
	end
end

local function socks5_handshake_sent(conn, data)

	local session = sessions[conn];

	if #data < 2 then
		module:log("debug", "Did not receive a full reply, waiting.");
		session.socks5_buffer = data
		return
	end

	-- version, method
	local request_status = string.byte(data, 2);

	module:log("debug", "SOCKS version: "..string.byte(data, 1));
	module:log("debug", "Response: "..request_status);

	if not request_status == 0x00 then
		module:log("debug", "Failed to connect to the SOCKS5 proxy. :( It seems to require authentication.");
		return;
	end

	module:log("debug", "Sending connect message: 05 01 00 03 " .. #session.socks5_to .. " " .. session.socks5_port);

	-- version 5, connect, (reserved), type: domainname, (length, hostname), port
	query = "\05\01\00\03"..string.char(#session.socks5_to)..session.socks5_to..string.char(bit.rshift(session.socks5_port, 8))..string.char(bit.band(session.socks5_port, 0xff));

	conn:send(query);

	session.socks5_handler = socks5_connect_sent;
end

function socks5listener.onconnect(conn)
	module:log("debug", "Connected to SOCKS5 proxy.");

	module:log("debug", "Sending SOCKS5 handshake.");

	-- Socks version 5, 1 method, no auth
	local query = '\05\01\00';

	conn:send(query);
	sessions[conn].socks5_handler = socks5_handshake_sent;
end

function socks5listener.register_outgoing(conn, session)
	session.direction = "outgoing";
	sessions[conn] = session;
	sessions[conn].socks5status = "new";
end

function socks5listener.ondisconnect(conn, err)
	module:log("debug", "Closing connection to SOCKS5 proxy.")
	local session = sessions[conn];
	sessions[conn]  = nil;
end

function socks5listener.onincoming(conn, data)
	module:log("debug", "Received something from SOCKS5 proxy, "..sessions[conn].socks5status);

	local session = sessions[conn];

	if session.socks5_buffer then
		data = session.socks5_buffer .. data
	end

	if session.socks5_handler then
		session.socks5_handler(conn, data);
	end
end

local function connect_socks5(host_session, connect_host, connect_port)
	
	local conn, handler = socket.tcp();

	module:log("debug", "Connecting to " .. connect_host .. ":" .. connect_port);

	-- this is not necessarily the same as .to_host (it can be that this is a SRV record)
	host_session.socks5_to = connect_host;
	host_session.socks5_port = connect_port;

	conn:settimeout(0);

	local success, err = conn:connect(proxy_ip, proxy_port);

	conn = wrapclient(conn, connect_host, connect_port, socks5listener, "*a");

	socks5listener.register_outgoing(conn, host_session);

	host_session.conn = conn;
end

-- There's two signals that are handled: pre-try-connect (which I added) and route/remote.
-- route/remote gets called when routing anything to a remote server, so if that already matches *.onion, intercept it
-- if the server has a hostname, but a SRV record with a *.onion address, pre-try-connect will intercept it.

module:hook("pre-try-connect", function (event)
	module:log("debug", "Wrapping connection attempt to " .. event.connect_host .. ":" .. event.connect_port);

	local connect_host = event.connect_host;

	if connect_host:find(".onion(.?)$") then

		if string.sub(connect_host, -1) == "." then
			connect_host = string.sub(connect_host, 1, -2);
		end

		module:log("debug", "It's an onion, intercepting it.");

		connect_socks5(event.host_session, connect_host, event.connect_port);

		return true;
	end

	return false;
end, 100);

local function route_to_onion(event)

	if not event.to_host:find(".onion(.?)$") then
		module:log("debug", event.to_host .. " is not an onion. Not doing anything.");
		return false;
	end

	module:log("debug", "Onion routing something to ".. event.to_host);

	if hosts[event.from_host].s2sout[event.to_host] then
		module:log("debug", "Host session exists, no need to set up SOCKS5.")
		return;
	end

	local host_session = s2s_new_outgoing(event.from_host, event.to_host);

	hosts[event.from_host].s2sout[event.to_host] = host_session;

	connect_socks5(host_session, event.to_host, 5269);

	return false;
end

function module.add_host(module)
	module:hook("route/remote", route_to_onion, 200);
end
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.