An overview of the Python SCTP module

Description

This module is a wrapper for libraries implementing the SCTP protocol and available on Linux and FreeBSD. This module strictly implements the API described in RFC 6458. It consists of a pure Python module sctp and three C-modules _sctp.sctp, _sctp.sctp_info and _sctp.sctp_event.

Note

The SCTP module does not implement everything marked as deprecated in RFC 6458 (e.g., SCTP_USE_EXT_RCVINFO socket option, sctp_sendmsg() function,…).

The core class is the sctp_socket class. It inherits all attributes and methods from the socket.socket class in the Python socket module (except for the method accept() - see note below). To create such a class:

>>> from socket import *
>>> from sctp import *
>>> sock = sctp_socket(AF_INET, SOCK_STREAM)
>>> sock.connect(('localhost', 20000)) # method inherited
>>> data, addr, info, infotype, flags = sock.sctp_recvv(1024) # new method
...
>>> sock.close()  # method inherited

Note

Method accept() of class sctp_socket behaves in the same way as the standard method accept(), i.e., it takes no arguments and returns a pair (conn, address). The only difference is that conn is a sctp_socket object and not a socket.socket object.

RFC 6458 defines a number of socket options and, where applicable, the corresponding C structures, the latter being available as classes, with the same name, inheriting from dict. The same applies to SCTP events and notifications, which are also implemented as classes inheriting from dict.

Warning

Unlike the standard Python module socket module, IPv6 addresses are necessarily 4-tuples of the form (host, port, flowinfo, scope_id), see IP addresses for details.

Class diagrams

Module sctp

digraph {
  rankdir=LR
  edge [arrowhead="vee", color="#5a6062"]
  node [shape="box", style=filled, fillcolor="#ffe7d4", color="#5a6062"]
  a [label="sctp.sctp_socket"]
  b [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#e9f6f1"
       align="left">
       <tr><td port="m0" border="1" bgcolor="#aee3f3">Methods</td></tr>
       <tr><td border="1" align="left">getsockopt()</td></tr>
       <tr><td border="1" align="left">setsockopt()</td></tr>
       <tr><td border="1" align="left">sctp_opt_info()</td></tr>
       </table>>
 ]
  a -> b:m0
  c [label="_sctp.sctp.sctp_socket"]
  d [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#e9f6f1"
       align="left">
       <tr><td port="m0" border="1" bgcolor="#aee3f3">Properties</td></tr>
       <tr><td border="1" align="left">sctp_type</td></tr>
       <tr><td border="1" align="left">sock</td></tr>
       <tr><td border="1" bgcolor="#aee3f3">Methods</td></tr>
       <tr><td border="1" align="left">sctp_bindx()</td></tr>
       <tr><td border="1" align="left">sctp_connectx()</td></tr>
       <tr><td border="1" align="left">sctp_getladdrs()</td></tr>
       <tr><td border="1" align="left">sctp_getpaddrs()</td></tr>
       <tr><td border="1" align="left">sctp_recvv()</td></tr>
       <tr><td border="1" align="left">sctp_sendv()</td></tr>
       <tr><td border="1" align="left">sctp_peeloff()</td></tr>
       </table>>
  ]
  c -> d:m0
  o [label="object"]
  {
    edge [constraint=false]
    a -> c -> o
  }
}
digraph {
  rankdir=LR
  edge [arrowhead="vee", arrowtail="vee", color="#5a6062"]
  node [shape="box", style=filled, fillcolor="#ffe7d4", color="#5a6062"]
  g [label="sctp.sctp_*"]
  h [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#ffe7d4"
       align="left">
       <tr><td border="1" align="left">sctp_initmsg</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_paddrparams</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_paddrinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_rtoinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_assocparams</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_setprim</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_setadaptation</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_assoc_value</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_authkeyid</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_sack_info</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_default_sndinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_default_prinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_status</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_setpeerprim</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_authchunk</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_authkey</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_event</td></tr>
       </table>>
  ]
  g -> h [dir=both]
  a [label="sctp.sctp_generic"]
  b [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#e9f6f1"
       align="left">
       <tr><td port="m0" border="1" bgcolor="#aee3f3">Properties</td></tr>
       <tr><td border="1" align="left">fmt</td></tr>
       <tr><td border="1" align="left">size</td></tr>
       <tr><td border="1" align="left">to_bytes</td></tr>
       <tr><td border="1" bgcolor="#aee3f3">Methods</td></tr>
       <tr><td border="1" align="left">set_value()</td></tr>
       </table>>
  ]
  a -> b:m0
  {
    edge [constraint=false]
    c [label="dict"]
    g -> a -> c
  }
}

Module sctp_info

