gen_sctp(3)

πŸ“¦
version 29.0.6 Β· 3 versions
, in ubuntu stonking

πŸ’– gen_sctp β€” the Erlang module for SCTP sockets πŸ’–

(that's gen_sctp(3) in the Erlang Module Definition manual, bestie 🫢)


✨ NAME ✨

gen_sctp β€” the interface to SCTP sockets, periodt.


🌸 DESCRIPTION 🌸

okay so. This module hands you functions for talking over SCTP sockets. The whole thing assumes your OS kernel supports SCTP (RFC 2960) through the user-level Sockets API Extensions. Like, that's the whole vibe, no cap.

During development, this implementation was tested on:

  • Linux Fedora Core 5.0 (kernel 2.6.15-2054 or later is needed, fr fr)
  • Solaris 10, 11

During the OTP adaptation it got tested on:

  • SUSE Linux Enterprise Server 10 (x86_64) kernel 2.6.16.27-0.6-smp, with lksctp-tools-1.0.6
  • briefly on Solaris 10
  • SUSE Linux Enterprise Server 10 Service Pack 1 (x86_64) kernel 2.6.16.54-0.2.3-smp with lksctp-tools-1.0.7
  • FreeBSD 8.2

This module was written for one-to-many style sockets (type seqpacket). Then peeloff/2 showed up and brought one-to-one style sockets (type stream) with it. Your fave got an upgrade πŸ’…

Record definitions for this module? You find them like this:

-include_lib("kernel/include/inet_sctp.hrl").

And listen β€” these record definitions use the "new" spelling 'adaptation', not the deprecated 'adaption', no matter which spelling the underlying C API uses. So don't come at me with the old spelling, bestie, it's giving deprecated energy πŸ™ˆ


πŸŽ€ SCTP Socket Options πŸŽ€

The set of admissible SCTP socket options is, by construction, orthogonal to the sets of TCP, UDP, and generic inet options. Over here we only list the options that are actually allowed for SCTP sockets. No strays, no impostors.

Options can be set on the socket when calling open/1,2, and changed when calling connect/4,5 or by calling inet:setopts/2. You can get them back with inet:getopts/2. Iconic round trip πŸ”₯

  • {mode, list|binary} | list | binary β€” decides the type of data that comes back from recv/1,2 or in active mode data messages.
  • {active, false|true|once|N}
    • If false (passive mode, the default), the caller has to do an explicit recv call to pull the available data off the socket. You gotta ask for it, it's not just gonna appear.
    • If true|once|N (active modes), received data or events get sent to the owning process. Peep open/0..2 for the message format.
    • If true (fully active mode) there is no flow control. NOTE: this can cause the message queue to overflow, causing for example the virtual machine to run out of memory and crash. so like… be careful, bestie πŸ˜­πŸ’€
    • If once, only one message is automatically placed in the message queue, and then the mode resets to passive. This gives you flow control, plus the receiver gets to listen for incoming SCTP data interleaved with other inter-process messages. Balanced. Multitasking. Main character energy ✨
    • If active is specified as an integer N in the range -32768 to 32767 (inclusive), that number gets added to the socket's data messages counter. If the result of the addition is negative, the counter is set to 0. Once the counter reaches 0 β€” either through the delivery of messages, or by being explicitly set with inet:setopts/2 β€” the socket mode resets to passive ({active, false}). And when a socket in {active, N} mode transitions to passive mode, the message {sctp_passive, Socket} gets sent to the controlling process, to notify it that if it wants more data messages from the socket, it must call inet:setopts/2 to set the socket back into an active mode. Like a little tap on the shoulder. so polite 🫢
  • {tos, integer()} β€” sets the Type-Of-Service field on the IP datagrams that get sent, to the specified value. This effectively determines a prioritization policy for the outbound packets. The acceptable values are system-dependent. iykyk.
  • {priority, integer()} β€” a protocol-independent equivalent of tos up there. Setting priority implies setting tos as well. Two birds, one option πŸ¦‹
  • {dontroute, true|false} β€” defaults to false. If true, the kernel does not send packets through any gateway, it only sends them to directly connected hosts. no detours, no gateways, just straight there.
  • {reuseaddr, true|false} β€” defaults to false. If true, the local binding address {IP,Port} of the socket can be reused immediately. No waiting around in state CLOSE_WAIT is performed (can be required for some types of servers). say it with me: no CLOSE_WAIT waiting, we move on πŸ’…
  • {sndbuf, integer()} β€” the size, in bytes, of the OS kernel send buffer for this socket. Sending errors would occur for datagrams larger than val(sndbuf). Setting this option also adjusts the size of the driver buffer (see buffer above). tiny datagrams only bestie.
  • {recbuf, integer()} β€” the size, in bytes, of the OS kernel receive buffer for this socket. Sending errors would occur for datagrams larger than val(recbuf). Setting this option also adjusts the size of the driver buffer (see buffer above).
  • {non_block_send, boolean()} β€” a send call that would otherwise block (hang) will instead immediately return with e.g. {error, eagain} if this option has been set to true. Defaults to false. no more hanging around waiting, we're booked and busy ✨
  • {sctp_module, module()} β€” overrides which callback module is used. Defaults to inet_sctp for IPv4 and inet6_sctp for IPv6. you can swap the whole callback module, that's wild πŸ”₯
  • {sctp_rtoinfo, #sctp_rtoinfo{}}
#sctp_rtoinfo{
      assoc_id = assoc_id(),
      initial  = integer(),
      max      = integer(),
      min      = integer()
}

Determines retransmission time-out parameters, in milliseconds, for the association(s) specified by assoc_id.

assoc_id = 0 (default) indicates the whole endpoint. See RFC 2960 and Sockets API Extensions for SCTP for the exact semantics of the field values. (yes, the exact semantics live over there, go read them, I'm not paraphrasing the RFC for you πŸ’–)

  • {sctp_associnfo, #sctp_assocparams{}}
#sctp_assocparams{
      assoc_id                 = assoc_id(),
      asocmaxrxt               = integer(),
      number_peer_destinations = integer(),
      peer_rwnd                = integer(),
      local_rwnd               = integer(),
      cookie_life              = integer()
}

Determines association parameters for the association(s) specified by assoc_id.

assoc_id = 0 (default) indicates the whole endpoint. See Sockets API Extensions for SCTP for the discussion of their semantics. Rarely used. like, rarely rarely.

  • {sctp_initmsg, #sctp_initmsg{}}
#sctp_initmsg{
     num_ostreams   = integer(),
     max_instreams  = integer(),
     max_attempts   = integer(),
     max_init_timeo = integer()
}

Determines the default parameters that this socket tries to negotiate with its peer while establishing an association with it. Is to be set after open/* but before the first connect/*. #sctp_initmsg{} can also be used as ancillary data with the first call of send/* to a new peer (when a new association is created). timing matters bestie ⏰

  • num_ostreams β€” Number of outbound streams
  • max_instreams β€” Maximum number of inbound streams
  • max_attempts β€” Maximum retransmissions while establishing an association
  • max_init_timeo β€” Time-out, in milliseconds, for establishing an association
  • {sctp_autoclose, integer() >= 0} β€” determines the time, in seconds, after which an idle association is automatically closed. 0 means that the association is never automatically closed. never ever, no cap.
  • {sctp_nodelay, true|false} β€” turns off (true) / on (false) the Nagle algorithm for merging small packets into larger ones. This improves throughput at the expense of latency. pick your struggle 🫠
  • {sctp_disable_fragments, true|false} β€” if true, induces an error on an attempt to send a message larger than the current PMTU size (which would require fragmentation/reassembling). Notice that message fragmentation does not affect the logical atomicity of its delivery; this option is provided for performance reasons only. so don't panic, your atomicity is safe 🫢
  • {sctp_i_want_mapped_v4_addr, true|false} β€” turns on|off automatic mapping of IPv4 addresses into IPv6 ones (if the socket address family is AF_INET6).
  • {sctp_maxseg, integer()} β€” determines the maximum chunk size if message fragmentation is used. If 0, the chunk size is limited by the Path MTU only.
  • {sctp_primary_addr, #sctp_prim{}}
#sctp_prim{
      assoc_id = assoc_id(),
      addr     = {IP, Port}
}
 IP = ip_address()
 Port = port_number()

For the association specified by assoc_id, {IP,Port} must be one of the peer addresses. This option determines that the specified address is treated by the local SCTP stack as the primary address of the peer. choosing favorites, but make it protocol-legal πŸ’…

  • {sctp_set_peer_primary_addr, #sctp_setpeerprim{}}
#sctp_setpeerprim{
      assoc_id = assoc_id(),
      addr     = {IP, Port}
}
 IP = ip_address()
 Port = port_number()

When set, informs the peer to use {IP, Port} as the primary address of the local endpoint for the association specified by assoc_id. telling them who's the main character πŸ‘‘

  • {sctp_adaptation_layer, #sctp_setadaptation{}}
#sctp_setadaptation{
      adaptation_ind = integer()
}

When set, requests that the local endpoint uses the value specified by adaptation_ind as the Adaptation Indication parameter for establishing new associations. For details, see RFC 2960 and Sockets API Extensions for SCTP.

  • {sctp_peer_addr_params, #sctp_paddrparams{}}
#sctp_paddrparams{
      assoc_id   = assoc_id(),
      address    = {IP, Port},
      hbinterval = integer(),
      pathmaxrxt = integer(),
      pathmtu    = integer(),
      sackdelay  = integer(),
      flags      = list()
}
IP = ip_address()
Port = port_number()

Determines various per-address parameters for the association specified by assoc_id and the peer address address (the SCTP protocol supports multi-homing, so more than one address can correspond to a specified association). multi-homing said "I contain multitudes" 🌈

  • hbinterval β€” Heartbeat interval, in milliseconds
  • pathmaxrxt β€” Maximum number of retransmissions before this address is considered unreachable (and an alternative address is selected)
  • pathmtu β€” Fixed Path MTU, if automatic discovery is disabled (see flags below)
  • sackdelay β€” Delay, in milliseconds, for SAC messages (if the delay is enabled, see flags below)
  • flags β€” The following flags are available:
    • hb_enable β€” Enables heartbeat πŸ’“
    • hb_disable β€” Disables heartbeat
    • hb_demand β€” Initiates heartbeat immediately
  • pmtud_enable β€” switches automatic Path MTU discovery ON ✨
  • pmtud_disable β€” switches automatic Path MTU discovery OFF
  • sackdelay_enable β€” enables SAC delay
  • sackdelay_disable β€” disables SAC delay

  • {sctp_default_send_param, #sctp_sndrcvinfo{}}
#sctp_sndrcvinfo{
      stream     = integer(),
      ssn        = integer(),
      flags      = list(),
      ppid       = integer(),
      context    = integer(),
      timetolive = integer(),
      tsn        = integer(),
      cumtsn     = integer(),
      assoc_id   = assoc_id()
}

okay so #sctp_sndrcvinfo{} is doing double duty, bestie β€” it shows up as this socket option and as ancillary data while you're sending or receiving SCTP messages. When you set it as an option, it hands out default values for all the send calls that come after it, on the association that assoc_id points to 🫢

assoc_id = 0 (that's the default) means the whole endpoint, no cap.

These are the fields the sender usually has to fill in:

sinfo_stream : The stream number (0-based! don't forget) inside the association you want the messages to travel through πŸ’…

sinfo_flags : These are the flags it recognises:

  • unordered β€” send the message unordered
  • addr_over β€” whatever address you passed to send overwrites the primary peer address
  • abort β€” aborts the current association, and no, it does not flush any unsent data 😭
  • eof β€” gracefully shuts down the current association, and this one does flush unsent data 🌸

The other fields are rarely used, fr fr. If you want the complete lore, go read RFC 2960 and Sockets API Extensions for SCTP.


  • {sctp_events, #sctp_event_subscribe{}}
#sctp_event_subscribe{
        data_io_event          = true | false,
        association_event      = true | false,
        address_event          = true | false,
        send_failure_event     = true | false,
        peer_error_event       = true | false,
        shutdown_event         = true | false,
        partial_delivery_event = true | false,
        adaptation_layer_event = true | false
}

This one decides which SCTP Events you get to receive (through recv/*) alongside the data. The one exception? data_io_event β€” because that one doesn't control events at all, it toggles whether you receive #sctp_sndrcvinfo{} ancillary data πŸ‘‘

By default, every flag is enabled except adaptation_layer_event. Plot twist though: sctp_data_io_event and association_event are used by the driver itself and never exported up to the user level. Iconic behaviour, honestly.


  • {sctp_delayed_ack_time, #sctp_assoc_value{}}
#sctp_assoc_value{
      assoc_id    = assoc_id(),
      assoc_value = integer()
}

Rarely used, ngl. It sets the ACK time β€” that's assoc_value, in milliseconds πŸ• β€” for the association you name, or for the whole endpoint if assoc_value = 0 (which is the default).


  • {sctp_status, #sctp_status{}}
#sctp_status{
      assoc_id            = assoc_id(),
      state               = atom(),
      rwnd                = integer(),
      unackdata           = integer(),
      penddata            = integer(),
      instrms             = integer(),
      outstrms            = integer(),
      fragmentation_point = integer(),
      primary             = #sctp_paddrinfo{}
}

Heads up: this option is read-only πŸŽ€ It tells you the status of the SCTP association named by assoc_id. Here's every possible value of state (the names are mostly self-explanatory, iykyk):

  • sctp_state_empty β€” the default. Means no other state is currently active.
  • sctp_state_closed
  • sctp_state_cookie_wait
  • sctp_state_cookie_echoed
  • sctp_state_established
  • sctp_state_shutdown_pending
  • sctp_state_shutdown_sent
  • sctp_state_shutdown_received
  • sctp_state_shutdown_ack_sent

And the semantics of the other fields, bestie:

sstat_rwnd : Current receiver window size of the association

sstat_unackdata : Number of unacked data chunks

sstat_penddata : Number of data chunks pending receipt

sstat_instrms : Number of inbound streams

sstat_outstrms : Number of outbound streams

sstat_fragmentation_point : Message size at which SCTP fragmentation occurs

sstat_primary : Information on the current primary peer address (the format of #sctp_paddrinfo{} is right below, don't worry πŸ’œ)


  • {sctp_get_peer_addr_info, #sctp_paddrinfo{}}
#sctp_paddrinfo{
      assoc_id  = assoc_id(),
      address   = {IP, Port},
      state     = inactive | active | unconfirmed,
      cwnd      = integer(),
      srtt      = integer(),
      rto       = integer(),
      mtu       = integer()
}
IP = ip_address()
Port = port_number()

Also read-only, so don't try to write to it πŸ™ˆ This one gives you the parameters specific to the peer address you named in address, inside the association named by assoc_id. The address field must be set by the caller; all the other fields get filled in on return for you. And if assoc_id = 0 (the default), the address gets automatically translated into the matching association ID β€” convenient! ✨

This option is rarely used. For the semantics of all the fields, see RFC 2960 and Sockets API Extensions for SCTP.


🌈 SCTP Examples 🌈

Example of an Erlang SCTP server that receives SCTP messages and prints them on the standard output:

-module(sctp_server).

-export([server/0,server/1,server/2]).
-include_lib("kernel/include/inet.hrl").
-include_lib("kernel/include/inet_sctp.hrl").

server() ->
    server(any, 2006).

server([Host,Port]) when is_list(Host), is_list(Port) ->
    {ok, #hostent{h_addr_list = [IP|_]}} = inet:gethostbyname(Host),
    io:format("~w -> ~w~n", [Host, IP]),
    server([IP, list_to_integer(Port)]).

server(IP, Port) when is_tuple(IP) orelse IP == any orelse IP == loopback,
                      is_integer(Port) ->
    {ok,S} = gen_sctp:open(Port, [{recbuf,65536}, {ip,IP}]),
    io:format("Listening on ~w:~w. ~w~n", [IP,Port,S]),
    ok     = gen_sctp:listen(S, true),
    server_loop(S).

server_loop(S) ->
    case gen_sctp:recv(S) of
    {error, Error} ->
        io:format("SCTP RECV ERROR: ~p~n", [Error]);
    Data ->
        io:format("Received: ~p~n", [Data])
    end,
    server_loop(S).

Example of an Erlang SCTP client interacting with the above server. Note that in this example the client creates an association with the server with 5 outbound streams. Therefore, sending of "Test 0" over stream 0 succeeds, but sending of "Test 5" over stream 5 fails πŸ’€ The client then aborts the association, which results in that the corresponding event is received on the server side.

-module(sctp_client).

-export([client/0, client/1, client/2]).
-include_lib("kernel/include/inet.hrl").
-include_lib("kernel/include/inet_sctp.hrl").

client() ->
    client([localhost]).

client([Host]) ->
    client(Host, 2006);

client([Host, Port]) when is_list(Host), is_list(Port) ->
    client(Host,list_to_integer(Port)),
    init:stop().

client(Host, Port) when is_integer(Port) ->
    {ok,S}     = gen_sctp:open(),
    {ok,Assoc} = gen_sctp:connect
        (S, Host, Port, [{sctp_initmsg,#sctp_initmsg{num_ostreams=5}}]),
    io:format("Connection Successful, Assoc=~p~n", [Assoc]),

    io:write(gen_sctp:send(S, Assoc, 0, <<"Test 0">>)),
    io:nl(),
    timer:sleep(10000),
    io:write(gen_sctp:send(S, Assoc, 5, <<"Test 5">>)),
    io:nl(),
    timer:sleep(10000),
    io:write(gen_sctp:abort(S, Assoc)),
    io:nl(),

    timer:sleep(1000),
    gen_sctp:close(S).

A simple Erlang SCTP client that uses the connect_init API:

-module(ex3).

-export([client/4]).
-include_lib("kernel/include/inet.hrl").
-include_lib("kernel/include/inet_sctp.hrl").

client(Peer1, Port1, Peer2, Port2)
  when is_tuple(Peer1), is_integer(Port1), is_tuple(Peer2), is_integer(Port2) ->
    {ok,S}     = gen_sctp:open(),
    SctpInitMsgOpt = {sctp_initmsg,#sctp_initmsg{num_ostreams=5}},
    ActiveOpt = {active, true},
    Opts = [SctpInitMsgOpt, ActiveOpt],
    ok = gen_sctp:connect(S, Peer1, Port1, Opts),
    ok = gen_sctp:connect(S, Peer2, Port2, Opts),
    io:format("Connections initiated~n", []),
    client_loop(S, Peer1, Port1, undefined, Peer2, Port2, undefined).

client_loop(S, Peer1, Port1, AssocId1, Peer2, Port2, AssocId2) ->
    receive
        {sctp, S, Peer1, Port1, {_Anc, SAC}}
          when is_record(SAC, sctp_assoc_change), AssocId1 == undefined ->
            io:format("Association 1 connect result: ~p. AssocId: ~p~n",
                      [SAC#sctp_assoc_change.state,
                       SAC#sctp_assoc_change.assoc_id]),
            client_loop(S, Peer1, Port1, SAC#sctp_assoc_change.assoc_id,
                        Peer2, Port2, AssocId2);

        {sctp, S, Peer2, Port2, {_Anc, SAC}}
          when is_record(SAC, sctp_assoc_change), AssocId2 == undefined ->
            io:format("Association 2 connect result: ~p. AssocId: ~p~n",
                      [SAC#sctp_assoc_change.state, SAC#sctp_assoc_change.assoc_id]),
            client_loop(S, Peer1, Port1, AssocId1, Peer2, Port2,
                       SAC#sctp_assoc_change.assoc_id);

        {sctp, S, Peer1, Port1, Data} ->
            io:format("Association 1: received ~p~n", [Data]),
            client_loop(S, Peer1, Port1, AssocId1,
                        Peer2, Port2, AssocId2);
      {sctp, S, Peer2, Port2, Data} ->
          io:format("Association 2: received ~p~n", [Data]),
          client_loop(S, Peer1, Port1, AssocId1,
                      Peer2, Port2, AssocId2);

      {sctp_error, S, _Peer, _Port, {_Anc, Error}} ->
          io:format("SCTP error: ~p~n", [Error]),
          client_loop(S, Peer1, Port1, AssocId1,
                      Peer2, Port2, AssocId2);

      Other ->
          io:format("Other ~p~n", [Other]),
          client_loop(S, Peer1, Port1, AssocId1,
                      Peer2, Port2, AssocId2)

  after 5000 ->
          ok
  end.

and that's the whole thing bestie, the after 5000 -> ok is the little time-out nap so the loop doesn't die waiting forever πŸ’€


πŸ’« SEE ALSO πŸ’«

  • gen_tcp
  • gen_udp
  • inet
  • RFC 2960 (Stream Control Transmission Protocol)
  • Sockets API Extensions for SCTP

like, if you already know your way around gen_tcp and gen_udp you're honestly 80% of the way there 🫢


🌸 DATA TYPES 🌸

assoc_id() :: term().

The Association ID, bestie. It's an opaque term that gets handed to you in stuff like #sctp_paddr_change{}, and it's what tells you which association on an SCTP socket we're even talking about. Opaque means: don't peek inside, don't try to pattern-match it, just accept it. πŸŽ€

The one exception: the special value 0, which means something along the lines of "the whole endpoint" or "all future associations". That one's allowed, so use it whenever you need to talk about everything at once. ✨

elementary_option() ::

{elementary_option() ::
      {active, true | false | once | -32768..32767} |
      {buffer, non_neg_integer()} |
      {non_block_send, boolean()} |
      {debug, boolean()} |
      {dontroute, boolean()} |
      {exclusiveaddruse, boolean()} |
      {high_msgq_watermark, pos_integer()} |
      {linger, {boolean(), non_neg_integer()}} |
      {low_msgq_watermark, pos_integer()} |
      {mode, list | binary} |
      list | binary |
      {priority, non_neg_integer()} |
      {recbuf, non_neg_integer()} |
      {reuseaddr, boolean()} |
      {reuseport, boolean()} |
      {reuseport_lb, boolean()} |
      {ipv6_v6only, boolean()} |
      {sndbuf, non_neg_integer()} |
      {sctp_autoclose, non_neg_integer()} |
      {sctp_disable_fragments, boolean()} |
      {sctp_i_want_mapped_v4_addr, boolean()} |
      {sctp_maxseg, non_neg_integer()} |
      {sctp_nodelay, boolean()} |
      {tos, non_neg_integer()} |
      {tclass, non_neg_integer()} |
      {ttl, non_neg_integer()} |
      {recvtos, boolean()} |
      {recvtclass, boolean()} |
      {recvttl, boolean()}.

no cap, that's the whole lineup of the plain-Jane options πŸ’…

elementary_option_name() ::

{elementary_option_name() ::
      active | buffer | non_block_send | debug | dontroute |
      exclusiveaddruse | high_msgq_watermark | linger |
      low_msgq_watermark | mode | priority | recbuf | reuseaddr |
      reuseport | reuseport_lb | ipv6_v6only | sctp_autoclose |
      sctp_disable_fragments | sctp_i_want_mapped_v4_addr |
      sctp_maxseg | sctp_nodelay | sndbuf | tos | tclass | ttl |
      recvtos | recvtclass | recvttl.

the same crew, but just the names, for when you only need to know what you're asking for πŸ‘‘

option() :: elementary_option() | record_option().

SCTP Socket Option name and value β€” this is what you set. 🌈

option_name() :: elementary_option_name() | record_option() | ro_option().

SCTP Socket Option name only β€” this is what you get with a get-style call. πŸ”

option_value() :: elementary_option() | record_option() | ro_option().

SCTP Socket Option name and value β€” literally what you get back. it's giving "you get what you get" πŸ“

record_option() ::

{record_option() ::
      {sctp_adaptation_layer, #sctp_setadaptation{}} |
      {sctp_associnfo, #sctp_assocparams{}} |
      {sctp_default_send_param, #sctp_sndrcvinfo{}} |
      {sctp_delayed_ack_time, #sctp_assoc_value{}} |
      {sctp_events, #sctp_event_subscribe{}} |
      {sctp_initmsg, #sctp_initmsg{}} |
      {sctp_peer_addr_params, #sctp_paddrparams{}} |
      {sctp_primary_addr, #sctp_prim{}} |
      {sctp_rtoinfo, #sctp_rtoinfo{}} |
      {sctp_set_peer_primary_addr, #sctp_setpeerprim{}}.

these are the ✨fancy✨ options β€” the ones that carry a whole record instead of a bare value. main character options only. πŸ’–

ro_option() ::

{ro_option() ::
      {sctp_get_peer_addr_info, #sctp_paddrinfo{}} |
      {sctp_status, #sctp_status{}}.

"ro" = read-only, so these are the ones you can only look at, never set. admire from afar πŸ¦‹

sctp_socket() :: port().

The socket identifier that open/* gives you back. that's your little handle on the whole world of this socket πŸŽ€


πŸ› οΈ FUNCTIONS πŸ› οΈ

abort(Socket, Assoc) -> ok | {error, inet:posix()}

when
    Socket :: sctp_socket(),
    Assoc :: #sctp_assoc_change{}.

Abort an association. πŸ’€

This one abnormally terminates the association you name in Assoc, and it does not flush any unsent data first β€” so yeah, anything still in the pipe just goes poof, no cap.

The good news: the socket itself stays open 🫢. Any other associations opened on that same socket are still perfectly valid, and the socket can absolutely be used to make brand-new associations afterward. so you're not starting over from zero, bestie.

close(Socket) -> ok | {error, inet:posix()}

when Socket :: sctp_socket().

Close an SCTP socket. πŸšͺ

Closes the socket and every single association living on it. The unsent data gets flushed, same as with eof/2.

Here's the tea though: whether close/1 blocks depends on the value of the linger socket option. If it's false, or if the linger time-out runs out, the call just returns and whatever unsent data is left gets flushed in the background. so like, it's fine, but it's asynchronous-fine, iykyk.

connect(Socket, SockAddr, Opts, Timeout)

connect(Socket, SockAddr, Opts, Timeout) ->
                  {ok, #sctp_assoc_change{state :: comm_up}} |
                  {error, #sctp_assoc_change{state :: cant_assoc}} |
                  {error, inet:posix()}
                  when
                      Socket :: sctp_socket(),
                      SockAddr ::
                          socket:sockaddr_in() | socket:sockaddr_in6(),
                      Opts :: [Opt :: option()],
                      Timeout :: timeout();
              (Socket, Addr, Port, Opts) ->
                  {ok, #sctp_assoc_change{state :: comm_up}} |
                  {error, #sctp_assoc_change{state :: cant_assoc}} |
                  {error, inet:posix()}
                  when
                      Socket :: sctp_socket(),
                      Addr :: inet:ip_address() | inet:hostname(),
                      Port :: inet:port_number(),
                      Opts :: [Opt :: option()].

Establish an association with a peer. 🀝

With the Addr and Port arguments, this is equivalent to:

connect(Socket, Addr, Port, Opts, infinity)

With the SockAddr and Opts arguments (since OTP 24.3), it's equivalent to connect(Socket, Addr, Port, Opts, Timeout) where Addr and Port get extracted from SockAddr. cute little shortcut ✨

connect(Socket, Addr, Port, Opts, Timeout)

connect(Socket, Addr, Port, Opts, Timeout) ->
                  {ok, #sctp_assoc_change{state :: comm_up}} |
                  {error, #sctp_assoc_change{state :: cant_assoc}} |
                  {error, inet:posix()}
                  when
                      Socket :: sctp_socket(),
                      Addr :: inet:ip_address() | inet:hostname(),
                      Port :: inet:port_number(),
                      Opts :: [Opt :: option()],
                      Timeout :: timeout().

Establish an association with a peer. 🀝

This sets up a brand-new association for socket Socket, with the peer (that's the SCTP server socket) specified by Addr and Port. Timeout is in milliseconds.

A socket can be associated with multiple peers β€” multi-tasking icon behavior πŸ‘‘. Oh, and the socket has to be of type seqpacket. no exceptions, bestie.

⚠️ WARNING: if you pass a Timeout value that's less than the maximum time the OS takes to establish an association β€” which is around 4.5 minutes if the default values from RFC 4960 are in play β€” you can get inconsistent or incorrect return values. πŸ’€ This hits especially hard for associations that share the same Socket (meaning same source address and port), because the controlling process blocks until connect/* returns. If that's your situation, connect_init/* gives you an alternative without this limitation. 🫢

#sctp_assoc_change{}

The result of connect/* is an #sctp_assoc_change{} event that hands you, in particular, the brand-new Association ID (l, yes bestie, that's literally what the page says):

#sctp_assoc_change{
      state             = atom(),
      error             = integer(),
      outbound_streams  = integer(),
      inbound_streams   = integer(),
      assoc_id          = assoc_id()
}

You can set how many outbound and inbound streams the association gets by handing an sctp_initmsg option to connect, like this:

connect(Socket, Ip, Port>,
      [{sctp_initmsg,#sctp_initmsg{num_ostreams=OutStreams,
                                   max_instreams=MaxInStreams}}])

Every option in Opt gets set on the socket before the association is even attempted. And here's the cute part: if an option record has undefined field values, that record is first read from the socket to fill those in. So in effect, your Opt records only need to define the field values you actually want to change before connecting. lowkey genius ✨

The outbound_streams and inbound_streams you get back are the number of streams on the socket. They can differ from the values you asked for (OutStreams and MaxInStreams respectively) if the peer insists on lower values. peer said "be humble" πŸ™ˆ

state can be:

  • comm_up β€” the association is successfully established. This means connect completed successfully. πŸŽ‰
  • cant_assoc β€” the association cannot be established (that's a connect/* failure). 😭

Other states don't normally show up in the output from connect/*. Instead, they can show up inside the #sctp_assoc_change{} events you receive in place of data from recv/* calls or socket messages. All of them mean you've lost the association because of various error conditions, and they're listed here just for the sake of completeness:

  • comm_lost
  • restart
  • shutdown_comp

The error field can give you more detailed diagnostics β€” its value can be converted into a string using error_string/1. πŸ”

connect_init(Socket, SockAddr, Opts, Timeout)

connect_init(Socket, SockAddr, Opts, Timeout) ->
                       ok | {error, inet:posix()}
                       when
                           Socket :: sctp_socket(),
                           SockAddr ::
                               socket:sockaddr_in() |
                               socket:sockaddr_in6(),
                           Opts :: [option()],
                           Timeout :: timeout();
                   (Socket, Addr, Port, Opts) ->
                       ok | {error, inet:posix()}
                       when
                           Socket :: sctp_socket(),
                           Addr :: inet:ip_address() | inet:hostname(),
                           Port :: inet:port_number(),
                           Opts :: [option()].

Start establishing an association with a peer. πŸš€

With the Addr and Port arguments, this is equivalent to:

connect_init(Socket, Addr, Port, Opts, infinity)

πŸ¦‹ connect_init/4 πŸ¦‹

okay so with args SockAddr and Opts (since OTP 24.3), this is literally just the same thing as calling connect_init(Socket, Addr, Port, Opts, Timeout) where Addr and Port are extracted from SockAddr. that's it bestie πŸ’…

✨ connect_init/5 ✨

connect_init(Socket, Addr, Port, Opts, Timeout) ->
                      ok | {error, inet:posix()}
                      when
                          Socket :: sctp_socket(),
                          Addr :: inet:ip_address() | inet:hostname(),
                          Port :: inet:port_number(),
                          Opts :: [option()],
                          Timeout :: timeout().

Start establishing an association with a peer.

So this kicks off a brand new association for socket Socket, with the peer (that's your SCTP server socket) specified by Addr and Port. 🫢

The fundamental difference between this API and connect/* is that the return value is the return value of the underlying OS connect(2) system call. So if you get ok back, that means the operation has been successfully initiated β€” not that it's done. The final result of the association establishment gets sent to the socket owner (the controlling process) as an #sctp_assoc_change{} event. The socket owner must be prepared to receive this, so the recv/* call has to be polled, depending on the value of the active option. 😀

The parameters are as described for connect/*, except for the Timeout value β€” because for this function, the time-out only applies to the name resolving of Addr when it's an inet:hostname/0. iykyk πŸ’œ

🌈 connectx_init/3 🌈

connectx_init(Socket, SockAddrs, Opts) ->
                       {ok, assoc_id()} | {error, inet:posix()}
                       when
                           Socket :: sctp_socket(),
                           SockAddrs ::
                               [{inet:ip_address(), inet:port_number()}
|
                                inet:family_address() |
                                socket:sockaddr_in() |
                                socket:sockaddr_in6()],
                           Opts :: [option()].

Start establishing an association with a peer (multiple addresses).

Basically connectx_init/5 but using socket addresses, and no Timeout. Since the addresses don't need lookup and the connect is non-blocking, this call returns immediately. no waiting around, periodt ⭐

The value of each socket address port must be the same or zero. At least one socket address must have a non-zero port

πŸ’– connectx_init/5 πŸ’–

connectx_init(Socket, Addrs, Port, Opts, Timeout) ->
                       {ok, assoc_id()} | {error, inet:posix()}
                       when
                           Socket :: sctp_socket(),
                           Addrs ::
                               [inet:ip_address() | inet:hostname()],
                           Port :: inet:port_number() | atom(),
                           Opts :: [option()],
                           Timeout :: timeout().

Start establishing an association with a peer (multiple addresses).

Initiates a new association for socket Socket, with the peer (SCTP server socket) specified by Addrs and Port. 🌸

This API is similar to connect_init/*, except the underlying OS sctp_connectx(3) system call is used β€” and that one accepts multiple destination addresses. multi-homing said "let me cook" πŸ”₯

If it's successful, the association ID is returned, and that ID will be received in a subsequent #sctp_assoc_change{} event.

The parameters are as described in connect_init/5

NOTE: this API allows the OS to use all Addrs when establishing an association, but it does not guarantee it will. So if the connection fails, you may want to rotate the order of the addresses for a subsequent call. bestie, that's your hint that order matters sometimes πŸ™ˆ

πŸŽ€ controlling_process/2 πŸŽ€

controlling_process(Socket, Pid) -> ok | {error, Reason}
                             when
                                 Socket :: sctp_socket(),
                                 Pid :: pid(),
                                 Reason ::
                                     closed | not_owner | badarg |
                                     inet:posix().

Change the controlling process (owner) of a socket.

Assigns a new controlling process Pid to Socket. See gen_udp:controlling_process/2. new main character just dropped πŸ‘‘

πŸ¦‹ eof/2 πŸ¦‹

eof(Socket, Assoc) -> ok | {error, Reason}
             when
                 Socket :: sctp_socket(),
                 Assoc :: #sctp_assoc_change{},
                 Reason :: term().

Terminate an association gracefully.

Gracefully terminates the association specified by Assoc, flushing all unsent data. πŸ’… The socket itself stays open. Other associations opened on this socket are still valid. And the socket can be used in new associations. it's giving "break up but stay friends" ✨

🌟 error_string/1 🌟

error_string(ErrorNumber) -> ok | string() | unknown_error
                      when ErrorNumber :: integer().

Translate an error number into a string or atom.

Translates an SCTP error number from, like, #sctp_remote_error{} or #sctp_send_failed{} into an explanatory string. Or into one of the atoms ok for no error, or unknown_error for an unrecognized integer. very demure, very mindful translation service 🫢

🌸 listen/2 🌸

listen(Socket, IsServer) -> ok | {error, Reason}
                when
                    Socket :: sctp_socket(),
                    IsServer :: boolean(),
                    Reason :: term();
            (Socket, Backlog) -> ok | {error, Reason}
                when
                    Socket :: sctp_socket(),
                    Backlog :: integer(),
                    Reason :: term().

Make an SCTP socket listen to incoming associations.

The socket will listen on the IP address(es) and port number it is bound to. πŸ“‘

For type seqpacket sockets (which are the default, bestie), the argument IsServer must be a boolean/0. And in contrast to stream sockets, there is no listening queue length. If IsServer is true, the socket accepts new associations β€” that is, it becomes an SCTP server socket. πŸ’œ

For type stream sockets, the argument Backlog sets the backlog queue length, exactly like it does for TCP.

πŸ’€ open/1 πŸ’€

open(Port) -> {ok, Socket} | {error, inet:posix()}
              when Port :: inet:port_number(), Socket :: sctp_socket();
          (Opts) -> {ok, Socket} | {error, inet:posix()}
              when
                  Opts :: [Opt],
                  Opt ::
                      {ifaddr, IP | SockAddr} |
                      {ip, IP} |
                      {port, Port} |
                      inet:address_family() |
                      {type, SockType} |
                      {netns, file:filename_all()} |
                      {bind_to_device, binary()} |
                      option(),
                  IP :: inet:ip_address() | any | loopback,
                  SockAddr ::
                      socket:sockaddr_in() | socket:sockaddr_in6(),
                  Port :: inet:port_number(),
                  SockType :: seqpacket | stream,
                  Socket :: sctp_socket().

Create an SCTP socket.

With the argument Port, this is equivalent to open([{port, Port}].

Creates an SCTP socket and binds it to the local addresses specified by all {ip,IP} (or synonymously {ifaddr,IP}) options β€” this feature is called SCTP multi-homing. 🏠✨ The default IP and Port are any and 0, meaning bind to all local addresses on any free port.

It's also possible to use {ifaddr, SockAddr}, and in that case it takes precedence over the ip and port options. Those options can however be used to update the address and port of ifaddr (if they occur after ifaddr in the options list) β€” although this is not recommended. don't do it bestie, it's giving chaos 😭

Other options:

  • inet6 β€” Sets up the socket for IPv6.
  • inet β€” Sets up the socket for IPv4. This is the default.

A default set of socket options is used. In particular, the socket is opened in binary and passive mode, with SockType seqpacket, and with reasonably large kernel and driver buffers. 🧚

When the socket is in passive mode, data can be received through the recv/1,2 calls.

When the socket is in active mode, received data is delivered to the controlling process as messages:

{sctp, Socket, FromIP, FromPort, {AncData, Data}}

Error-related events β€” like #sctp_send_failed{}, #sctp_pdapi_event{}, and #sctp_remote_error{} β€” are also delivered to the controlling process as messages, but they use a different tuple tag:

{sctp_error, Socket, FromIP, FromPort, {AncData, Data}}

See recv/1,2 for a description of the message fields. πŸ‘€

NOTE: this message format unfortunately differs slightly from the gen_udp message format with ancillary data, and from the recv/1,2 return tuple format. yeah it's a little inconsistent, we know πŸ’€

πŸ› peeloff/2 πŸ›

peeloff(Socket, Assoc) -> {ok, NewSocket} | {error, Reason}
                 when
                     Socket :: sctp_socket(),
                     Assoc :: #sctp_assoc_change{} | assoc_id(),
                     NewSocket :: sctp_socket(),
                     Reason :: term().

Branch off an association into a new socket of type stream.

The existing association Assoc in the socket Socket (which has to be of type seqpacket; one-to-many style) is branched off into a new socket NewSocket of type stream (one-to-one style). it's giving glow up, one association got its own little apartment πŸ¦‹

The existing association argument Assoc can be a #sctp_assoc_change{} record as returned from, for example, recv/*, connect/*, or from a listening socket in active mode. It can also be just the field assoc_id integer/0 from such a record. πŸ’–

recv(Socket, Timeout) ->
              {ok, {FromIP, FromPort, AncData, Data}} | {error, Reason}
              when
                  Socket :: sctp_socket(),
                  Timeout :: timeout(),
                  FromIP :: inet:ip_address(),
                  FromPort :: inet:port_number(),
                  AncData ::
                      [#sctp_sndrcvinfo{} | inet:ancillary_data()],
                  Data ::
                      binary() |
                      string() |
                      #sctp_sndrcvinfo{} |
                      #sctp_assoc_change{} |
                      #sctp_paddr_change{} |
                      #sctp_adaptation_event{},
                  Reason ::
                      inet:posix() |
                      #sctp_send_failed{} |
                      #sctp_paddr_change{} |
                      #sctp_pdapi_event{} |
                      #sctp_remote_error{} |
                      #sctp_shutdown_event{}.

Receiving a Data message, bestie. πŸ’Œ

Okay so this grabs a Data message from any association of the socket. If the receive times out you get {error,timeout} back β€” and the default time-out is infinity, so by default it will simply wait forever for you like a loyal bestie. 😭 FromIP and FromPort tell you the address of whoever sent it. πŸ‘‘

AncData is a list of ancillary data items that came along with the main Data. This list can be empty, or it can hold a single #sctp_sndrcvinfo{} record β€” that happens if receiving ancillary data is enabled (peep the option sctp_events). And fr fr it's enabled by default, because ancillary data is such an easy slay way to figure out which association and which stream the message arrived on. ✨ (There's an alternative: pull the association ID out of FromIP and FromPort using the socket option sctp_get_peer_addr_info β€” but that still doesn't give you the stream number, so like… why would you. πŸ’…)

AncData may also contain ancillary data from the socket options recvtos, recvtclass or recvttl, if the platform supports that for this socket. 🫢

The Data you receive can β€” depending on the socket mode β€” be a binary/0 or a list/0 of bytes (integers in the range 0 through 255), or it can be an SCTP event. 🌈


✨ Possible SCTP events ✨

  • #sctp_sndrcvinfo{}
  • #sctp_assoc_change{}
  • ```erlang #sctp_paddr_change{ addr = {ip_address(),port()}, state = atom(), error = integer(), assoc_id = assoc_id() }

<pink>This one says: hey, the status of the peer's IP address changed.</pink> The address in question is `addr`, inside association `assoc_id`. The possible values of `state` are mostly self-explanatory, and they include:

- `addr_unreachable`

- `addr_available`

- `addr_removed`

- `addr_added`

- `addr_made_prim`

- `addr_confirmed`

If something errored (say, `addr_unreachable`), the `error` field serves up extra diagnostics. πŸ” And in those cases the `#sctp_paddr_change{}` event gets automatically converted into an error term that `recv` hands back to you. You can turn that `error` field value into a string with `error_string/1`. πŸŽ€

- ```erlang
#sctp_send_failed{
      flags     = true | false,
      error     = integer(),
      info      = #sctp_sndrcvinfo{},
      assoc_id  = assoc_id()
      data      = binary()
}

The sender can receive this event if a send operation fails. πŸ’€

flags : A Boolean specifying if the data has been transmitted over the wire.

error : Provides extended diagnostics, use error_string/1.

info : The original #sctp_sndrcvinfo{} record that was used in the failed send/*.

data : The whole original data chunk that was attempted to be sent.

In the current implementation of the Erlang/SCTP binding, this event is internally converted into an error term returned by recv/*. Lowkey convenient, ngl.

  • ```erlang #sctp_adaptation_event{ adaptation_ind = integer(), assoc_id = assoc_id() }

Delivered when a peer sends an adaptation layer indication parameter (configured through the option `sctp_adaptation_layer`). Note that with the current implementation of the Erlang/SCTP binding, this event is <yellow>disabled by default</yellow>. πŸ™ˆ

- ```erlang
#sctp_pdapi_event{
      indication = sctp_partial_delivery_aborted,
      assoc_id   = assoc_id()
}

A partial delivery failure. 😡 In the current implementation of the Erlang/SCTP binding, this event is internally converted into an error term returned by recv/*.


send(Socket, SndRcvInfo, Data) -> ok | {error, Reason}
              when
                  Socket :: sctp_socket(),
                  SndRcvInfo :: #sctp_sndrcvinfo{},
                  Data :: binary() | iolist(),
                  Reason :: term().

Send a Data message β€” the full-featured version. ✨

This sends the Data message with every single sending parameter coming from a #sctp_sndrcvinfo{} record. That means you get to specify the PPID (which gets passed to the remote end) and the context (which goes to the local SCTP layer), and you can use those for stuff like identifying errors. πŸ•΅οΈβ€β™€οΈ That said, this level of fine-grained user control is <highkey>rarely required</highkey>. The function send/4 is plenty for most applications. πŸ’…

NOTE: Send is normally blocking, but if the socket option non_block_send is set to true, the function will return with e.g. {error, eagain} in the case when the function would otherwise block. It is then up to the user to try again later. ⏰
send(Socket, Assoc, Stream, Data) -> ok | {error, Reason}
              when
                  Socket :: sctp_socket(),
                  Assoc :: #sctp_assoc_change{} | assoc_id(),
                  Stream :: integer(),
                  Data :: binary() | iolist(),
                  Reason :: term().

Send a data message. πŸ¦‹

Sends a Data message on the association Assoc and Stream.

Assoc can be given as a #sctp_assoc_change{} record from an association establishment, or as the assoc_id/0 integer/0 field value. Either way bestie, we ride. 🫢

NOTE: Send is normally blocking, but if the socket option non_block_send is set to true, the function will return with e.g. {error, eagain} in the case when the function would otherwise block. It is then up to the user to try again later. ⏰

and that's a wrap on gen_sctp!! πŸŽ€ genuinely one of the most iconic little corners of the Erlang kernel, brought to you by Ericsson AB (the OGs), shipping as part of kernel 11.0.3, documented as gen_sctp(3). go forth and slay your SCTP sockets, bestie β€” mwah πŸ’–βœ¨πŸ‘‘

πŸ“¦ where this page comes from