Patch for ejabberd 2.0.0 or SVN r1277
Original announcement: http://xram.livejournal.com/2887.html
Original also hosted in: http://www.ejabberd.im/s2s_proxy

Original 1.1.3 patch by Xram.
Adapted to ejabberd 2.0.0 by Badlop on 6 Apr 2008.

diff --git a/doc/guide.tex b/doc/guide.tex
index 4178294..9702ef8 100644
--- a/doc/guide.tex
+++ b/doc/guide.tex
@@ -1296,6 +1296,68 @@ Examples:
 \end{verbatim}
 \end{itemize}
 
+
+\subsection{S2S-proxy}
+\label{s2sproxy}
+\ind{options!s2sproxy}\ind{s2sproxy}
+
+It is possible to configure an ejabberd server to act as S2S-proxy server or as S2S-proxy client.
+
+This feature will be illustrated with an example scenario
+with two ejabberd servers: \term{server1.example.org} and \term{server2.example.org}.
+Both servers allow client and server connections.
+Both servers can connect to each other using S2S.
+But only server1 is connected to Internet;
+while server2 can't connect to Internet because a firewall blocks him the port 5269.
+You want to allow users in server2 to connect to Jabber servers outside your LAN.
+
+In this setup, server1 will act as a S2S-proxy server,
+and server2 as a S2S-proxy client.
+So, when a user in server2 wants to connect to a remote Jabber server in Internet,
+server2 will send all the traffic over server1, and this one to and from Internet.
+
+Note that there isn't DNS address translation or JID rewrite.
+The DNS names of the proxy clients and proxy server must be resolvable.
+It will not work correctly if using local DNS names.
+The S2S connections of server2 will be proxied by server1,
+but the JID will remain intact.
+This means that server2 DNS name must point to server1
+so remote servers can reach the S2S-proxy server.
+
+Configuration in ejabberd.cfg of server1:
+\begin{verbatim}
+{hosts, ["server1.example.org"]}.
+
+{modules,
+ [
+  %% Enable the module and define the proxy clients
+  {mod_s2s_proxy, [{hosts, ["server2.example.org"]}]},
+  ...
+ ]
+}.
+\end{verbatim}
+
+Configuration in ejabberd.cfg of server2:
+\begin{verbatim}
+{hosts, ["server2.example.org"]}.
+
+%%
+%% Define local domains that are inside the LAN and don't require proxying
+%%
+{acl, local_domains, {server_glob, "*.example.org"}}.
+
+%%
+%% Don't relay internal s2s connections via the proxy server
+%%
+{access, via_proxy, [{deny, local_domains}, {allow, all}]}.
+
+%%
+%% s2s_hub: define the proxy server and Access rule
+%%
+{s2s_hub, [{host, "server1.example.org"}, {access, via_proxy}]}.
+\end{verbatim}
+
+
 \subsection{Include Additional Configuration Files}
 \label{includeconfigfile}
 \ind{options!includeconfigfile}\ind{includeconfigfile}
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index a585b39..bf17ef9 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -324,6 +324,8 @@ process_term(Term, State) ->
 	    add_option(s2s_use_starttls, Port, State);
 	{s2s_certfile, CertFile} ->
 	    add_option(s2s_certfile, CertFile, State);
+	{s2s_hub, Server} ->
+		add_option(s2s_hub, Server, State);
 	{domain_certfile, Domain, CertFile} ->
 	    add_option({domain_certfile, Domain}, CertFile, State);
 	{node_type, NodeType} ->
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index c72af5a..644e962 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -38,6 +38,7 @@
 	 remove_connection/3,
 	 dirty_get_connections/0,
 	 allow_host/2,
+	 find_connection/2,
 	 ctl_process/2
 	]).
 
@@ -261,9 +262,37 @@ do_route(From, To, Packet) ->
 	    false
     end.
 