digraph {
  rankdir=LR
  edge [arrowhead="vee", arrowtail="vee", color="#5a6062"]
  node [shape="box", style=filled, fillcolor="#ffe7d4", color="#5a6062"]
  g [label="_sctp.sctp_info.sctp_*"]
  h [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#ffe7d4"
       align="left">
       <tr><td border="1" align="left">sctp_sndinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_prinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_authinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_sendv_spa</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_rcvinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_nxtinfo</td></tr>
       <tr><td bgcolor="white" height="5px"></td></tr>
       <tr><td border="1" align="left">sctp_recvv_rn</td></tr>
       </table>>
  ]
  g -> h [dir=both]
  {
    edge [constraint=false]
    a [label="dict"]
    g -> a
  }
}

Module sctp_event

digraph {
  rankdir=LR
  edge [arrowhead="vee", color="#5a6062"]
  node [shape="box", style=filled, fillcolor="#ffe7d4", color="#5a6062"]
  a [label="_sctp.sctp_event"]
  b [
       style=none
       shape = none
       label = <<table border="0" cellspacing="0" bgcolor="#e9f6f1"
       align="left">
       <tr><td port="m0" border="1" bgcolor="#aee3f3">Methods</td></tr>
       <tr><td border="1" align="left">parse_notification()</td></tr>
       </table>>
  ]
  a -> b:m0
}

Some examples

A simple echo client/server

This is the echo client/server revisited. Here, both the client and the server use different streams for sending and receiving data.

echo_client.py
from socket import *
from sctp import *
from enum import IntEnum

ADDR = '127.0.0.1'
PORT = 20000
DATA = b'Some data to be echoed'

class Streams(IntEnum):
    IN =  0
    OUT = 1

sock = sctp_socket(AF_INET, SOCK_STREAM)
sock.setsockopt(IPPROTO_SCTP, SCTP_RECVRCVINFO, 1)

info = sctp_sndinfo(snd_sid=Streams.OUT)
sock.sctp_sendv((DATA,), ((ADDR, PORT),), info)

data, addr, info, infotype, flags = sock.sctp_recvv(1024)
if infotype == SCTP_RECVV_RCVINFO and info['rcv_sid'] == Streams.IN:
    print(f'Received from {addr}:\n  {data}')

The client sends data on stream Streams.OUT (1) and receives it on stream Streams.IN (0).

echo_server.py
from socket import *
from sctp import *
from enum import IntEnum

ADDR = '0.0.0.0'
PORT = 20000

class Streams(IntEnum):
    IN =  1
    OUT = 0

sock = sctp_socket(AF_INET, SOCK_STREAM)
sock.bind((ADDR, PORT))
sock.listen(1)

while True:
    s, addr = sock.accept()
    s.setsockopt(IPPROTO_SCTP, SCTP_RECVRCVINFO, 1)
    
    data, addr, info, infotype, _ = s.sctp_recvv(1024)
    if infotype == SCTP_RECVV_RCVINFO and info['rcv_sid'] == Streams.IN:
        sndinfo = sctp_sndinfo(snd_sid=Streams.OUT)
        s.sctp_sendv((data,), (addr,), sndinfo)
    s.close()
sock.close()

The server sends data on stream Streams.OUT (0) and receives it on stream Streams.IN (1).

The Python version of the example shown in Appendix A of RFC 6458

one-to-one.py
from socket import *
from sctp import *

PORT = 9
ADDR = '127.0.0.1'
SIZE_OF_MESSAGE = 1000
NUMBER_OF_MESSAGES = 10
PPID = 1234

# Create a one-to-one style SCTP socket.
sd = sctp_socket(AF_INET, SOCK_STREAM)

# Prepare for requesting 2048 outgoing streams.
init = sctp_initmsg(sinit_num_ostreams=2048)
sd.setsockopt(IPPROTO_SCTP, SCTP_INITMSG, init)

ind = sctp_setadaptation(ssb_adaptation_ind=0x01020304)
sd.setsockopt(IPPROTO_SCTP, SCTP_ADAPTATION_LAYER, ind)

# Connect to the discard server.
sd.connect((ADDR, PORT))

# Get the actual number of outgoing streams.
status = sd.getsockopt(IPPROTO_SCTP, SCTP_STATUS, sctp_status())

info = sctp_sndinfo(snd_ppid=htonl(PPID), snd_flags=SCTP_UNORDERED)
buffer = b'A' + bytes(SIZE_OF_MESSAGE - 1)
for i in range(NUMBER_OF_MESSAGES):
    info['snd_sid'] = i % status['sstat_outstrms']
    sd.sctp_sendv((buffer,), None, info)
sd.close()

To test this script with discard server on Linux, you must first start this server. This requires two steps (as root):

  1. Edit file /etc/xinetd.d/discard and set disable=no for TCP protocol.

  2. Start discard server with command withsctp:

    # withsctp xinetd
    

And finally, run the script as a non-privileged user:

$ python3 one_to_one.py

The Python version of the example shown in Appendix B of RFC 6458

