Add M3UA implementation to Osmo SS7

The current code is not tested yet, and it has no eunit tests.
diff --git a/ebin/osmo_ss7.app b/ebin/osmo_ss7.app
index b444d97..9f0a643 100644
--- a/ebin/osmo_ss7.app
+++ b/ebin/osmo_ss7.app
@@ -6,6 +6,7 @@
 			bssmap_codec,
 			isup_codec,
 			m2ua_codec,
+			m3ua_codec, m3ua_core,
 			mtp3_codec,
 			sccp_codec, sccp_scoc,  sccp_scrc,
 			osmo_ss7_gtt,
diff --git a/include/m3ua.hrl b/include/m3ua.hrl
new file mode 100644
index 0000000..a33e153
--- /dev/null
+++ b/include/m3ua.hrl
@@ -0,0 +1,72 @@
+% M3UA in accordance with RFC4666 (http://tools.ietf.org/html/rfc4666)
+
+-define(M3UA_MSGC_MGMT,			0).	% Management
+-define(M3UA_MSGC_TRANSFER,		1).	% Transfer
+-define(M3UA_MSGC_SSNM,			2).	% SS7 Signalling Network Management
+-define(M3UA_MSGC_ASPSM,		3).	% ASP State Management
+-define(M3UA_MSGC_ASPTM,		4).	% ASP Traffic Maintenance
+-define(M3UA_MSGC_RKM,			9).	% Routing Key Management
+
+-define(M3UA_MSGT_MGMT_ERR,		0).
+-define(M3UA_MSGT_MGMT_NTFY,		1).
+
+-define(M3UA_MSGT_XFR_DATA,		1).
+
+-define(M3UA_MSGT_SSNM_DUNA,		1).	% Destination Unavailable
+-define(M3UA_MSGT_SSNM_DAVA,		2).	% Destination Available
+-define(M3UA_MSGT_SSNM_DAUD,		3).	% Destination State Audit
+-define(M3UA_MSGT_SSNM_SCON,		4).	% Signalling Congestion
+-define(M3UA_MSGT_SSNM_DUPU,		5).	% Destination User Part Unavailable
+-define(M3UA_MSGT_SSNM_DRST,		6).	% Destination Restricted
+
+-define(M3UA_MSGT_ASPSM_ASPUP,		1).	% ASP Up
+-define(M3UA_MSGT_ASPSM_ASPDN,		2).	% ASP Down
+-define(M3UA_MSGT_ASPSM_BEAT,		3).	% Heartbeat
+-define(M3UA_MSGT_ASPSM_ASPUP_ACK,	4).	% ASP Up Acknowledgement
+-define(M3UA_MSGT_ASPSM_ASPDN_ACK,	5).	% ASP Down Acknowledgement
+-define(M3UA_MSGT_ASPSM_BEAT_ACK,	6).	% Heartbeat Acknowledgement
+
+-define(M3UA_MSGT_ASPTM_ASPAC,		1).	% ASP Active
+-define(M3UA_MSGT_ASPTM_ASPIA,		2).	% ASP Inactive
+-define(M3UA_MSGT_ASPTM_ASPAC_ACK,	3).	% ASP Active Acknowledgement
+-define(M3UA_MSGT_ASPTM_ASPIA_ACK,	3).	% ASP Inactive Acknowledgement
+
+-define(M3UA_MSGT_RKM_REG_REQ,		1).	% Registration Request
+-define(M3UA_MSGT_RKM_REG_RSP,		2).	% Registration Response
+-define(M3UA_MSGT_RKM_DEREG_REQ,	3).	% Deregistration Request
+-define(M3UA_MSGT_RKM_DEREG_RSP,	4).	% Deregistration Response
+
+-define(M3UA_IEI_INFO_STRING,		16#0004).
+-define(M3UA_IEI_ROUTE_CTX,		16#0006).
+-define(M3UA_IEI_DIAG_INFO,		16#0007).
+-define(M3UA_IEI_HEARTB_DATA,		16#0009).
+-define(M3UA_IEI_TRAF_MODE_TYPE,	16#000b).
+-define(M3UA_IEI_ERR_CODE,		16#000c).
+-define(M3UA_IEI_STATUS,		16#000d).
+-define(M3UA_IEI_ASP_ID,		16#0011).
+-define(M3UA_IEI_AFFECTED_PC,		16#0012).
+-define(M3UA_IEI_CORR_ID,		16#0013).
+% M3UA-Specific parameters
+-define(M3UA_IEI_NET_APPEARANCE,	16#0200).
+-define(M3UA_IEI_USER_CAUSE,		16#0204).
+-define(M3UA_IEI_CONGESTION_IND,	16#0205).
+-define(M3UA_IEI_CONCERNED_IND,		16#0206).
+-define(M3UA_IEI_ROUTING_KEY,		16#0207).
+-define(M3UA_IEI_REG_RESULT,		16#0208).
+-define(M3UA_IEI_DEREG_RESULT,		16#0209).
+-define(M3UA_IEI_LOCAL_RKEY_ID,		16#020a).
+-define(M3UA_IEI_DEST_PC,		16#020b).
+-define(M3UA_IEI_SERVICE_IND,		16#020c).
+-define(M3UA_IEI_ORIG_PC_LIST,		16#020e).
+-define(M3UA_IEI_PROTOCOL_DATA,		16#0210).
+-define(M3UA_IEI_REG_STATUS,		16#0212).
+-define(M3UA_IEI_DEREG_STATUS,		16#0213).
+
+-record(m3ua_msg, {
+	 version,
+	 msg_class,
+	 msg_type,
+	 msg_length,
+	 payload
+	}).
+
diff --git a/src/m3ua_codec.erl b/src/m3ua_codec.erl
new file mode 100644
index 0000000..f25d80b
--- /dev/null
+++ b/src/m3ua_codec.erl
@@ -0,0 +1,74 @@
+% M3UA in accordance with RFC4666 (http://tools.ietf.org/html/rfc4666)
+
+% (C) 2011 by Harald Welte <laforge@gnumonks.org>
+%
+% All Rights Reserved
+%
+% This program is free software; you can redistribute it and/or modify
+% it under the terms of the GNU Affero General Public License as
+% published by the Free Software Foundation; either version 3 of the
+% License, or (at your option) any later version.
+%
+% This program is distributed in the hope that it will be useful,
+% but WITHOUT ANY WARRANTY; without even the implied warranty of
+% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+% GNU General Public License for more details.
+%
+% You should have received a copy of the GNU Affero General Public License
+% along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+-module(m3ua_codec).
+-author('Harald Welte <laforge@gnumonks.org>').
+-include("m3ua.hrl").
+
+-export([parse_m3ua_msg/1, encode_m3ua_msg/1]).
+
+%-compile(export_all).
+
+-compile({parse_transform, exprecs}).
+-export_records([m3ua_msg]).
+
+% compute the number of pad bits required after a binary parameter
+get_num_pad_bytes(BinLenBytes) ->
+	case BinLenBytes rem 4 of
+		0 ->    0;
+		Val ->  4 - Val
+	end.
+
+parse_m3ua_msg(DataBin) when is_binary(DataBin) ->
+	<<Version:8, _Reserved:8, MsgClass:8, MsgType:8, MsgLen:32/big, Remain/binary>> = DataBin,
+	OptList = parse_m3ua_opts(Remain),
+	#m3ua_msg{version = Version, msg_class = MsgClass, msg_type = MsgType,
+		  msg_length = MsgLen-4, payload = OptList};
+parse_m3ua_msg(Data) when is_list(Data) ->
+	parse_m3ua_msg(list_to_binary(Data)).
+
+parse_m3ua_opts(OptBin) when is_binary(OptBin) ->
+	parse_m3ua_opts(OptBin, []).
+
+parse_m3ua_opts(OptBin, OptList) when is_binary(OptBin), is_list(OptList) ->
+	<<Tag:16/big, Length:16/big, Remain/binary>> = OptBin,
+	PadLen = get_num_pad_bytes(Length),
+	LengthNet = Length - 4,
+	<<CurOpt:LengthNet/binary, 0:PadLen/integer-unit:8, Remain2/binary>> = Remain,
+	parse_m3ua_opts(Remain2, OptList ++ [{Tag, CurOpt}]).
+
+encode_m3ua_msg(#m3ua_msg{version = Version, msg_class = MsgClass,
+			  msg_type = MsgType, payload = OptList}) ->
+	OptBin = encode_m3ua_opts(OptList),
+	MsgLen = length(OptBin) + 8,
+	<<Version:4, 0:8, MsgClass:8, MsgType:8, MsgLen:32/big, OptBin/binary>>.
+
+encode_m3ua_opts(OptList) when is_list(OptList) ->
+	encode_m3ua_opts(OptList, <<>>).
+
+encode_m3ua_opts([], Bin) ->
+	Bin;
+encode_m3ua_opts([Head|Tail], Bin) ->
+	OptBin = encode_m3ua_opt(Head),
+	encode_m3ua_opts(Tail, <<Bin/binary, OptBin/binary>>).
+
+encode_m3ua_opt({Iei, Data}) when is_integer(Iei), is_binary(Data) ->
+	Length = length(Data) + 4,
+	PadLen = get_num_pad_bytes(Length),
+	<<Iei:16/big, Length:16/big, 0:PadLen/integer-unit:8, Data/binary>>.
diff --git a/src/m3ua_core.erl b/src/m3ua_core.erl
new file mode 100644
index 0000000..380e1b4
--- /dev/null
+++ b/src/m3ua_core.erl
@@ -0,0 +1,209 @@
+% M3UA in accordance with RFC4666 (http://tools.ietf.org/html/rfc4666)
+
+% (C) 2011 by Harald Welte <laforge@gnumonks.org>
+%
+% All Rights Reserved
+%
+% This program is free software; you can redistribute it and/or modify
+% it under the terms of the GNU Affero General Public License as
+% published by the Free Software Foundation; either version 3 of the
+% License, or (at your option) any later version.
+%
+% This program is distributed in the hope that it will be useful,
+% but WITHOUT ANY WARRANTY; without even the implied warranty of
+% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+% GNU General Public License for more details.
+%
+% You should have received a copy of the GNU Affero General Public License
+% along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+-module(m3ua_core).
+-author('Harald Welte <laforge@gnumonks.org>').
+
+-include_lib("kernel/include/inet_sctp.hrl").
+-include("sccp.hrl").
+-include("m3ua.hrl").
+
+-export([start_link/1]).
+
+-export([init/1, handle_event/3]).
+
+% FSM states:
+-export([asp_down/2, asp_inactive/2, asp_active/2]).
+
+-define(T_ACK_TIMEOUT, 2*60*100).
+
+% Loop Data
+-record(m3ua_state, {
+	  role,		% asp | sgp
+	  asp_state,	% down, inactive, active
+	  t_ack,
+	  user_pid,
+	  sctp_remote_ip,
+	  sctp_remote_port,
+	  sctp_sock,
+	  sctp_assoc_id
+	}).
+
+start_link(InitOpts) ->
+	gen_fsm:start_link(?MODULE, InitOpts, [{debug, [trace]}]).
+
+reconnect_sctp(L = #m3ua_state{sctp_remote_ip = Ip, sctp_remote_port = Port, sctp_sock = Sock}) ->
+	io:format("SCTP Reconnect ~p:~p~n", [Ip, Port]),
+	InitMsg = #sctp_initmsg{num_ostreams = 1, max_instreams = 1},
+	case gen_sctp:connect(Sock, Ip, Port, [{active, once}, {reuseaddr, true},
+					       {sctp_initmsg, InitMsg}]) of
+		{ok, Assoc} ->
+			L#m3ua_state{sctp_assoc_id = Assoc#sctp_assoc_change.assoc_id};
+		{error, Error } ->
+			reconnect_sctp(L)
+	end.
+
+init(InitOpts) ->
+	{ok, SctpSock} = gen_sctp:open([{active, once}, {reuseaddr, true}]),
+	LoopDat = #m3ua_state{role = asp, sctp_sock = SctpSock,
+				user_pid = proplists:get_value(user_pid, InitOpts),
+				sctp_remote_ip = proplists:get_value(sctp_remote_ip, InitOpts),
+				sctp_remote_port = proplists:get_value(sctp_remote_port, InitOpts)},
+	LoopDat2 = reconnect_sctp(LoopDat),
+	{ok, asp_down, LoopDat2}.
+
+% Helper function to send data to the SCTP peer
+send_sctp_to_peer(LoopDat, PktData) when is_binary(PktData) ->
+	#m3ua_state{sctp_sock = Sock, sctp_assoc_id = Assoc} = LoopDat,
+	SndRcvInfo = #sctp_sndrcvinfo{assoc_id = Assoc, ppid = 3, stream = 0},
+	gen_sctp:send(Sock, SndRcvInfo, PktData);
+
+% same as above, but for un-encoded #m3ua_msg{}
+send_sctp_to_peer(LoopDat, M3uaMsg) when is_record(M3uaMsg, m3ua_msg) ->
+	MsgBin = m3ua_codec:encode_m3ua_msg(M3uaMsg),
+	send_sctp_to_peer(LoopDat, MsgBin).
+
+% helper to send one of the up/down/act/inact management messages + start timer
+send_msg_start_tack(LoopDat, State, MsgClass, MsgType, Params) ->
+	% generate and send the respective message
+	Msg = #m3ua_msg{version = 1, msg_class = MsgClass, msg_type = MsgType, payload = Params},
+	send_sctp_to_peer(LoopDat, Msg),
+	% start T(ack) timer and wait for ASP_UP_ACK
+	Tack = timer:apply_after(?T_ACK_TIMEOUT, gen_fsm, send_event,
+				 [self(), {timer_expired, t_ack, {MsgClass, MsgType, Params}}]),
+	{next_state, State, LoopDat#m3ua_state{t_ack = Tack}}.
+
+
+handle_event(Msg = #m3ua_msg{msg_class = ?M3UA_MSGC_ASPSM,
+			     msg_type = ?M3UA_MSGT_ASPSM_BEAT}, State, LoopDat) ->
+	% Send BEAT_ACK using the same payload as the BEAT msg
+	send_sctp_to_peer(LoopDat, Msg#m3ua_msg{msg_type = ?M3UA_MSGT_ASPSM_BEAT_ACK}),
+	{next_state, State, LoopDat};
+
+handle_event({sctp, Socket, _RemoteIp, _RemotePort, {ANC, SAC}},
+	     _State, LoopDat) when is_record(SAC, sctp_assoc_change) ->
+	io:format("SCTP Assoc Change ~p ~p~n", [ANC, SAC]),
+	#sctp_assoc_change{state = SacState, outbound_streams = OutStreams,
+			   inbound_streams = InStreams, assoc_id = AssocId} = SAC,
+	case SacState of 
+		comm_up ->
+			% FIXME: primmitive to the user
+			LoopDat2 = LoopDat;
+		comm_lost ->
+			LoopDat2 = reconnect_sctp(LoopDat);
+		addr_unreachable ->
+			LoopDat2 = reconnect_sctp(LoopDat)
+	end,
+	inet:setopts(Socket, [{active, once}]),
+	{next_state, asp_down, LoopDat2};
+
+handle_event({sctp, Socket, _RemoteIp, _RemotePort, {[Anc], Data}}, State, LoopDat) ->
+	io:format("SCTP rx data: ~p ~p~n", [Anc, Data]),
+	% FIXME: process incoming SCTP data 
+	inet:setopts(Socket, [{active, once}]),
+	{next_state, State, LoopDat};
+
+handle_event({sctp, Socket, RemoteIp, RemotePort, {_Anc, Data}}, _State, LoopDat)
+					when is_record(Data, sctp_shutdown_event) ->
+	io:format("SCTP remote ~p:~p shutdown~n", [RemoteIp, RemotePort]),
+	inet:setopts(Socket, [{active, once}]),
+	{next_state, asp_down, LoopDat}.
+
+
+
+asp_down(#primitive{subsystem = 'M', gen_name = 'ASP_UP',
+		    spec_name = request, parameters = Params}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_down, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPUP, Params);
+asp_down({timer_expired, t_ack, {?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPUP, Params}}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_down, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPUP, Params);
+
+asp_down(#m3ua_msg{msg_class = ?M3UA_MSGC_ASPSM,
+		   msg_type = ?M3UA_MSGT_ASPSM_ASPUP_ACK}, LoopDat) ->
+	% transition into ASP_INACTIVE
+	{next_state, asp_inactive, LoopDat}.
+
+
+
+asp_inactive(#primitive{subsystem = 'M', gen_name = 'ASP_ACTIVATE',
+			spec_name = request, parameters = Params}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_inactive, ?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPAC, Params);
+
+asp_inactive({timer_expired, t_ack, {?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPAC, Params}}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_inactive, ?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPAC, Params);
+
+asp_inactive(#primitive{subsystem = 'M', gen_name = 'ASP_DOWN',
+		      spec_name = request, parameters = Params}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_inactive, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params);
+
+asp_inactive({timer_expired, t_ack, {?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params}}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_inactive, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params);
+
+asp_inactive(#m3ua_msg{msg_class = ?M3UA_MSGC_ASPTM,
+		       msg_type = ?M3UA_MSGT_ASPTM_ASPAC_ACK}, LoopDat) ->
+	% transition into ASP_ACTIVE
+	% FIXME: signal this to the user
+	{next_state, asp_active, LoopDat};
+
+asp_inactive(#m3ua_msg{msg_class = ?M3UA_MSGC_ASPSM,
+		       msg_type = ?M3UA_MSGT_ASPSM_ASPDN_ACK}, LoopDat) ->
+	% transition into ASP_DOWN
+	% FIXME: signal this to the user
+	{next_state, asp_down, LoopDat}.
+
+
+
+asp_active(#m3ua_msg{msg_class = ?M3UA_MSGC_ASPSM,
+		     msg_type = ?M3UA_MSGT_ASPSM_ASPDN_ACK}, LoopDat) ->
+	% transition into ASP_DOWN
+	% FIXME: signal this to the user
+	{next_state, asp_down, LoopDat};
+
+asp_active(#m3ua_msg{msg_class = ?M3UA_MSGC_ASPTM,
+		     msg_type = ?M3UA_MSGT_ASPTM_ASPIA_ACK}, LoopDat) ->
+	% transition into ASP_INACTIVE
+	% FIXME: signal this to the user
+	{next_state, asp_inactive, LoopDat};
+
+asp_active(#primitive{subsystem = 'M', gen_name = 'ASP_DOWN',
+		      spec_name = request, parameters = Params}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_active, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params);
+
+asp_active({timer_expired, t_ack, {?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params}}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_active, ?M3UA_MSGC_ASPSM, ?M3UA_MSGT_ASPSM_ASPDN, Params);
+
+asp_active(#primitive{subsystem = 'M', gen_name = 'ASP_INACTIVE',
+		      spec_name = request, parameters = Params}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_active, ?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPIA, Params);
+
+asp_active({timer_expired, t_ack, {?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPIA, Params}}, LoopDat) ->
+	send_msg_start_tack(LoopDat, asp_active, ?M3UA_MSGC_ASPTM, ?M3UA_MSGT_ASPTM_ASPIA, Params);
+
+asp_active(#primitive{subsystem = 'MTP', gen_name = 'TRANSFER',
+		      spec_name = request, parameters = Params}, LoopDat) ->
+	% Send message to remote peer
+	OptList = [{?M3UA_IEI_PROTOCOL_DATA, Params}],
+	Msg = #m3ua_msg{version = 1, msg_class = ?M3UA_MSGC_TRANSFER,
+			msg_type = ?M3UA_MSGT_XFR_DATA,
+			payload = OptList},
+	send_sctp_to_peer(LoopDat, Msg),
+	{next_state, asp_active, LoopDat};
+asp_active(#m3ua_msg{version = 1, msg_class = ?M3UA_MSGC_TRANSFER,
+		     msg_type = ?M3UA_MSGT_XFR_DATA, payload = Params}, LoopDat) ->
+	% FIXME: Send primitive to the user
+	{next_state, asp_active, LoopDat}.