+%% @spec find_proxy(DestinationHost) -> direct | {via, ProxyServer}
+%%       DestinationHost = jid()
+%%       ProxyServer = string()
+find_proxy(To) ->
+    case ejabberd_config:get_local_option(s2s_hub) of
+	Opts when is_list(Opts) ->
+	    LTo = To#jid.lserver,
+	    case gen_mod:get_opt(host, Opts, LTo) of
+		ProxyServer when LTo /= ProxyServer ->
+		    ACL = gen_mod:get_opt(access, Opts, all),
+		    case acl:match_rule(global, ACL, To) of
+			allow ->
+			    {via, ProxyServer};
+			deny ->
+			    direct
+		    end;
+		_ ->
+		    direct
+	    end;
+	_ ->
+	    direct
+    end.
+
+
 find_connection(From, To) ->
     #jid{lserver = MyServer} = From,
-    #jid{lserver = Server} = To,
+    #jid{lserver = ToServer} = To,
+    Server = case find_proxy(To) of
+		 direct -> ToServer;
+		 {via, ProxyServer} -> ProxyServer
+	     end,
     FromTo = {MyServer, Server},
     MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo),
     MaxS2SConnectionsNumberPerNode =
diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl
index 6233398..8bc16f8 100644
--- a/src/ejabberd_s2s_in.erl
+++ b/src/ejabberd_s2s_in.erl
@@ -70,6 +70,7 @@
 		tls_enabled = false,
 		tls_options = [],
 		authenticated = false,
+		proxy,
 		auth_domain,
 	        connections = ?DICT:new(),
 		timer}).
@@ -156,12 +157,19 @@ init([{SockMod, Socket}, Opts]) ->
 		  CertFile ->
 		      [{certfile, CertFile}]
 	      end,