one-to-many.py
from socket import *
from sctp import *

BUFFER_SIZE = (1 << 16)
PORT = 9
ADDR = '::'
TIMEOUT = 5
   
def print_notification(buf):
    data = parse_notification(buf)
    if data['sn_type'] == SCTP_ASSOC_CHANGE:
        if data['sac_state'] == SCTP_COMM_UP:
            print(
                "Communication up (streams (in/out)="
                f"({data['sac_inbound_streams']}/"
                f"{data['sac_outbound_streams']}))."
            )
        elif data['sac_state'] == SCTP_COMM_LOST:
            print(f"Communication lost (error={data['sac_error']}).")
        elif data['sac_state'] == SCTP_RESTART:
            print(
                "Communication restarted (streams (in/out)="
                f"({data['sac_inbound_streams']}/"
                f"{data['sac_outbound_streams']}))."
            )
        elif data['sac_state'] == SCTP_SHUTDOWN_COMP:
            print("Communication completed.")
        elif data['sac_state'] == SCTP_CANT_STR_ASSOC:
            print("Communication couldn't be started.")
    elif data['sn_type'] == SCTP_PEER_ADDR_CHANGE:
        print(f"^^^ Peer Address change: {data['spc_aaddr']} ", end='')
        if data['spc_state'] == SCTP_ADDR_AVAILABLE:
            print("is available.")
        elif data['spc_state'] == SCTP_ADDR_UNREACHABLE:
            print(f"is not available (error={data['spc_error']}).")
        elif data['spc_state'] == SCTP_ADDR_REMOVED:
            print("was removed.")
        elif data['spc_state'] == SCTP_ADDR_ADDED:
            print("was added.")
        elif data['spc_state'] == SCTP_ADDR_MADE_PRIM:
            print("is primary.")
    elif data['sn_type'] == SCTP_SHUTDOWN_EVENT:
        print("^^^ Shutdown received.")
    elif data['sn_type'] == SCTP_ADAPTATION_INDICATION:
        print(
            f"^^^ Adaptation indication {data['sai_adaptation_ind']:08x}"
              " received."
        )

def main():
    event_types = (
        SCTP_ASSOC_CHANGE,
        SCTP_PEER_ADDR_CHANGE,
        SCTP_SHUTDOWN_EVENT,
        SCTP_ADAPTATION_INDICATION
        )
    sd = sctp_socket(AF_INET6, SOCK_SEQPACKET)
    event = sctp_event(se_assoc_id=SCTP_FUTURE_ASSOC, se_on=1)
    for e in event_types:
        event['se_type'] = e
        sd.setsockopt(IPPROTO_SCTP, SCTP_EVENT, event)
    sd.setsockopt(IPPROTO_SCTP, SCTP_AUTOCLOSE, TIMEOUT)
    sd.setsockopt(IPPROTO_SCTP, SCTP_RECVRCVINFO, 1)
    sd.bind((ADDR, PORT))
    sd.listen(1)
    while True:
        data, addr, info, infotype, flags = sd.sctp_recvv(BUFFER_SIZE)
        if flags & MSG_NOTIFICATION:
            print_notification(data)
        else:
            print(
                f"Message received from {addr[0]}:{addr[1]}: "
                f"len={len(data)}", end=''
            )
            if infotype == SCTP_RECVV_RCVINFO:
                print(f", sid={info['rcv_sid']}")
                if (info['rcv_flags'] & SCTP_UNORDERED):
                    print(", unordered", end='')
                else: 
                    print(f", ssn={info['rcv_ssn']}", end='')
                print(f", tsn={info['rcv_tsn']}", end='')
                print(f", ppid={ntohl(info['rcv_ppid'])}.")
            elif infotype in (
                    SCTP_RECVV_NOINFO, SCTP_RECVV_NXTINFO, SCTP_RECVV_RN):
                print(".")
    sd.close()
    
if __name__ == '__main__':
    main()
    

To test this script, first run it as root (port=9 is a priviliged one):

root window
# python3 one-to-many.py

In another window, run one_to_one.py as a non-privileged user:

user window
$ python3 one_to_one.py

In root window, you will see something like that:

root window
# python3 one-to-many.py
Communication up (streams (in/out)=(2048/10)).
^^^ Adaptation indication 01020304 received.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=0
, unordered, tsn=3093043390, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=1
, unordered, tsn=3093043391, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=2
, unordered, tsn=3093043392, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=3
, unordered, tsn=3093043393, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=4
, unordered, tsn=3093043394, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=5
, unordered, tsn=3093043395, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=6
, unordered, tsn=3093043396, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=7
, unordered, tsn=3093043397, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=8
, unordered, tsn=3093043398, ppid=1234.
Message received from ::ffff:127.0.0.1:52378: len=1000, sid=9
, unordered, tsn=3093043399, ppid=1234.
^^^ Shutdown received.
Communication completed.