diff options
-rw-r--r-- | lib/diameter/test/diameter_relay_SUITE.erl | 414 | ||||
-rw-r--r-- | lib/diameter/test/modules.mk | 3 |
2 files changed, 416 insertions, 1 deletions
diff --git a/lib/diameter/test/diameter_relay_SUITE.erl b/lib/diameter/test/diameter_relay_SUITE.erl new file mode 100644 index 0000000000..cd53fbfd90 --- /dev/null +++ b/lib/diameter/test/diameter_relay_SUITE.erl @@ -0,0 +1,414 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Tests of traffic between seven Diameter nodes connected as follows. +%% +%% --- SERVER1.REALM2 +%% / +%% ---- RELAY.REALM2 ---- SERVER2.REALM2 +%% / | +%% CLIENT.REALM1 | +%% \ | +%% ---- RELAY.REALM3 ---- SERVER2.REALM3 +%% \ +%% --- SERVER2.REALM3 +%% + +-module(diameter_relay_SUITE). + +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, + end_per_suite/1]). + +%% testcases +-export([send1/1, + send2/1, + send3/1, + send4/1, + send_loop/1, + send_timeout_1/1, + send_timeout_2/1, + remove_transports/1, + stop_services/1]). + +%% diameter callbacks +-export([peer_up/3, + peer_down/3, + pick_peer/4, + prepare_request/3, + prepare_retransmit/3, + handle_answer/4, + handle_error/4, + handle_request/3]). + +-ifdef(DIAMETER_CT). +-include("diameter_gen_base_rfc3588.hrl"). +-else. +-include_lib("diameter/include/diameter_gen_base_rfc3588.hrl"). +-endif. + +-include_lib("diameter/include/diameter.hrl"). +-include("diameter_ct.hrl"). + +%% =========================================================================== + +-define(ADDR, {127,0,0,1}). + +-define(CLIENT, "CLIENT.REALM1"). +-define(RELAY1, "RELAY.REALM2"). +-define(SERVER1, "SERVER1.REALM2"). +-define(SERVER2, "SERVER2.REALM2"). +-define(RELAY2, "RELAY.REALM3"). +-define(SERVER3, "SERVER1.REALM3"). +-define(SERVER4, "SERVER2.REALM3"). + +-define(DICT_COMMON, ?DIAMETER_DICT_COMMON). +-define(DICT_RELAY, ?DIAMETER_DICT_RELAY). + +-define(APP_ALIAS, the_app). +-define(APP_ID, ?DICT_COMMON:id()). + +%% Config for diameter:start_service/2. +-define(SERVICE(Host, Dict), + [{'Origin-Host', Host}, + {'Origin-Realm', realm(Host)}, + {'Host-IP-Address', [?ADDR]}, + {'Vendor-Id', 12345}, + {'Product-Name', "OTP/diameter"}, + {'Acct-Application-Id', [Dict:id()]}, + {application, [{alias, ?APP_ALIAS}, + {dictionary, Dict}, + {module, ?MODULE}, + {answer_errors, callback}]}]). + +%% Config for diameter:add_transport/2. In the listening case, listen +%% on a free port that we then lookup using the implementation detail +%% that diameter_tcp registers the port with diameter_reg. +-define(CONNECT(PortNr), + {connect, [{transport_module, diameter_tcp}, + {transport_config, [{raddr, ?ADDR}, + {rport, PortNr}, + {ip, ?ADDR}, + {port, 0}]}]}). +-define(LISTEN, + {listen, [{transport_module, diameter_tcp}, + {transport_config, [{ip, ?ADDR}, {port, 0}]}]}). + +-define(SUCCESS, 2001). +-define(LOOP_DETECTED, 3005). +-define(UNABLE_TO_DELIVER, 3002). + +-define(LOGOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_DIAMETER_LOGOUT'). +-define(AUTHORIZE_ONLY, ?'DIAMETER_BASE_RE-AUTH-REQUEST-TYPE_AUTHORIZE_ONLY'). + +-define(A, list_to_atom). +-define(L, atom_to_list). + +%% =========================================================================== + +suite() -> + [{timetrap, {seconds, 10}}]. + +all() -> + [{group, N} || {N, _, _} <- groups()] + ++ [remove_transports, stop_services]. + +groups() -> + Ts = tc(), + [{all, [], Ts}, + {p, [parallel], Ts}]. + +init_per_suite(Config) -> + ok = diameter:start(), + + dbg:tracer(port, dbg:trace_port(file, "relay.dbg")), + dbg:p(all,c), + dbg:tpl(diameter_service, x), + dbg:tp(?MODULE, x), + + + [S1,S2,S3,S4] = S = [server(N, ?DICT_COMMON) || N <- [?SERVER1, + ?SERVER2, + ?SERVER3, + ?SERVER4]], + [R1,R2] = R = [server(N, ?DICT_RELAY) || N <- [?RELAY1, ?RELAY2]], + + ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT, ?DICT_COMMON)), + + true = diameter:subscribe(?RELAY1), + true = diameter:subscribe(?RELAY2), + true = diameter:subscribe(?CLIENT), + + [C1,C2] = connect(?RELAY1, [S1,S2]), + [C3,C4] = connect(?RELAY2, [S3,S4]), + [C5,C6] = connect(?CLIENT, [R1,R2]), + + C7 = connect(?RELAY1, R2), + + [{transports, {S, R, [C1,C2,C3,C4,C5,C6,C7]}} | Config]. + +end_per_suite(_Config) -> + ok = diameter:stop(). + +%% Testcases to run when services are started and connections +%% established. +tc() -> + [send1, + send2, + send3, + send4, + send_loop, + send_timeout_1, + send_timeout_2]. + +server(Host, Dict) -> + ok = diameter:start_service(Host, ?SERVICE(Host, Dict)), + {ok, LRef} = diameter:add_transport(Host, ?LISTEN), + {LRef, portnr(LRef)}. + +connect(Host, {_LRef, PortNr}) -> + {ok, Ref} = diameter:add_transport(Host, ?CONNECT(PortNr)), + ok = receive + #diameter_event{service = Host, + info = {up, Ref, _, _, #diameter_packet{}}} -> + ok + after 2000 -> + false + end, + Ref; +connect(Host, Ports) -> + [connect(Host, P) || P <- Ports]. + +portnr(LRef) -> + portnr(LRef, 20). + +portnr(LRef, N) + when 0 < N -> + case diameter_reg:match({diameter_tcp, listener, {LRef, '_'}}) of + [{T, _Pid}] -> + {_, _, {LRef, {_Addr, LSock}}} = T, + {ok, PortNr} = inet:port(LSock), + PortNr; + [] -> + receive after 50 -> ok end, + portnr(LRef, N-1) + end. + +realm(Host) -> + tl(lists:dropwhile(fun(C) -> C /= $. end, Host)). + +%% =========================================================================== + +%% Send an STR intended for a specific server and expect success. +send1(_Config) -> + call(?SERVER1). +send2(_Config) -> + call(?SERVER2). +send3(_Config) -> + call(?SERVER3). +send4(_Config) -> + call(?SERVER4). + +%% Send an ASR that loops between the relays and expect the loop to +%% be detected. +send_loop(_Config) -> + Req = ['ASR', {'Destination-Realm', realm(?SERVER1)}, + {'Destination-Host', ?SERVER1}, + {'Auth-Application-Id', ?APP_ID}], + #'diameter_base_answer-message'{'Result-Code' = ?LOOP_DETECTED} + = call(Req, [{filter, realm}]). + +%% Send a RAR that is incorrectly routed and then discarded and expect +%% different results depending on whether or not we or the relay +%% timeout first. +send_timeout_1(_Config) -> + #'diameter_base_answer-message'{'Result-Code' = ?UNABLE_TO_DELIVER} + = send_timeout(7000). +send_timeout_2(_Config) -> + {error, timeout} = send_timeout(3000). + +send_timeout(Tmo) -> + Req = ['RAR', {'Destination-Realm', realm(?SERVER1)}, + {'Destination-Host', ?SERVER1}, + {'Auth-Application-Id', ?APP_ID}, + {'Re-Auth-Request-Type', ?AUTHORIZE_ONLY}], + call(Req, [{filter, realm}, {timeout, Tmo}]). + +%% Remove the client transports and expect the corresponding server +%% transport to go down. +remove_transports(Config) -> + {[S1,S2,S3,S4], [R1,R2], [C1,C2,C3,C4,C5,C6,C7]} + = proplists:get_value(transports, Config), + + true = diameter:subscribe(?SERVER1), + true = diameter:subscribe(?SERVER2), + true = diameter:subscribe(?SERVER3), + true = diameter:subscribe(?SERVER4), + true = diameter:subscribe(?RELAY1), + true = diameter:subscribe(?RELAY2), + + disconnect(S1, ?RELAY1, C1), + disconnect(S2, ?RELAY1, C2), + disconnect(S3, ?RELAY2, C3), + disconnect(S4, ?RELAY2, C4), + disconnect(R1, ?CLIENT, C5), + disconnect(R2, ?CLIENT, C6), + disconnect(R2, ?RELAY1, C7). + +disconnect({LRef, _PortNr}, Client, CRef) -> + ok = diameter:remove_transport(Client, CRef), + ok = receive #diameter_event{info = {down, LRef, _, _}} -> ok + after 2000 -> false + end. + +stop_services(_Config) -> + S = [?CLIENT, ?RELAY1, ?RELAY2, ?SERVER1, ?SERVER2, ?SERVER3, ?SERVER4], + Ok = [ok || _ <- S], + Ok = [diameter:stop_service(H) || H <- S]. + +%% =========================================================================== + +call(Server) -> + Realm = realm(Server), + Req = ['STR', {'Destination-Realm', Realm}, + {'Destination-Host', [Server]}, + {'Termination-Cause', ?LOGOUT}, + {'Auth-Application-Id', ?APP_ID}], + #diameter_base_STA{'Result-Code' = ?SUCCESS, + 'Origin-Host' = Server, + 'Origin-Realm' = Realm} + = call(Req, [{filter, realm}]). + +call(Req, Opts) -> + diameter:call(?CLIENT, ?APP_ALIAS, Req, Opts). + +set([H|T], Vs) -> + [H | Vs ++ T]. + +%% =========================================================================== +%% diameter callbacks + +%% peer_up/3 + +peer_up(_SvcName, _Peer, State) -> + State. + +%% peer_down/3 + +peer_down(_SvcName, _Peer, State) -> + State. + +%% pick_peer/4 + +pick_peer([Peer | _], _, Svc, _State) + when Svc == ?RELAY1; + Svc == ?RELAY2; + Svc == ?CLIENT-> + {ok, Peer}. + +%% prepare_request/3 + +prepare_request(Pkt, Svc, _Peer) + when Svc == ?RELAY1; + Svc == ?RELAY2 -> + {send, Pkt}; + +prepare_request(Pkt, ?CLIENT, {_Ref, Caps}) -> + {send, prepare(Pkt, Caps)}. + +prepare(#diameter_packet{msg = Req}, Caps) -> + #diameter_caps{origin_host = {OH, _}, + origin_realm = {OR, _}} + = Caps, + set(Req, [{'Session-Id', diameter:session_id(OH)}, + {'Origin-Host', OH}, + {'Origin-Realm', OR}]). + +%% prepare_retransmit/3 + +prepare_retransmit(_Pkt, false, _Peer) -> + discard. + +%% handle_answer/4 + +%% A relay must return Pkt. +handle_answer(Pkt, _Req, Svc, _Peer) + when Svc == ?RELAY1; + Svc == ?RELAY2 -> + Pkt; + +handle_answer(Pkt, _Req, ?CLIENT, _Peer) -> + #diameter_packet{msg = Rec, errors = []} = Pkt, + Rec. + +%% handle_error/4 + +handle_error(Reason, _Req, _Svc, _Peer) -> + {error, Reason}. + +%% handle_request/3 + +handle_request(Pkt, OH, {_Ref, #diameter_caps{origin_host = {OH,_}} = Caps}) + when OH /= ?CLIENT -> + request(Pkt, Caps). + +%% RELAY1 routes any ASR or RAR to RELAY2 ... +request(#diameter_packet{header = #diameter_header{cmd_code = C}}, + #diameter_caps{origin_host = {?RELAY1, _}}) + when C == 274; %% ASR + C == 258 -> %% RAR + {relay, [{filter, {realm, realm(?RELAY2)}}]}; + +%% ... which in turn routes it back. Expect diameter to either answer +%% either with DIAMETER_LOOP_DETECTED/DIAMETER_UNABLE_TO_COMPLY. +request(#diameter_packet{header = #diameter_header{cmd_code = 274}}, + #diameter_caps{origin_host = {?RELAY2, _}}) -> + {relay, [{filter, {host, ?RELAY1}}]}; +request(#diameter_packet{header = #diameter_header{cmd_code = 258}}, + #diameter_caps{origin_host = {?RELAY2, _}}) -> + discard; + +%% Other request to a relay: send on to one of the servers in the +%% same realm. +request(_Pkt, #diameter_caps{origin_host = {OH, _}}) + when OH == ?RELAY1; + OH == ?RELAY2 -> + {relay, [{filter, {all, [host, realm]}}]}; + +%% Request received by a server: answer. +request(#diameter_packet{msg = #diameter_base_STR{'Session-Id' = SId, + 'Origin-Host' = Host, + 'Origin-Realm' = Realm, + 'Route-Record' = Route}}, + #diameter_caps{origin_host = {OH, _}, + origin_realm = {OR, _}}) -> + %% The request should have the Origin-Host/Realm of the original + %% sender. + R = realm(?CLIENT), + {?CLIENT, R} = {Host, Realm}, + %% A relay appends the identity of the peer that a request was + %% received from to the Route-Record avp. + [?CLIENT] = Route, + {reply, #diameter_base_STA{'Result-Code' = ?SUCCESS, + 'Session-Id' = SId, + 'Origin-Host' = OH, + 'Origin-Realm' = OR}}. diff --git a/lib/diameter/test/modules.mk b/lib/diameter/test/modules.mk index f84ec45d52..c6f709dc36 100644 --- a/lib/diameter/test/modules.mk +++ b/lib/diameter/test/modules.mk @@ -33,7 +33,8 @@ MODULES = \ diameter_stats_SUITE \ diameter_watchdog_SUITE \ diameter_transport_SUITE \ - diameter_traffic_SUITE + diameter_traffic_SUITE \ + diameter_relay_SUITE INTERNAL_HRL_FILES = \ diameter_ct.hrl |