+    Proxy = case ejabberd_config:get_local_option(s2s_hub) of
+		OptList when is_list(OptList) ->
+		    gen_mod:get_opt(host, OptList, none);
+		_ ->
+		    none
+	    end,
     Timer = erlang:start_timer(?S2STIMEOUT, self(), []),
     {ok, wait_for_stream,
      #state{socket = Socket,
 	    sockmod = SockMod,
 	    streamid = new_id(),
 	    shaper = Shaper,
+	    proxy = Proxy,
 	    tls = StartTLS,
 	    tls_enabled = false,
 	    tls_options = TLSOpts,
@@ -400,40 +408,29 @@ stream_established({xmlstreamelement, El}, StateData) ->
 		(To /= error) and (From /= error) ->
 		    LFrom = From#jid.lserver,
 		    LTo = To#jid.lserver,
+		    ProxyServer = StateData#state.proxy,
+		    MyDomains = ejabberd_router:dirty_get_all_domains(),
+		    AuthDomain = StateData#state.auth_domain,
 		    if
 			StateData#state.authenticated ->
-			    case (LFrom == StateData#state.auth_domain)
-				andalso
-				lists:member(
-				  LTo,
-				  ejabberd_router:dirty_get_all_domains()) of
+			    case lists:member(LTo, MyDomains) of
 				true ->
-				    if ((Name == "iq") or
-					(Name == "message") or
-					(Name == "presence")) ->
-					    ejabberd_router:route(
-					      From, To, NewEl);
-				       true ->
+				    case lists:member(AuthDomain, [LFrom, ProxyServer]) of
+					true ->
+					    route_stanza(Name, From, To, NewEl);
+					false ->
 					    error
 				    end;
 				false ->
-				    error
+				    case lists:member(LFrom, MyDomains) of
+					true ->
+					    route_stanza(Name, From, To, NewEl);
+					false ->
+					    error
+				    end
 			    end;
 			true ->
-			    case ?DICT:find({LFrom, LTo},
-					    StateData#state.connections) of
-				{ok, established} ->
-				    if ((Name == "iq") or
-					(Name == "message") or
-					(Name == "presence")) ->
-					    ejabberd_router:route(
-					      From, To, NewEl);
-				       true ->
-					    error
-				    end;
-				_ ->
-				    error
-			    end
+			    error
 		    end;
 		true ->
 		    error
@@ -454,7 +451,9 @@ stream_established({valid, From, To}, StateData) ->
     LTo = jlib:nameprep(To),
     NSD = StateData#state{
 	    connections = ?DICT:store({LFrom, LTo}, established,
-				      StateData#state.connections)},
+				      StateData#state.connections),
+		auth_domain = LFrom,
+		authenticated = true},
     {next_state, stream_established, NSD};
 
 stream_established({invalid, From, To}, StateData) ->
@@ -564,6 +563,14 @@ send_text(StateData, Text) ->
 send_element(StateData, El) ->
     send_text(StateData, xml:element_to_string(El)).
 
+route_stanza(Name, From, To, El) ->
+    if ((Name == "iq") or
+	(Name == "message") or
+	(Name == "presence")) ->
+	    ejabberd_router:route(From, To, El);
+       true ->
+	    error
+    end.
 
 change_shaper(StateData, Host, JID) ->
     Shaper = acl:match_rule(Host, StateData#state.shaper, JID),
diff --git a/src/mod_s2s_proxy.erl b/src/mod_s2s_proxy.erl
new file mode 100644
index 0000000..5d1c39d
--- /dev/null
+++ b/src/mod_s2s_proxy.erl
@@ -0,0 +1,96 @@
+%%%----------------------------------------------------------------------
+%%% File    : mod_s2s_proxy.erl
+%%% Author  : Evgeniy Khramtsov <xram@jabber.ru>
+%%% Purpose : S2S-proxy server
+%%% Created : 13 Aug 2006 by Evgeniy Khramtsov <xram@jabber.ru>
+%%%----------------------------------------------------------------------
+
+-module(mod_s2s_proxy).
+-author('xram@jabber.ru').
+
+-behaviour(gen_mod).
+-behaviour(gen_server).
+
+%% gen_server callbacks.
+-export([init/1,
+	 handle_info/2,
+	 handle_call/3,
+	 handle_cast/2,
+	 terminate/2,
+	 code_change/3
+	]).
+
+%% gen_mod callbacks.
+-export([start/2, stop/1]).
+
+%% API.
+-export([start_link/2]).
+
+-define(PROCNAME, ?MODULE).
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+
+-record(state, {host, routes}).
+
+%% Unused callbacks.
+handle_cast(_Request, State) ->
+    {noreply, State}.
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+%% -----
+
+start(Host, Opts) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    ChildSpec = {
+      Proc, {?MODULE, start_link, [Host, Opts]},
+      transient, 5000, worker, [?MODULE]
+     },
+    supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    gen_server:call(Proc, stop),
+    supervisor:terminate_child(ejabberd_sup, Proc),
+    supervisor:delete_child(ejabberd_sup, Proc).
+
+start_link(Host, Opts) ->
+    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+    gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+init([Host, Opts]) ->
+    case gen_mod:get_opt(hosts, Opts, []) of
+	RelayRoutes when is_list(RelayRoutes) ->
+	    ejabberd_router:register_routes(RelayRoutes),
+	    {ok, #state{routes=RelayRoutes, host=Host}};
+	Junk ->
+	    ?ERROR_MSG("Incorrect configuration of relay domains: ~p", [Junk]),
+	    error
+    end.
+
+handle_info({route, From, To, Packet}, #state{host=Host} = State) ->
+    MyHost = From#jid{lserver=Host, server=Host},
+    %% FIXME: Exporting find_connection/2 from ejabberd_s2s.erl
+    %% looks like copy-pasting hack, but what can we do?
+    case ejabberd_s2s:find_connection(MyHost, To) of
+	{atomic, Pid} when is_pid(Pid) ->
+	    {xmlelement, Name, Attrs, Els} = Packet,
+	    NewAttrs = jlib:replace_from_to_attrs(jlib:jid_to_string(From),
+						  jlib:jid_to_string(To),
+						  Attrs),
+	    Pid ! {send_element, {xmlelement, Name, NewAttrs, Els}};
+	{aborted, _Reason} ->
+	    ?DEBUG("delivery failed: ~p~n", [_Reason])
+    end,
+    {noreply, State};
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+handle_call(stop, _From, State) ->
+    {stop, normal, ok, State};
+handle_call(_Request, _From, State) ->
+    {reply, ok, State}.
+
+terminate(_Reason, #state{routes = RelayRoutes}) ->
+    ejabberd_router:unregister_routes(RelayRoutes),
+    ok.
