Skip to content

Diameter

The diameter namespace exposes the IMS Diameter interfaces — Cx (HSS), Rx (PCRF), Sh (HSS AS), and Rf (offline charging) — plus a unified inbound @diameter.on_request hook for serving requests (RAR, PNR, ASR, …).

from siphon import diameter

@diameter.on_request
async def handle(request):
    if request.command_name == "RAR":
        return request.answer(2001)
    return request.reject(3002)

What siphon answers when the handler does not

A request the script does not answer itself gets one of three codes, and they are meant to be distinguishable at the peer:

Result-Code When
3002 DIAMETER_UNABLE_TO_DELIVER Nothing serves the request: no @diameter.on_request matched its application and command, or the handler that matched returned None.
5012 DIAMETER_UNABLE_TO_COMPLY siphon has a handler and could not carry it out: it raised, returned something that is not a DiameterAnswer, or produced an answer that would not serialize. Logged at error with the handler's name and the exception.
5014 DIAMETER_INVALID_AVP_LENGTH The inbound message did not parse.

These three are the generic answer-message of RFC 6733 section 7.2, with the header E bit set and an Error-Message saying which case it was, because siphon cannot compose the answer of a command it has no handler's answer for. An answer the handler builds is different: request.answer(code) and request.reject(code) set the E bit for a protocol error (3xxx) only. A permanent failure (5xxx) a handler answers, such as request.answer(5012) to a Registration-Termination-Request for a user it does not serve, is an answer of the command and goes out with the bit clear (section 7.1.5).

The 3002/5012 split matters most on Ro, where a 3002 to a CCR-UPDATE is read as a credit denial and the call is torn down. A handler that raises is a fault on siphon's side of the interface, not a decision about the subscriber's credit, so it answers 5012 — which lets an operator tell a script fault from an OCS denial instead of seeing every live call die at its first re-authorisation with nothing pointing at the script.

Returning None stays 3002 on purpose: declining is a routing answer, and it is the documented way for a handler to say "not mine".

Application names

A filter on @diameter.on_request can name the application as well as the command ("S6a:ULR", "Gx:CCR"), and diameter.send_request(..., application=...) and diameter.routes[].application take the same names. They are case-insensitive in a script and lowercase in siphon.yaml.

Name Application-Id Vendor-Id Specification
Cx 16777216 10415 TS 29.229
Sh 16777217 10415 TS 29.329
Rx 16777236 10415 TS 29.214
Gx 16777238 10415 TS 29.212
S6a 16777251 10415 TS 29.272
S13 16777252 10415 TS 29.272
S6c 16777312 10415 TS 29.338
SGd 16777313 10415 TS 29.338
Zh 16777221 10415 TS 29.109
Zn 16777220 10415 TS 29.109
Ro 4 none RFC 8506, TS 32.299
Rf 3 none RFC 6733, TS 32.299

Some command codes belong to more than one application: Credit-Control (272) is both Ro and Gx, Re-Auth (258) both Rx and Gx, Multimedia-Auth (303) both Cx and Zh. A bare "CCR" filter matches the command on any application; "Gx:CCR" and "Ro:CCR" match only their own Application-Id, so one script can serve both without either handler seeing the other's requests.

@diameter.on_request("Gx:CCR")
def gx_credit_control(request):
    return request.answer(2001)

@diameter.on_request("Ro:CCR")
def ro_credit_control(request):
    return request.answer(2001)

An application listed under diameter.routes is advertised to that peer in the CER: the 3GPP ones as a Vendor-Specific-Application-Id holding Vendor-Id 10415 and the Auth-Application-Id, Ro as a bare Auth-Application-Id, Rf as a bare Acct-Application-Id.

Capabilities exchange: the applications siphon advertises

A node that accepts Diameter connections (diameter.listen with clients) lists the applications it serves in the Capabilities-Exchange-Answer, as RFC 6733 §5.3 requires. The list has two sources:

  1. Every application named in a registered @diameter.on_request filter: @diameter.on_request("S6a:AIR") makes the node an S6a server.
  2. The applications list in the configuration, for what the filters do not say.

A bare-command filter ("RAR") or a catch-all handler names no application, so it adds nothing to the list. siphon does not fall back to every application its dictionary knows, because that would claim interfaces the script may not implement. A script that serves through such handlers lists its applications in the configuration, and a relay lists relay:

diameter:
  listen:
    tcp: "0.0.0.0:3868"
  origin_host: "hss.epc.mnc001.mcc001.3gppnetwork.org"
  origin_realm: "epc.mnc001.mcc001.3gppnetwork.org"
  applications: [s6a, cx]      # cx, sh, rx, gx, ro, rf, s6a, s13, s6c, sgd, zh, zn, or relay
  clients:
    - name: mme
      allowed_ips: ["192.0.2.0/24"]

With tenants:, each tenant has its own applications next to its identity. The handlers belong to the one script, so the applications their filters name are advertised to every tenant.

On the wire a 3GPP application goes out as a Vendor-Specific-Application-Id (Vendor-Id 10415 with the Auth-Application-Id) and as an Auth-Application-Id, base accounting (Rf) as an Acct-Application-Id, and relay as Auth-Application-Id 0xffffffff. The list is read from the running script at each handshake, so a reload that adds or removes a handler changes what the next connection is told.

The same list goes into the Capabilities-Exchange-Request siphon sends on the connections it opens for that tenant, servers and connect_to.

When a connecting peer is refused

siphon compares the application ids in the peer's CER with its own list. It looks at every Auth-Application-Id and Acct-Application-Id, including the ones inside a Vendor-Specific-Application-Id, and ignores the Vendor-Id.

siphon's list The peer's CER Outcome
empty anything Accepted. The CEA lists no application.
contains relay anything Accepted.
not empty contains the Relay application (0xffffffff) Accepted.
not empty shares at least one application Accepted.
not empty shares none, or lists none at all CEA with 5010 DIAMETER_NO_COMMON_APPLICATION, then the connection is closed. The CEA still lists what siphon serves.

The refusal is logged at warn with the peer's name, what it offered and what this node serves.

The first row is what siphon did for every peer before it advertised applications at all, and it is kept so that a relay or agent written as a catch-all handler keeps accepting its peers without a configuration change. Such a node answers with a CEA that names no application, which a strict peer refuses on its side, and siphon says so with a warn at startup. Setting applications (to [relay] for an agent) fixes it.

Two things follow for a script that mixes filter styles. A script with @diameter.on_request("S6a:AIR") and a catch-all for everything else advertises S6a alone, and refuses a peer that offers only Cx. List the other applications, or relay, under applications. And a peer that sends a CER with no application in it is refused by any node whose list is not empty.

Cx: registration

The I-CSCF asks the HSS which S-CSCF serves a user with diameter.cx_uar, and the S-CSCF fetches authentication vectors with a Multimedia-Auth-Request. Both commands require the private user identity as User-Name, and the MAR also the S-CSCF's own SIP URI as Server-Name (TS 29.229 clauses 6.1.1 and 6.1.7).

from siphon import auth, diameter

SCSCF_URI = "sip:scscf.ims.example.com:6060"

# I-CSCF. user_name is the username of the REGISTER's Authorization header;
# leave it out and it is derived from the public identity (TS 24.229 5.3.1.2).
result = await diameter.cx_uar(public_identity, visited_network, user_name=private_identity)

# S-CSCF, IMS AKA or SIP Digest: the MAR, the 401 challenge and the
# verification in one call.
if not await auth.require_ims_digest(request, realm=REALM, server_name=SCSCF_URI):
    return

# Or the MAR on its own, for another scheme or to hold the vector yourself.
answer = await diameter.cx_mar(public_identity, private_identity, SCSCF_URI,
                               scheme="SIP Digest")
ha1 = answer["auth_items"][0]["digest_ha1"]

auth.require_ims_digest asks for the scheme the REGISTER calls for (Digest-AKAv1-MD5, SIP Digest or Unknown, see IMS AKA or SIP Digest), takes Public-Identity from the To header of the REGISTER and User-Name from its Authorization header (or derives it), and sends the MAR to the peer the cx route names. Pass server_name: without it the MAR goes out with no Server-Name, a warning is logged, and an HSS that checks the command answers DIAMETER_MISSING_AVP. Checked against Wireshark's Diameter dissector with scripts/validate_diameter_cx_requests.sh.

What the HSS answers decides what require_ims_digest does:

The HSS answers require_ims_digest
DIAMETER_SUCCESS with an item in the scheme asked for sets the 401 challenge, returns False
DIAMETER_SUCCESS with an item that names another scheme than the one named raises RuntimeError and logs an error, no challenge
DIAMETER_SUCCESS to Unknown in a scheme that is not implemented sets 403 Forbidden, returns False, logs an error
DIAMETER_SUCCESS without SIP-Auth-Data-Item, or for IMS AKA without SIP-Authenticate or SIP-Authorization, or for SIP Digest without a usable SIP-Digest-Authenticate raises RuntimeError and logs an error, no challenge
DIAMETER_ERROR_AUTH_SCHEME_NOT_SUPPORTED (5006) sets 403 Forbidden, returns False, logs a warning
any other result sets 403 Forbidden, returns False, logs a warning with the result
nothing (transport error or timeout) raises RuntimeError

Neither H(A1) nor the response of the UE is written to the log at any level.

After a successful authentication the S-CSCF tells the HSS with diameter.cx_sar, which takes the private identity as user_name= (request.auth_user): TS 29.228 Table 6.1.2.1 requires it in the request whenever the S-CSCF has it.

await diameter.cx_sar(public_identity, SCSCF_URI, 1, user_name=request.auth_user)

IMS restoration

TS 23.380 clause 4 has the S-CSCF back a registration up in the HSS and rebuild it from there after a restart or on a reselected S-CSCF. The Cx members (TS 29.229 clause 6.3.46 to 6.3.52, 6.3.62, 6.3.63, 6.3.70, 6.3.71, 6.3.74 to 6.3.76) are keywords of diameter.cx_sar:

Keyword Sent as
ims_restoration=True feature bit 2 (IMS-Restoration-Indication) in Supported-Features
restoration_info={...} SCSCF-Restoration-Info: user_name, restoration_info (list of path, contact, optional initial_cseq_sequence_number, call_id, subscription_info, pcscf_subscription_info), optional registration_time_out (Unix seconds), sip_authentication_scheme
user_data_already_available=True User-Data-Already-Available 1, for an update of the restoration information alone (§4.6.2)
multiple_registration=True Multiple-Registration-Indication
pcscf_restoration_mechanism=True feature bit 3 (P-CSCF-Restoration-mechanism)
pcscf_restoration_indication=True SAR-Flags bit 0
failed_pcscf={"fqdn": ..., "ip_addresses": [...]} Failed-PCSCF

A malformed restoration_info or failed_pcscf raises ValueError, peer or no peer. The answer adds experimental_result_code, scscf_restoration_info (one dict per private identity, in the shape above), associated_registered_identities and supported_features. An HSS returning the restoration information of a registered user answers DIAMETER_ERROR_IN_ASSIGNMENT_TYPE (5007) as an Experimental-Result, so result_code is 5007 there.

The restoration AVPs carry the V bit and never the M bit (TS 29.229 table 6.3.1). diameter.cx_lir(public_identity, user_authorization_type=2) asks for the S-CSCF capabilities when the assigned S-CSCF cannot be contacted (TS 29.228 clause 6.1.4.1 step 2a). registrar.restore(aor, contact, *, expires, path=, call_id=, cseq=, auth_user=, aliases=) binds what came back; Contact.sip_instance and Contact.reg_id give a script what it needs to write a binding back as a Contact.

answer = await diameter.cx_sar(public_identity, SCSCF_URI, 3, ims_restoration=True)
if answer and answer["experimental_result_code"] == 5007:
    for group in answer["scscf_restoration_info"]:
        for entry in group["restoration_info"]:
            registrar.restore(public_identity, entry["contact"], expires=600,
                              path=[entry["path"]], auth_user=group["user_name"])

Wireshark's Diameter dictionary (4.6) lacks Registration-Time-Out (661) and Failed-PCSCF (664 to 666) and marks 654 and 655 M; the validation script checks those against the table and hand-written octets.

GBA: Zh and Zn

The Generic Bootstrapping Architecture (TS 33.220) has two Diameter reference points, both in TS 29.109. On Zh a Bootstrapping Server Function asks the HSS for one authentication vector and the user's GBA security settings (the GUSS). On Zn a Network Application Function asks the BSF for the key material of a bootstrapping transaction.

diameter:
  origin_host: "bsf.mnc001.mcc001.pub.3gppnetwork.org"
  origin_realm: "mnc001.mcc001.pub.3gppnetwork.org"
  peers:
    - name: hss1
      host: "192.0.2.10"
      destination_realm: "ims.mnc001.mcc001.3gppnetwork.org"
  routes:
    - application: zh          # advertised to hss1 in the CER
      peers: [hss1]

Zh: the BSF asks the HSS

Zh uses the Cx command, Multimedia-Auth (303), on its own application (16777221). The request has no Server-Name and no SIP-Number-Auth-Items, so the HSS does not run its Cx procedure and stores no S-CSCF name. Do not send a Cx MAR in its place.

from siphon import diameter

answer = await diameter.zh_mar(impi)
if answer is None:
    ...                                    # no zh peer, or no answer in time
elif answer["experimental_result_code"] == 5401:
    ...                                    # DIAMETER_ERROR_IDENTITY_UNKNOWN
else:
    vector = answer["auth_items"][0]       # the HSS provides one
    rand, autn = vector["authenticate"][:16], vector["authenticate"][16:]
    xres = vector["authorization"]
    ck, ik = vector["confidentiality_key"], vector["integrity_key"]
    guss = answer["guss"]                  # the GBA-UserSecSettings XML, or None

# After a synchronisation failure: RAND as sent, then the AUTS of the terminal.
answer = await diameter.zh_mar(impi, authorization=rand + auts)

For IMS AKA the request carries no SIP-Auth-Data-Item: TS 29.109 clause 4.2 defines its absence as Digest-AKAv1-MD5 and advises against sending it. authorization= (30 octets, TS 29.229 clause 6.3.11) adds the item with SIP-Authorization, scheme= names another scheme, and guss_timestamp= (Unix seconds) sends the timestamp of the GUSS already held, to which the HSS answers guss "GUSS TIMESTAMP EQUAL" when nothing changed. GUSS-Timestamp goes out without the M bit, as Table 6.1 of TS 29.109 requires; every other GBA AVP carries it.

A Grouped AVP through send_request

diameter.send_request takes a Grouped AVP as a dict of its members, each keyed and typed like a top-level keyword, in the order of the dict. A member can be a dict itself. A Time AVP takes Unix seconds. The Zh request above, in the generic form:

answer = await diameter.send_request(
    "Multimedia-Auth-Request", "Zh",
    user_name=impi,
    sip_auth_data_item={
        "sip_authentication_scheme": "Digest-AKAv1-MD5",
        "sip_authorization": rand + auts,          # bytes
    })
guss_hex = answer.get("gba_usersecsettings")       # an OctetString reads as hex

A member name the dictionary does not know raises ValueError naming the group and the member. A keyword gives an AVP once, so a command that repeats an AVP at the top level (several GAA-Service-Identifier) needs its typed call. With peer=None the generic call takes any connected peer, not the one the application's route names: pass peer= on a node with more than one.

Zn: the NAF asks the BSF

answer = await diameter.zn_bir(
    b_tid, naf_fqdn.encode() + ua_security_protocol_id,      # NAF-Id, bytes
    gaa_service_identifiers=["1"],
    destination_realm=b_tid.split("@", 1)[1])                # the BSF of the B-TID
if answer and answer["result_code"] == 2001:
    ks_naf = answer["me_key_material"]
    expires = answer["key_expiry_time"]                      # Unix seconds
elif answer and answer["experimental_result_code"] == 5403:
    ...   # DIAMETER_ERROR_TRANSACTION_IDENTIFIER_INVALID: bootstrap again

Zn: the BSF answers

A BSF serves Zn with @diameter.on_request("Zn:BIR"), which also makes the node advertise Zn in its CEA. The handler reads the request with get_avp and builds the answer with set_avp. A Time AVP is set from Unix seconds.

TS 29.109 clause 6.2 puts its own result codes (5401, 5402, 5403) "inside an Experimental-Result AVP and Result-Code AVP shall be absent". Start from request.answer(2001), which does not set the error bit, remove the Result-Code and set the group:

TGPP = 10415

@diameter.on_request("Zn:BIR")
def bootstrapping_info(request):
    b_tid = request.get_avp("Transaction-Identifier")        # bytes
    naf_id = request.get_avp("NAF-Id")                       # bytes
    services = [value for code, vendor, value in request.iter_avps()
                if (code, vendor) == (403, TGPP)]            # GAA-Service-Identifier
    gba_u_aware = request.get_avp("GBA_U-Awareness-Indicator") == 1

    answer = request.answer(2001)
    answer.set_avp("Vendor-Specific-Application-Id",
                   [("Vendor-Id", TGPP), ("Auth-Application-Id", 16777220)])
    answer.set_avp("Auth-Session-State", 1)                  # NO_STATE_MAINTAINED

    session = sessions.get(b_tid)
    if session is None:
        answer.remove_avp("Result-Code")
        answer.set_avp("Experimental-Result",
                       [("Vendor-Id", TGPP), ("Experimental-Result-Code", 5403)])
        return answer

    answer.set_avp("ME-Key-Material", session.ks_naf(naf_id))
    answer.set_avp("Key-ExpiryTime", session.expires)        # Unix seconds
    answer.set_avp("BootstrapInfoCreationTime", session.created)
    answer.set_avp("GBA-UserSecSettings", session.uss_list(services))
    return answer

siphon carries the messages and knows the AVPs. The key derivation of TS 33.220, the store of bootstrapping transactions and the authorisation of a NAF are the script's.

Every message above is checked against Wireshark's Diameter dissector with scripts/validate_diameter_gba.sh: the application id and command code, each AVP's code, vendor and flags, and that nothing is malformed or unknown. Wireshark's dictionary names AVP 402 NAF-Hostname; TS 29.109 names it NAF-Id, and so does siphon. Not tested against an HSS.

S13: the EIR answers

On S13 (TS 29.272 clause 5.2.4) an MME, or an SGSN on S13', asks the Equipment Identity Register whether a terminal may use the network. The request is ME-Identity-Check (324) on application 16777252, "ECR" and "ECA" by name. An EIR serves it with @diameter.on_request("S13:ECR"), which also makes the node advertise S13 in its CEA; a node whose handlers name no application lists it with applications: [s13].

diameter:
  listen:
    tcp: "0.0.0.0:3868"
  origin_host: "eir.epc.mnc001.mcc001.3gppnetwork.org"
  origin_realm: "epc.mnc001.mcc001.3gppnetwork.org"
  applications: [s13]
  clients:
    - name: mme
      allowed_ips: ["192.0.2.0/24"]

Terminal-Information is a Grouped AVP: get_avp returns its members as (code, value, vendor) tuples, IMEI (1402) and Software-Version (1403) for a 3GPP terminal, or 3GPP2-MEID (1471). The answer carries Equipment-Status (1445): 0 PERMITTEDLISTED, 1 PROHIBITEDLISTED, 2 TRACKINGLISTED. A terminal the EIR does not know is answered with DIAMETER_ERROR_EQUIPMENT_UNKNOWN (5422) inside an Experimental-Result, with no Result-Code and no Equipment-Status (TS 29.272 clause 7.4.3.5):

TGPP = 10415

@diameter.on_request("S13:ECR")
def me_identity_check(request):
    terminal = {code: value for code, value, vendor
                in request.get_avp("Terminal-Information")}
    imei = terminal.get(1402)                     # 14 digits, no check digit
    status = equipment.get(imei)                  # 0, 1 or 2, or None

    answer = request.answer(2001)
    if status is None:
        answer.remove_avp("Result-Code")
        answer.set_avp("Experimental-Result",
                       [("Vendor-Id", TGPP), ("Experimental-Result-Code", 5422)])
        return answer
    answer.set_avp("Equipment-Status", status)
    return answer

siphon carries the messages and knows the AVPs. The equipment list, and what a tracked terminal means, are the script's.

The CEA of an EIR and the answers above are checked against Wireshark's Diameter dissector with scripts/validate_diameter_s13.sh. Wireshark's dictionary still names the Equipment-Status values WHITELISTED, BLACKLISTED and GREYLISTED; TS 29.272 names them as siphon does. Not tested against an MME.

Rx: QoS and bearer events

diameter.rx_aar asks the PCRF to authorize the media of a call. With specific_actions it also subscribes to IP-CAN events (TS 29.214 §5.3.13), one Specific-Action AVP per value. The PCRF reports each event in an RAR, which arrives at @diameter.on_request. The values and their names are listed under rx_aar below; 0 and 5 are void in TS 29.214 and raise ValueError, like any value it does not define.

from siphon import diameter, qos

result = await diameter.rx_aar(
    framed_ip=request.source_ip,
    media_components=qos.media_flows_from_sdp(
        offer=request.body, answer=reply.body, direction="orig",
    ),
    # loss of bearer, release of bearer, failed resources allocation
    specific_actions=[2, 4, 9],
)

qos.media_flows_from_sdp writes two Flow-Descriptions per sub-component, the uplink flow first. In TS 29.214 §5.3.8 in is the uplink flow and out the downlink flow, so the UE is the source of the in rule and the destination of the out rule (RFC 6733 §4.3.1, TS 29.212 §5.4.2):

permit in 17 from 192.0.2.2 50000 to 198.51.100.10 30000
permit out 17 from 198.51.100.10 30000 to 192.0.2.2 50000

Here 192.0.2.2 is the UE, on the originating and on the terminating leg alike; direction= only says which of the two SDP bodies is the UE's. The RTCP sub-component follows the same rule one port up. The same list goes to sbi.create_session unchanged, where TS 29.514 defines fDescs by the same clause. A script that writes its own descriptions has to follow the same convention: siphon sends them as given.

Every component it returns names its Flow-Status, derived per TS 29.213 table 6.2.1 from the offer and the answer together: the answer's port and direction attribute, except that a=inactive in the offer wins (NOTE 5), and the session-level attribute for a media line without one. A port-0 line comes back as removed with no flows, and number is the ordinal of the m= line, so the lines after it keep their numbers. TCP transport and a negotiated a=rtcp-mux are enabled whatever the direction. enabled is always named: on an update an absent Flow-Status means unchanged (TS 29.214 §5.3.16), which would leave a stream resumed from hold gated. N5 gets the same values as fStatus.

Subscribe in the first AAR for a session. Apart from one-time actions such as ACCESS_NETWORK_INFO_REPORT, a Specific-Action only counts there and then holds for the life of the Rx session.

For an emergency session pass the service URN of the request as service_urn, for example "urn:service:sos". It goes out as Service-URN, which tells the PCRF the AF session is an emergency one so it can apply its emergency policy (TS 29.214 §4.4.1). The AVP holds the URN without its urn:service: (§5.3.23), so the PCRF receives sos.

Every AAR carries Rx-Request-Type: INITIAL_REQUEST, or UPDATE_REQUEST when session_id reuses an existing session (TS 29.214 §4.4.1, §4.4.2). It is sent without the M-bit, so a PCRF that does not know the AVP skips it. When the Rx peer has a destination_host configured, the AAR carries it as Destination-Host.

diameter namespace

Mock Diameter namespace for testing scripts that use from siphon import diameter.

Exposes connection status and Cx/Rx methods matching the Rust DiameterNamespace.

Example::

import asyncio
from siphon_sdk import mock_module
mock_module.install()
diameter = mock_module.get_diameter()
diameter.add_peer("hss1", connected=True)
diameter.set_default_server_name("sip:scscf.ims.example.com:6060")

from siphon import diameter
assert diameter.is_connected("hss1")
# The request methods are awaitable, as they are in siphon, so a
# synchronous test drives one through asyncio.run.
result = asyncio.run(diameter.cx_uar("sip:alice@ims.example.com"))
assert result["server_name"] == "sip:scscf.ims.example.com:6060"

config property

config: dict

Read-only view of the parsed diameter config (tenants/listen).

Set it in tests with diameter.set_config({...}).

event_sink property

event_sink: 'MockEventSink'

The generic event sink (diameter.event_sink.emit(row)).

is_connected

is_connected(peer_name: str) -> bool

Check if a Diameter peer is connected.

Parameters:

Name Type Description Default
peer_name str

Name of the peer (e.g. "hss1").

required

Returns:

Type Description
bool

True if the peer was added and is marked as connected.

peer_count

peer_count() -> int

Get the number of connected peers.

Returns:

Type Description
int

Count of peers that are marked as connected.

decode_isdn_address

decode_isdn_address(value: Union[bytes, str]) -> str

Decode an ISDN-AddressString to its E.164 digit string.

Accepts the raw bytes (0x91 ToN/NPI + TBCD digits) or an already-decoded str, which is returned unchanged. This is the MAP type. The Diameter AVPs MSISDN, SC-Address, SGSN-Number, MME-Number-for-MT-SMS and MSC-Number are bare TBCD with no ToN/NPI octet and get_avp already returns them as digits. Bare TBCD is only recognised here when its second digit is 0 to 7.

Parameters:

Name Type Description Default
value Union[bytes, str]

bytes (raw ISDN-AddressString) or str (digits).

required

Returns:

Type Description
str

The E.164 digit string (no leading +).

Example

diameter.decode_isdn_address(req.get_avp("MSISDN")) '31612345678'

encode_isdn_address

encode_isdn_address(
    digits: str, ton_npi: int = TON_NPI_INTERNATIONAL_E164
) -> bytes

Encode an E.164 digit string as an ISDN-AddressString — one ToN/NPI octet followed by the TBCD digit string.

Use when building a raw OctetString AVP by hand for an unknown code. The Diameter AVPs MSISDN, SC-Address, SGSN-Number, MME-Number-for-MT-SMS and MSC-Number take a digit string and send it as bare TBCD, without the ToN/NPI octet this adds. A leading + is stripped.

Parameters:

Name Type Description Default
digits str

The E.164 number as a digit string.

required
ton_npi int

ToN/NPI byte (default 0x91 = international E.164).

TON_NPI_INTERNATIONAL_E164

Returns:

Type Description
bytes

The encoded ISDN-AddressString bytes.

Example

diameter.encode_isdn_address("31612345678") b'\x91\x13\x16\x32\x54\x76\xf8'

cx_uar async

cx_uar(
    public_identity: str,
    visited_network_id: Optional[str] = None,
    user_auth_type: Optional[int] = None,
    user_name: Optional[str] = None,
) -> Optional[dict]

Send a User-Authorization-Request to discover S-CSCF assignment.

Parameters:

Name Type Description Default
public_identity str

User's public identity (e.g. "sip:alice@ims.example.com").

required
visited_network_id Optional[str]

Visited network identifier.

None
user_auth_type Optional[int]

User-Authorization-Type AVP value (3GPP TS 29.229). 0 = REGISTRATION, 1 = DE_REGISTRATION, 2 = REGISTRATION_AND_CAPABILITIES. Omit to not send the AVP.

None
user_name Optional[str]

The private user identity (User-Name): the username of the REGISTER's Authorization header. Omitted, siphon derives it from public_identity. The mock accepts and ignores it.

None

Returns:

Type Description
Optional[dict]

Dict with result_code, server_name and

Optional[dict]

server_capabilities (None, or a dict of mandatory,

Optional[dict]

optional and server_names), or None.

cx_mar async

cx_mar(
    public_identity: str,
    user_name: str,
    server_name: str,
    scheme: Optional[str] = None,
    number_auth_items: int = 1,
    authorization: Optional[bytes] = None,
) -> Optional[dict]

Send a Multimedia-Auth-Request to fetch authentication vectors.

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
user_name str

The private user identity (User-Name).

required
server_name str

This S-CSCF's SIP URI (Server-Name).

required
scheme Optional[str]

SIP-Authentication-Scheme, "Digest-AKAv1-MD5" by default.

None
number_auth_items int

How many vectors to ask for.

1
authorization Optional[bytes]

RAND then AUTS, after an IMS AKA synchronisation failure only.

None

Returns:

Type Description
Optional[dict]

Dict with result_code and auth_items (one dict per

Optional[dict]

SIP-Auth-Data-Item), as configured with :meth:set_mar_response,

Optional[dict]

or None when nothing is configured for the identity.

Raises:

Type Description
ValueError

an identity or server_name is empty, or number_auth_items is 0.

zh_mar async

zh_mar(
    user_name: Optional[str] = None,
    authorization: Optional[bytes] = None,
    scheme: Optional[str] = None,
    public_identity: Optional[str] = None,
    guss_timestamp: Optional[int] = None,
) -> Optional[dict]

Send a Zh Multimedia-Auth-Request to the HSS (GBA, TS 29.109).

What a Bootstrapping Server Function asks for one authentication vector and the user's GBA security settings (clause 4.2). It is the Cx command on the Zh application (16777221), without Server-Name or SIP-Number-Auth-Items, and goes to the peer the zh route names.

Parameters:

Name Type Description Default
user_name Optional[str]

The private identity (IMPI), sent as User-Name.

None
authorization Optional[bytes]

After a synchronisation failure, RAND as sent to the terminal followed by the AUTS it answered with: 30 octets (TS 29.229 clause 6.3.11).

None
scheme Optional[str]

SIP-Authentication-Scheme. Leave it out for IMS AKA: the request then carries no SIP-Auth-Data-Item, which TS 29.109 defines as Digest-AKAv1-MD5.

None
public_identity Optional[str]

The public identity, sent as Public-Identity. Only bootstrapping push uses it.

None
guss_timestamp Optional[int]

The timestamp of the GUSS already held, in Unix seconds. The HSS then answers guss "GUSS TIMESTAMP EQUAL" when its copy is the same.

None

Returns:

Type Description
Optional[dict]

Dict with result_code (the Result-Code, or without one the

Optional[dict]

Experimental-Result-Code), experimental_result_code (int or

Optional[dict]

None; 5401 is DIAMETER_ERROR_IDENTITY_UNKNOWN), user_name,

Optional[dict]

auth_items (as cx_mar returns them; the HSS sends one) and

Optional[dict]

guss (the GBA-UserSecSettings XML or None), as configured with

Optional[dict]

meth:set_zh_mar_response. None when nothing is configured

Optional[dict]

for the identity, which is what a script sees with no peer.

Raises:

Type Description
ValueError

neither user_name nor public_identity is given, authorization is not 30 octets, or scheme is empty.

zn_bir async

zn_bir(
    transaction_identifier: Union[str, bytes],
    naf_id: bytes,
    gaa_service_identifiers: Optional[list] = None,
    gba_u_aware: Optional[bool] = None,
    destination_realm: Optional[str] = None,
    destination_host: Optional[str] = None,
) -> Optional[dict]

Send a Zn Bootstrapping-Info-Request to the BSF (GBA, TS 29.109).

What a Network Application Function asks for the key material of a bootstrapping transaction (clause 5.2). It goes to the peer the zn route names.

Parameters:

Name Type Description Default
transaction_identifier Union[str, bytes]

The B-TID the terminal presented (str or bytes).

required
naf_id bytes

The NAF-Id (bytes): the FQDN of the NAF as the terminal used it, followed by the five octets of the Ua security protocol identifier (TS 33.220 annex H).

required
gaa_service_identifiers Optional[list]

The services the settings are asked for (a list of str or bytes), one GAA-Service-Identifier each.

None
gba_u_aware Optional[bool]

GBA_U-Awareness-Indicator. Leave it out and the AVP is not sent, which means not aware.

None
destination_realm Optional[str]

The realm of the subscriber's BSF, which is the part of the B-TID after the @. Defaults to the peer's.

None
destination_host Optional[str]

Destination-Host. Defaults to the peer's.

None

Returns:

Name Type Description
Optional[dict]

Dict with result_code and experimental_result_code as

Optional[dict]

zh_mar gives them (5403 is

Optional[dict]

DIAMETER_ERROR_TRANSACTION_IDENTIFIER_INVALID, 5402

Optional[dict]

DIAMETER_ERROR_NOT_AUTHORIZED), user_name,

Optional[dict]

me_key_material and uicc_key_material (bytes),

Optional[dict]

key_expiry_time and bootstrap_info_creation_time (Unix

Optional[dict]

seconds), user_sec_settings (the selected USSs as XML) and

Optional[dict]

gba_type (without it the type is 0, 3G GBA), as configured

with Optional[dict]

meth:set_zn_bir_response. A key not configured is None.

Optional[dict]

None when nothing is configured for the transaction.

Raises:

Type Description
ValueError

transaction_identifier or naf_id is empty.

cx_sar async

cx_sar(
    public_identity: str,
    server_name: Optional[str] = None,
    assignment_type: int = 1,
    user_name: Optional[str] = None,
    *,
    user_data_already_available: bool = False,
    ims_restoration: bool = False,
    restoration_info: Optional[dict] = None,
    multiple_registration: Optional[bool] = None,
    pcscf_restoration_mechanism: bool = False,
    pcscf_restoration_indication: bool = False,
    failed_pcscf: Optional[dict] = None
) -> Optional[dict]

Send a Server-Assignment-Request after REGISTER auth.

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
server_name Optional[str]

This S-CSCF's SIP URI.

None
assignment_type int

Server-Assignment-Type (default 1 = REGISTRATION).

1
user_name Optional[str]

The private user identity, sent as User-Name: request.auth_user after a successful authentication. TS 29.228 Table 6.1.2.1 requires it whenever the S-CSCF has it; omit it and the AVP is not sent. The mock accepts and ignores it.

None
user_data_already_available bool

True sends User-Data-Already-Available (1), as a refresh of the restoration information does (TS 23.380 clause 4.6.2).

False
ims_restoration bool

True advertises IMSRestorationInd (feature bit 2 of Feature-List-ID 1, TS 29.229 clause 7.1.1).

False
restoration_info Optional[dict]

the SCSCF-Restoration-Info to back up (TS 29.229 clause 6.3.46), a dict in the shape of the answer's scscf_restoration_info entries.

None
multiple_registration Optional[bool]

Multiple-Registration-Indication (TS 29.229 clause 6.3.51); None leaves it out.

None
pcscf_restoration_mechanism bool

True advertises P-CSCF-Restoration-mechanism (feature bit 3).

False
pcscf_restoration_indication bool

True sets bit 0 of SAR-Flags (TS 29.229 clause 6.3.63).

False
failed_pcscf Optional[dict]

Failed-PCSCF (TS 29.229 clause 6.3.74), a dict with fqdn and/or ip_addresses. The mock records each request in sar_requests.

None

Returns:

Type Description
Optional[dict]

Dict with result_code, experimental_result_code,

Optional[dict]

user_data (iFC XML), scscf_restoration_info,

Optional[dict]

associated_registered_identities and supported_features,

Optional[dict]

or None. A response set with :meth:set_sar_response is

Optional[dict]

returned with the keys it lacks filled in.

cx_lir async

cx_lir(
    public_identity: str,
    user_authorization_type: Optional[int] = None,
) -> Optional[dict]

Send a Location-Info-Request to find the serving S-CSCF.

Parameters:

Name Type Description Default
public_identity str

Target user's public identity.

required
user_authorization_type Optional[int]

User-Authorization-Type, 2 (REGISTRATION_AND_CAPABILITIES) to ask for the S-CSCF capabilities when the assigned one cannot be contacted (TS 23.380 §4.3.3). The mock records it in lir_requests.

None

Returns:

Type Description
Optional[dict]

Dict with result_code, server_name and

Optional[dict]

server_capabilities (None, or a dict of mandatory,

Optional[dict]

optional and server_names), or None.

rx_aar

rx_aar(
    session_id: Optional[str] = None,
    framed_ip: Optional[str] = None,
    framed_ipv6: Union[str, bytes, None] = None,
    media_components: Optional[list] = None,
    af_application_id: str = "IMS Services",
    subscription_id: Optional[tuple] = None,
    specific_actions: Optional[list[int]] = None,
    service_urn: Optional[str] = None,
) -> Awaitable[Optional[dict]]

Send an Rx AA-Request for QoS resource reservation.

Without session_id the AAR is sent as Rx-Request-Type INITIAL_REQUEST; with one it is UPDATE_REQUEST (TS 29.214 §4.4.1, §4.4.2). The Rx peer's configured destination_host, if set, goes out as Destination-Host.

Parameters:

Name Type Description Default
session_id Optional[str]

Reuse an existing Rx session ID (modification AAR per TS 29.214 §4.4.5). None allocates a new session.

None
framed_ip Optional[str]

UE IPv4 address (Framed-IP-Address AVP).

None
framed_ipv6 Union[str, bytes, None]

UE IPv6 address (str or bytes).

None
media_components Optional[list]

list of media-component dicts shaped per TS 29.214 §5.3.7 (see project docs for the full schema).

None
af_application_id str

AF-Application-Identifier (default "IMS Services").

'IMS Services'
subscription_id Optional[tuple]

Optional (data, type) tuple identifying the IMS subscriber (RFC 4006 §8.47).

None
specific_actions Optional[list[int]]

Events the PCRF should report back, as ints. Each entry becomes one Specific-Action AVP (TS 29.214 §5.3.13). None (the default) or [] subscribes to nothing and sends no Specific-Action AVP. Defined values:

  • 1 CHARGING_CORRELATION_EXCHANGE
  • 2 INDICATION_OF_LOSS_OF_BEARER
  • 3 INDICATION_OF_RECOVERY_OF_BEARER
  • 4 INDICATION_OF_RELEASE_OF_BEARER
  • 6 IP-CAN_CHANGE
  • 7 INDICATION_OF_OUT_OF_CREDIT
  • 8 INDICATION_OF_SUCCESSFUL_RESOURCES_ALLOCATION
  • 9 INDICATION_OF_FAILED_RESOURCES_ALLOCATION
  • 10 INDICATION_OF_LIMITED_PCC_DEPLOYMENT
  • 11 USAGE_REPORT
  • 12 ACCESS_NETWORK_INFO_REPORT
  • 13 INDICATION_OF_RECOVERY_FROM_LIMITED_PCC_DEPLOYMENT
  • 14 INDICATION_OF_ACCESS_NETWORK_INFO_REPORTING_FAILURE
  • 15 INDICATION_OF_TRANSFER_POLICY_EXPIRED
  • 16 PLMN_CHANGE
  • 17 EPS_FALLBACK
  • 18 INDICATION_OF_REALLOCATION_OF_CREDIT
  • 19 SUCCESSFUL_QOS_UPDATE
  • 20 FAILED_QOS_UPDATE
  • 21 CN_HEALTH_MONITOR

0 and 5 are void in TS 29.214. Most of these only take effect in the initial AAR and then hold for the whole Rx session (ACCESS_NETWORK_INFO_REPORT is one-time and may be repeated), so subscribe on the first call. The PCRF reports each event in an RAR carrying the matching Specific-Action, which reaches @diameter.on_request (request.command_name == "RAR").

None
service_urn Optional[str]

The service URN of an emergency session, e.g. "urn:service:sos" or "urn:service:sos.police", sent as Service-URN so the PCRF applies its emergency policy (TS 29.214 §4.4.1). The AVP holds the URN without its urn:service: (§5.3.23), so "sos" is accepted too.

None

Returns:

Type Description
Awaitable[Optional[dict]]

Dict with result_code and session_id, or None.

Raises:

Type Description
ValueError

A specific_actions entry is not a defined value; the message names it. Or service_urn has nothing after urn:service:.

TypeError

specific_actions is not a list of int.

Example::

result = await diameter.rx_aar(
    framed_ip=request.source_ip,
    media_components=components,
    # loss of bearer, release of bearer, failed allocation
    specific_actions=[2, 4, 9],
)

rx_str async

rx_str(session_id: str) -> Optional[int]

Send an Rx Session-Termination-Request.

Parameters:

Name Type Description Default
session_id str

The Rx session ID from the original AAR.

required

Returns:

Type Description
Optional[int]

Result code (int), or None.

sh_udr async

sh_udr(
    public_identity: str,
    data_reference: Union[int, list[int]],
    service_indication: Optional[str] = None,
) -> Optional[dict]

Send a Sh User-Data-Request to fetch user profile data from the HSS.

Parameters:

Name Type Description Default
public_identity str

Target user's public identity.

required
data_reference Union[int, list[int]]

Data-Reference int or list[int] (TS 29.328 §7.6).

required
service_indication Optional[str]

e.g. "simservs" for Repository-Data.

None

Returns:

Type Description
Optional[dict]

Dict with result_code and user_data (XML), or None.

sh_pur async

sh_pur(
    public_identity: str,
    data_reference: int,
    xml: str,
    service_indication: Optional[str] = None,
) -> Optional[dict]

Send a Sh Profile-Update-Request to push user profile data to the HSS.

Parameters:

Name Type Description Default
public_identity str

Target user's public identity.

required
data_reference int

Data-Reference (e.g. 0 = Repository-Data).

required
xml str

UTF-8 XML payload.

required
service_indication Optional[str]

e.g. "simservs"; required by the HSS when Data-Reference is Repository-Data (TS 29.328 §6.1.3).

None

Returns:

Type Description
Optional[dict]

Dict with result_code, or None.

sh_snr async

sh_snr(
    public_identity: str,
    data_reference: Union[int, list[int]],
    subs_req_type: int,
    service_indication: Optional[str] = None,
) -> Optional[dict]

Send a Sh Subscribe-Notifications-Request to the HSS.

Parameters:

Name Type Description Default
public_identity str

Target user's public identity.

required
data_reference Union[int, list[int]]

Data-Reference int or list[int] to subscribe to.

required
subs_req_type int

0 = SUBSCRIBE, 1 = UNSUBSCRIBE.

required
service_indication Optional[str]

e.g. "simservs"; required by the HSS when Data-Reference is Repository-Data (TS 29.328 §6.1.4).

None

Returns:

Type Description
Optional[dict]

Dict with result_code, or None.

add_peer

add_peer(name: str, connected: bool = True) -> None

Register a mock Diameter peer (test helper).

Parameters:

Name Type Description Default
name str

Peer name.

required
connected bool

Whether the peer should appear as connected.

True

set_default_server_name

set_default_server_name(server_name: str) -> None

Set a default S-CSCF name returned by UAR/LIR when no per-user response is configured.

Parameters:

Name Type Description Default
server_name str

S-CSCF SIP URI (e.g. "sip:scscf.ims.example.com:6060").

required

set_mar_response

set_mar_response(
    public_identity: str,
    result_code: int = 2001,
    auth_items: Optional[list] = None,
) -> None

Configure a mock MAA response for a specific user (test helper).

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
result_code int

Diameter result code (default 2001 = SUCCESS).

2001
auth_items Optional[list]

One dict per SIP-Auth-Data-Item, with the keys diameter.cx_mar returns (scheme, authenticate, ...).

None

set_zh_mar_response

set_zh_mar_response(
    identity: str,
    result_code: int = 2001,
    auth_items: Optional[list] = None,
    guss: Optional[str] = None,
    experimental_result_code: Optional[int] = None,
) -> None

Configure what zh_mar answers for an identity (test helper).

Parameters:

Name Type Description Default
identity str

The user_name (or, without one, the public_identity) of the request.

required
result_code int

Result-Code (default 2001 = SUCCESS).

2001
auth_items Optional[list]

One dict per SIP-Auth-Data-Item, with the keys diameter.cx_mar returns (scheme, authenticate, ...).

None
guss Optional[str]

The GBA-UserSecSettings XML.

None
experimental_result_code Optional[int]

An Experimental-Result-Code, e.g. 5401. The answer then has no Result-Code, so result_code reads this value, as it does at runtime.

None

set_zn_bir_response

set_zn_bir_response(
    transaction_identifier: str,
    result_code: int = 2001,
    user_name: Optional[str] = None,
    me_key_material: Optional[bytes] = None,
    uicc_key_material: Optional[bytes] = None,
    key_expiry_time: Optional[int] = None,
    bootstrap_info_creation_time: Optional[int] = None,
    user_sec_settings: Optional[str] = None,
    gba_type: Optional[int] = None,
    experimental_result_code: Optional[int] = None,
) -> None

Configure what zn_bir answers for a B-TID (test helper).

The keyword arguments are the keys of the dict zn_bir returns. With experimental_result_code (e.g. 5403) the answer has no Result-Code, so result_code reads that value.

set_uar_response

set_uar_response(
    public_identity: str,
    result_code: int = 2001,
    server_name: Optional[str] = None,
    server_capabilities: Optional[dict] = None,
) -> None

Configure a mock UAA response for a specific user (test helper).

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
result_code int

Diameter result code (default 2001 = SUCCESS).

2001
server_name Optional[str]

Assigned S-CSCF URI.

None
server_capabilities Optional[dict]

Server-Capabilities as cx_uar returns it: mandatory, optional, server_names.

None

set_sar_response

set_sar_response(
    public_identity: str,
    result_code: int = 2001,
    user_data: Optional[str] = None,
    *,
    experimental: bool = False,
    scscf_restoration_info: Optional[list[dict]] = None,
    associated_registered_identities: Optional[
        list[str]
    ] = None,
    supported_features: int = 0
) -> None

Configure a mock SAA response for a specific user (test helper).

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
result_code int

Diameter result code.

2001
user_data Optional[str]

iFC XML string from user profile.

None
experimental bool

result_code came as an Experimental-Result-Code (e.g. 5007 DIAMETER_ERROR_IN_ASSIGNMENT_TYPE with the restoration information, TS 29.228 §6.1.2.1).

False
scscf_restoration_info Optional[list[dict]]

The SCSCF-Restoration-Info groups, in the shape cx_sar documents.

None
associated_registered_identities Optional[list[str]]

Other registered private identities.

None
supported_features int

Feature-List of Feature-List-ID 1.

0

set_lir_response

set_lir_response(
    public_identity: str,
    result_code: int = 2001,
    server_name: Optional[str] = None,
    server_capabilities: Optional[dict] = None,
) -> None

Configure a mock LIA response for a specific user (test helper).

Parameters:

Name Type Description Default
public_identity str

User's public identity.

required
result_code int

Diameter result code.

2001
server_name Optional[str]

Serving S-CSCF URI.

None
server_capabilities Optional[dict]

Server-Capabilities as cx_lir returns it: mandatory, optional, server_names.

None

set_aar_response

set_aar_response(
    session_id: str, result_code: int = 2001
) -> None

Configure a mock AAA response for a specific Rx session (test helper).

Parameters:

Name Type Description Default
session_id str

Rx session ID.

required
result_code int

Diameter result code.

2001

rf_acr_start async

rf_acr_start(
    *,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    sip_method: Optional[str] = None,
    role_of_node: Optional[str] = None,
    node_functionality: Optional[str] = None,
    ims_charging_identifier: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originating_ioi: Optional[str] = None,
    terminating_ioi: Optional[str] = None,
    application_server: Optional[str] = None,
    application_provided_called_party_address: Optional[
        str
    ] = None,
    incoming_trunk_group_id: Optional[str] = None,
    outgoing_trunk_group_id: Optional[str] = None,
    visited_network_id: Optional[str] = None,
    access_network_information: Optional[str] = None,
    user_name: Optional[str] = None,
    subscription_id: Optional[Union[str, list[str]]] = None,
    subscription_id_type: Optional[
        Union[str, list[str]]
    ] = None,
    cause_code: Optional[int] = None,
    service_context_id: Optional[str] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send Rf ACR-START to the CDF (TS 32.299 §6.2.2).

rf_acr_interim async

rf_acr_interim(
    session_id: str,
    record_number: int,
    *,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    sip_method: Optional[str] = None,
    role_of_node: Optional[str] = None,
    node_functionality: Optional[str] = None,
    ims_charging_identifier: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originating_ioi: Optional[str] = None,
    terminating_ioi: Optional[str] = None,
    application_server: Optional[str] = None,
    application_provided_called_party_address: Optional[
        str
    ] = None,
    incoming_trunk_group_id: Optional[str] = None,
    outgoing_trunk_group_id: Optional[str] = None,
    visited_network_id: Optional[str] = None,
    access_network_information: Optional[str] = None,
    user_name: Optional[str] = None,
    subscription_id: Optional[Union[str, list[str]]] = None,
    subscription_id_type: Optional[
        Union[str, list[str]]
    ] = None,
    cause_code: Optional[int] = None,
    service_context_id: Optional[str] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send Rf ACR-INTERIM (mid-session accounting update).

rf_acr_stop async

rf_acr_stop(
    session_id: str,
    record_number: int,
    *,
    termination_cause: int = 1,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    sip_method: Optional[str] = None,
    role_of_node: Optional[str] = None,
    node_functionality: Optional[str] = None,
    ims_charging_identifier: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originating_ioi: Optional[str] = None,
    terminating_ioi: Optional[str] = None,
    application_server: Optional[str] = None,
    application_provided_called_party_address: Optional[
        str
    ] = None,
    incoming_trunk_group_id: Optional[str] = None,
    outgoing_trunk_group_id: Optional[str] = None,
    visited_network_id: Optional[str] = None,
    access_network_information: Optional[str] = None,
    user_name: Optional[str] = None,
    subscription_id: Optional[Union[str, list[str]]] = None,
    subscription_id_type: Optional[
        Union[str, list[str]]
    ] = None,
    cause_code: Optional[int] = None,
    service_context_id: Optional[str] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send Rf ACR-STOP. termination_cause per RFC 6733 §8.15 (1=LOGOUT, 4=ADMINISTRATIVE, 5=LINK_BROKEN, 8=SESSION_TIMEOUT).

rf_acr_event async

rf_acr_event(
    *,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    sip_method: Optional[str] = None,
    role_of_node: Optional[str] = None,
    node_functionality: Optional[str] = None,
    ims_charging_identifier: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originating_ioi: Optional[str] = None,
    terminating_ioi: Optional[str] = None,
    application_server: Optional[str] = None,
    application_provided_called_party_address: Optional[
        str
    ] = None,
    incoming_trunk_group_id: Optional[str] = None,
    outgoing_trunk_group_id: Optional[str] = None,
    visited_network_id: Optional[str] = None,
    access_network_information: Optional[str] = None,
    user_name: Optional[str] = None,
    subscription_id: Optional[Union[str, list[str]]] = None,
    subscription_id_type: Optional[
        Union[str, list[str]]
    ] = None,
    cause_code: Optional[int] = None,
    service_context_id: Optional[str] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send Rf ACR-EVENT (one-shot accounting — REGISTER/MESSAGE).

access_network_information (also on rf_acr_start / rf_acr_interim / rf_acr_stop) is the value of the request's P-Access-Network-Info header. It is sent inside IMS-Information as Access-Network-Information (TS 32.299, AVP 1263), one AVP per comma-separated access-net-spec::

await diameter.rf_acr_event(
    sip_method="REGISTER",
    calling_party=str(request.from_uri),
    access_network_information=request.get_header("P-Access-Network-Info"),
)

The automatic session ACRs (rf: config) fill it from the header themselves when rf.access_network_information: true. It is off by default: the AVP carries the M bit, so a CDF that does not know it rejects the record.

set_rf_result_code

set_rf_result_code(code: int) -> None

Override the Result-Code returned by every Rf ACA (default 2001).

set_rf_interim_interval

set_rf_interim_interval(
    interval_secs: Optional[int],
) -> None

Configure the Acct-Interim-Interval returned in ACA-START.

captured_acrs

captured_acrs() -> list[dict]

Return all ACRs the script has emitted via rf_acr_*.

Returns a fresh copy on each call. Useful for asserting on accounting flows in tests.

clear_captured_acrs

clear_captured_acrs() -> None

Reset the captured-ACR list between tests.

ro_ccr_initial async

ro_ccr_initial(
    subscription_id: str,
    *,
    subscription_id_type: Optional[str] = None,
    service_context_id: Optional[str] = None,
    requested_seconds: Optional[int] = None,
    rating_group: Optional[int] = None,
    service_identifier: Optional[int] = None,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    sip_method: Optional[str] = None,
    role_of_node: Optional[str] = None,
    node_functionality: Optional[str] = None,
    ims_charging_identifier: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originating_ioi: Optional[str] = None,
    terminating_ioi: Optional[str] = None,
    application_server: Optional[str] = None,
    application_provided_called_party_address: Optional[
        str
    ] = None,
    incoming_trunk_group_id: Optional[str] = None,
    outgoing_trunk_group_id: Optional[str] = None,
    visited_network_id: Optional[str] = None,
    cause_code: Optional[int] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send a Ro CCR-INITIAL and return the CCA dict.

Returns {result_code, session_id, request_number, granted_time, validity_time, final_unit_action}. For SCUR, thread the returned session_id through :meth:ro_ccr_update / :meth:ro_ccr_terminate.

Example

answer = await diameter.ro_ccr_initial( "+310000000001", requested_seconds=30, rating_group=100, calling_party="sip:alice@ims", called_party="sip:bob@ims") if answer["result_code"] != 2001: call.reject(402, "Payment Required")

ro_ccr_update async

ro_ccr_update(
    subscription_id: str,
    session_id: str,
    request_number: int,
    *,
    subscription_id_type: Optional[str] = None,
    service_context_id: Optional[str] = None,
    used_seconds: Optional[int] = None,
    requested_seconds: Optional[int] = None,
    rating_group: Optional[int] = None,
    service_identifier: Optional[int] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send a Ro CCR-UPDATE reporting usage and requesting the next quota.

ro_ccr_terminate async

ro_ccr_terminate(
    subscription_id: str,
    session_id: str,
    request_number: int,
    *,
    subscription_id_type: Optional[str] = None,
    service_context_id: Optional[str] = None,
    used_seconds: Optional[int] = None,
    rating_group: Optional[int] = None,
    service_identifier: Optional[int] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send a Ro CCR-TERMINATION closing the session with final usage.

ro_ccr_event async

ro_ccr_event(
    subscription_id: str,
    *,
    subscription_id_type: Optional[str] = None,
    service_context_id: Optional[str] = None,
    requested_action: Optional[int] = None,
    calling_party: Optional[str] = None,
    called_party: Optional[str] = None,
    node_functionality: Optional[str] = None,
    user_session_id: Optional[str] = None,
    originator_address: Optional[str] = None,
    recipient_address: Optional[str] = None,
    sm_message_type: Optional[int] = None,
    sm_service_type: Optional[int] = None,
    sms_node: Optional[int] = None,
    data_coding_scheme: Optional[int] = None,
    peer: Optional[str] = None
) -> Optional[dict]

Send a one-shot Ro CCR-EVENT (IEC — SMS/RCS DIRECT_DEBITING).

Example

answer = await diameter.ro_ccr_event( "+310000000001", service_context_id="32274@3gpp.org", originator_address="+310000000001", recipient_address="+310000000002", sm_message_type=0) if answer["result_code"] != 2001: request.reply(402, "Payment Required") # no balance

set_ro_result_code

set_ro_result_code(code: int) -> None

Override the Result-Code returned by every Ro CCA (default 2001).

set_ro_granted_time

set_ro_granted_time(seconds: Optional[int]) -> None

Configure the granted CC-Time (seconds) returned in a successful CCA.

set_ro_final_unit_action

set_ro_final_unit_action(action: Optional[int]) -> None

Configure the Final-Unit-Action (0=TERMINATE) returned in the CCA.

ro_session async

ro_session(
    subscription_id: str,
    *,
    subscription_id_type: Optional[str] = None,
    additional_subscription_ids: Optional[list] = None,
    key: Optional[str] = None,
    call_id: Optional[str] = None,
    metering: str = "local",
    charge_from: Optional[str] = None,
    on_event: Optional[Callable] = None,
    vcs_information: Optional[dict] = None,
    **ims: Any
) -> "MockRoSession"

Open a Ro session held by siphon and return its RoSession.

Any number may be open at once; key files it for :meth:ro_find. The CCRs land in :meth:captured_ccrs with source="ro_session". Drive the OCS side with :meth:trigger_ro_event.

Example

camel = await diameter.ro_session( "15550100004", subscription_id_type="e164", additional_subscription_ids=[("001010000000004", "imsi")], key="dialogue-1", metering="external", service_context_id="32276@3gpp.org", role_of_node="forwarding", node_functionality="proxy", vcs_information={"msc_address": "51551000f9"}) grant = await camel.report(used_total=30) await camel.terminate(used_total=42, vcs_information={"isup_cause_value": 16})

ro_find

ro_find(key: str) -> Optional['MockRoSession']

The open RoSession filed under key, or None.

trigger_ro_event

trigger_ro_event(
    session: "MockRoSession",
    event: str,
    result_code: Optional[int] = None,
) -> Any

Act as the OCS: "aborted" (ASR, ends the session), "reauthorize" (RAR or Validity-Time on an external session) or "released". Calls the session's on_event(session, event) and returns what it returned (a coroutine for an async def).

captured_ccrs

captured_ccrs() -> list[dict]

Return all CCRs the script has emitted via ro_ccr_* (fresh copy).

clear_captured_ccrs

clear_captured_ccrs() -> None

Reset the captured-CCR list between tests.

on_inbound_cer staticmethod

on_inbound_cer(fn: Any) -> Any

Register the server-mode CER identity callback.

Called for an already-authenticated peer (both Rust auth gates have passed) with (peer_addr, peer_name, asserted_origin_host). Return (origin_host, origin_realm) to accept, or None to reject.

Example::

@diameter.on_inbound_cer
def cer_received(peer_addr, peer_name, asserted_origin_host):
    identity = diameter.config["tenants"]["default"]["identity"]
    return identity["origin_host"], identity["origin_realm"]

on_request staticmethod

on_request(arg: Any = None) -> Any

Register the server-mode inbound-request dispatcher.

Called for inbound requests (R-bit set). Return req.reject(code), await req.forward_to(peer, ...), req.answer(code), or None (→ DIAMETER_UNABLE_TO_DELIVER, 3002).

An optional command filter scopes the handler — bare @diameter.on_request (all), @diameter.on_request("ULR"), "ULR|AIR", or app-qualified "S6a:ULR". The mock treats it as an identity decorator either way.

The filter has the same shape as @proxy.on_request("INVITE") but not the same dispatch rule: exactly one Diameter handler runs per request — the most specific filter that matches — and a bare @diameter.on_request is the lowest-specificity fallback, reached only when nothing more specific matched. @proxy.on_request instead runs every handler whose filter matches, unfiltered ones included.

Deliberate, not an inconsistency: a Diameter request needs exactly one answer, so one handler must own it. A SIP request can legitimately interest several handlers at once (metrics, lawful intercept, authentication, routing), so they compose — and request.stop_propagation() is how one of them claims the outcome.

Example::

@diameter.on_request("S6a:ULR")
async def update_location(req):
    return req.answer(2001)

on_reply staticmethod

on_reply(fn: Any) -> Any

Register the server-mode answer-rewrite hook.

Called with (req, answer) on the answer an on_request handler produced — relayed via forward_to or built by answer/reject — just before it goes back upstream. A central place to rewrite answer AVPs for every reply (topology hiding, Origin-Host/Result-Code mapping). Mutate answer in place; the return value is ignored.

on_request_completed staticmethod

on_request_completed(fn: Any) -> Any

Register the server-mode post-answer hook.

Called after the answer is sent upstream with (req, answer, latency_us) — typically to emit an event.

peer_pool

peer_pool(
    target: Any, tenant: str = "default"
) -> "MockPeerPool"

Build a mock backend peer pool. target is a peer name or list of names; tenant is an optional scope label (defaults to "default" — single-domain servers leave it unset). Register backends with :meth:add_peer(connected=True).

ip_in_cidr staticmethod

ip_in_cidr(addr: str, cidr: str) -> bool

Whether addr falls within cidr (mirrors the Rust helper).

fnmatch staticmethod

fnmatch(value: str, pattern: str) -> bool

Shell-style glob match (*/?).

now_us staticmethod

now_us() -> int

Wall-clock microseconds since the Unix epoch.

set_config

set_config(config: dict) -> None

Test helper: set the dict returned by :attr:config.

s6a_air async

s6a_air(
    imsi: str,
    visited_plmn_id: bytes,
    num_vectors: int = 1,
    immediate_response_preferred: bool = True,
    resync_info: Optional[bytes] = None,
    peer: Optional[str] = None,
) -> Optional[dict]

Mock Authentication-Information. Returns canned E-UTRAN vectors; configure with :meth:set_air_response.

s6a_ulr async

s6a_ulr(
    imsi: str,
    visited_plmn_id: bytes,
    rat_type: int = 1004,
    ulr_flags: int = 0,
    peer: Optional[str] = None,
) -> Optional[dict]

Mock Update-Location. Returns a 2001 with subscription data present.

s6a_purge_ue async

s6a_purge_ue(
    imsi: str,
    pur_flags: Optional[int] = None,
    peer: Optional[str] = None,
) -> Optional[dict]

Mock Purge-UE. Returns a 2001.

s6c_srr async

s6c_srr(
    msisdn: str,
    sc_address: str,
    sm_rp_mti: Optional[int] = None,
) -> Optional[dict]

Mock Send-Routing-Info-for-SM. Configure responses via :meth:set_srr_response; default is a successful answer with an empty served-node (test scripts can detect the unset case).

set_srr_response

set_srr_response(
    msisdn: str,
    *,
    result_code: int = 2001,
    user_name: Optional[str] = None,
    sgsn_number: Optional[str] = None,
    mme_number_for_mt_sms: Optional[str] = None,
    mme_name: Optional[str] = None,
    mme_realm: Optional[str] = None,
    sgsn_name: Optional[str] = None,
    sgsn_realm: Optional[str] = None,
    msc_number: Optional[str] = None,
    experimental_result_code: Optional[int] = None
) -> None

mme_name / mme_realm are the located node's Diameter identity from the grouped Serving-Node — for a UE doing SMS over NAS on 5G that is the SMSF (TS 29.338 §6.3.2.4), which is what an MT-Forward-Short-Message is addressed to.

s6c_rsr async

s6c_rsr(
    user_name: str,
    sc_address: str,
    delivery_outcome: int,
    node: str = "mme",
    absent_user_diagnostic: Optional[int] = None,
) -> Optional[dict]

Mock Report-SM-Delivery-Status. Records the call on self.rsrs for assertions and returns a 2001.

Parameters:

Name Type Description Default
user_name str

IMSI of the served subscriber.

required
sc_address str

Address of the service centre.

required
delivery_outcome int

0 successful transfer, 1 absent user, 2 UE memory capacity exceeded. siphon sends it as the SM-Delivery-Cause of TS 29.338.

required
node str

The node the delivery was attempted through: "mme" (default), "sgsn", "msc" or "ip_sm_gw", compared without case. The HSS keeps its message waiting flags per node, so name the one the message went to: "sgsn" when s6c_srr located no MME and the forward went to sgsn_name. Selects the group inside SM-Delivery-Outcome.

'mme'
absent_user_diagnostic Optional[int]

Optional Absent-User-Diagnostic-SM, why the node found the user absent (TS 23.040 clause 3.3.2, e.g. 1 IMSI detached, 6 GPRS detached). Only with delivery_outcome=1.

None

Raises:

Type Description
ValueError

delivery_outcome is not 0, 1 or 2, node is not one of the four, or absent_user_diagnostic is given for a delivery that did not end in an absent user.

Example::

located = await diameter.s6c_srr(msisdn, sc_address)
node = "mme" if located["mme_name"] else "sgsn"
...
await diameter.s6c_rsr(located["user_name"], sc_address, 1,
                       node=node, absent_user_diagnostic=6)

sgd_tfr async

sgd_tfr(
    user_name: str,
    sc_address: str,
    sm_rp_ui: bytes,
    smsmi_correlation_id: Optional[str] = None,
    sm_rp_mti: Optional[int] = None,
    destination_host: Optional[str] = None,
    destination_realm: Optional[str] = None,
) -> Optional[dict]

Mock MT-Forward-Short-Message. Records the TPDU on self.tfrs for assertions; returns 2001 unless overridden via :meth:set_tfr_response.

destination_host is the node the preceding SRI-SM located (mme_name). Omitting it falls back to static peer config, which addresses the request at whatever the relay's catch-all route resolves to rather than at the serving node.

reauthorize async

reauthorize(
    peer: str,
    session_id: str,
    destination_host: str,
    destination_realm: str,
    application: str,
    tenant: str = "default",
    timeout_ms: int = 5000,
    **avps: Any
) -> int

Send a Re-Auth-Request for application on a serving peer.

The mock records it on generic_requests as a RAR for application (configure the RAA through set_generic_response("RAR", application, ...)), with auth_application_id resolved from the application name and any extra AVP kwargs alongside.

originate async

originate(
    command,
    application: str,
    avps: list,
    destination_realm: str,
    destination_host: Optional[str] = None,
    peer: Optional[str] = None,
    session_id: Optional[str] = None,
    tenant: str = "default",
    proxiable: bool = True,
    timeout_ms: int = 10000,
) -> "MockDiameterAnswer"

Originate a request on a connection this node has.

The mock records each call on originated (command, application, the avps tuples as given, the destination) and resolves to a 2001 MockDiameterAnswer, or to what set_originate_outcome configured for (command, application): an answer, or an exception (UnableToDeliver for a destination_host with no connection and no agent relaying for its realm, LookupError for no connection otherwise, TimeoutError, ConnectionError) to raise.

In siphon the connection is the one that carried requests from destination_host, else one of an agent that relayed requests from destination_realm (their Route-Record shows it). Another direct peer of the realm is never used for a named host. Without destination_host, any connection that carried requests from the realm.

set_originate_outcome

set_originate_outcome(
    command, application: str, outcome
) -> None

What originate(command, application, ...) resolves to: a MockDiameterAnswer, or an exception it raises.

send_request async

send_request(
    command: str,
    application: str,
    peer: Optional[str] = None,
    timeout_ms: int = 10000,
    **avps: Any
) -> Optional[dict]

Generic Diameter request by spec name.

Each keyword is an AVP by its snake_case name, with a value of the AVP's type: str, int, bytes, Unix seconds for a Time AVP, and for a Grouped AVP a dict of its members by the same rules (sip_auth_data_item={"sip_authorization": rand + auts}). The mock records the values as given and does not check them against the dictionary.

Records every call on self.generic_requests for assertions. Returns a default 2001-success answer unless overridden via :meth:set_generic_response.

set_generic_response

set_generic_response(
    command: str, application: str, **answer: Any
) -> None

Configure a mock answer for send_request(command, application, ...).

set_udr_response

set_udr_response(
    public_identity: str,
    result_code: int = 2001,
    user_data: Optional[str] = None,
) -> None

Configure a mock UDA response for a specific user (test helper).

set_pur_response

set_pur_response(
    public_identity: str, result_code: int = 2001
) -> None

Configure a mock PUA response for a specific user (test helper).

set_snr_response

set_snr_response(
    public_identity: str, result_code: int = 2001
) -> None

Configure a mock SNA response for a specific user (test helper).

clear

clear() -> None

Reset all mock peers and responses (test helper).

DiameterRequest

The inbound request passed to @diameter.on_request.

Mock DiameterRequest passed to @diameter.on_request in tests.

Construct one in your test and invoke your handler with it.

The mock has no dictionary, so the AVPs the real dictionary would not know are given apart, as unknown_avps=[(code, vendor, value, mandatory), ...]: :meth:unknown_avps lists them and :meth:reject_unsupported_avps refuses the ones with mandatory.

iter_avps

iter_avps() -> list

The top-level AVPs as (code, vendor, value) tuples, in wire order.

The members of a grouped AVP are in another order: its value is a list of (code, value, vendor) tuples, the shape set_avp takes for a child. Unpacking one in the order of the other compares a value with a vendor id and matches nothing. :meth:avps lists the same AVPs with the fields by name::

for avp in request.avps():
    if avp.name == "Deregistration-Reason":
        reason = {member.name: member.value for member in avp.value}

avps

avps() -> list

The top-level AVPs as :class:MockDiameterAvp objects, in wire order: .code, .vendor, .name and .value, which for a grouped AVP is a list of the same objects.

unknown_avps

unknown_avps() -> list

The AVPs the dictionary does not define, as :class:MockDiameterAvp objects with name None and mandatory as given.

reject_unsupported_avps

reject_unsupported_avps(
    understood: Optional[list] = None,
) -> Optional[MockDiameterAnswer]

The DIAMETER_AVP_UNSUPPORTED (5001) answer, with a Failed-AVP (279) holding every unknown AVP with the M bit that understood does not list, or None when there is none (RFC 6733 sections 4.1 and 7.1.5). understood lists codes (vendor 0) or (code, vendor) tuples.

answer

answer(
    result_code: int = 2001,
    error_message: Optional[str] = None,
) -> MockDiameterAnswer

Build a local answer to serve this request (HSS-style). Populate it with :meth:MockDiameterAnswer.set_avp, including grouped AVPs (pass a list of (code, value[, vendor]) child tuples as the value), and with :meth:MockDiameterAnswer.insert_avp for an AVP the answer carries more than once.

The answer starts with the request's Vendor-Specific-Application-Id (260) and Auth-Session-State (277) when the request has them, which the answers of the 3GPP applications list, unless result_code is a protocol error (3xxx). Setting either yourself replaces the copy.

reject

reject(
    result_code: int, error_message: Optional[str] = None
) -> MockDiameterAnswer

Build an answer that refuses this request: an alias of :meth:answer, kept for readability. The header E bit is set for a protocol error (3xxx) and for nothing else.

forward_to async

forward_to(
    peer: MockPeer,
    identity=None,
    timeout_secs: float = 10.0,
) -> MockDiameterAnswer

Mock relay — returns a 2001 success answer (override in tests by monkeypatching if a different result is needed).

Reading AVPs: two tuple orders, and avps()

get_avp and iter_avps hand AVPs out as tuples, and the two shapes differ:

Where Shape
iter_avps(), each top-level AVP (code, vendor, value)
the members of a grouped AVP, in the value of either call (code, value, vendor)

The second is the shape set_avp takes for a child, so a group read out can be set back in. A handler that unpacks a member as code, vendor, value compares a value with a vendor id, finds nothing and, typically, refuses every request. Neither order can change without breaking scripts. request.avps() and answer.avps() list the same AVPs as DiameterAvp objects instead, with the fields by name at every level:

for avp in request.avps():
    if avp.name == "Deregistration-Reason" and avp.vendor == 10415:
        reason = {member.name: member.value for member in avp.value}

Mock DiameterAvp: one AVP with its fields by name, as request.avps() / answer.avps() list them.

iter_avps yields (code, vendor, value) tuples and the members of a grouped AVP are (code, value, vendor) tuples, so a handler that unpacks one in the order of the other compares a value with a vendor id. Here the fields are attributes, the same at every level.

Attributes:

Name Type Description
code

The AVP code.

vendor

The Vendor-Id, 0 for a base AVP.

name

The dictionary name, or None for an AVP the dictionary does not know. The mock has no dictionary: an AVP stored under a name has that name and the name as its code, one stored under a number has no name.

value

The value as get_avp returns it, or for a grouped AVP a list of DiameterAvp.

flags

The flags octet of the AVP header: V 0x80, M 0x40, P 0x20. The mock gives every AVP the M bit and, for a non-zero vendor, the V bit, unless the test says otherwise.

mandatory bool

Whether the M bit is set.

vendor_specific bool

Whether the V bit is set.

AVP flags and AVPs the dictionary does not define

Each DiameterAvp carries the flags octet it was received with (flags), and mandatory and vendor_specific read the M and V bits out of it. request.unknown_avps() lists the AVPs of the request the dictionary does not define, at the top level and inside the grouped AVPs it does define.

RFC 6733 section 4.1 has a receiver that does not understand an AVP with the M bit refuse the message, with DIAMETER_AVP_UNSUPPORTED (5001) and the AVP in a Failed-AVP (section 7.1.5), and has relay and redirect agents let unknown AVPs through. siphon does not answer 5001 on its own: the same @diameter.on_request path carries relays, and a handler can read an AVP the dictionary does not define by its code. An end server asks for the answer instead, naming what it reads beyond the dictionary:

@diameter.on_request("Gx:CCR")
def credit_control(request):
    refusal = request.reject_unsupported_avps(understood=[(4242, 10415)])
    if refusal is not None:
        return refusal
    ...

reject_unsupported_avps returns None when there is nothing to refuse. The Failed-AVP holds each offending AVP as received; one inside a grouped AVP comes inside that group, holding it alone, so that the peer can tell where it was (section 7.5). An unknown AVP without the M bit is not refused.

Policy AVPs: Gx and Rx

The dictionary holds every AVP of TS 29.212 and TS 29.214 table 5.3.0.1 (Rel-18), and the 3GPP-* attributes of TS 29.061 their re-used tables take over (3GPP-SGSN-MCC-MNC, 3GPP-User-Location-Info, 3GPP-MS-TimeZone and the rest), each sent with the M bit its table gives it. So a PCRF sets Revalidation-Time, 3GPP-PS-Data-Off-Status, Usage-Monitoring-Information, RR-Bandwidth, RS-Bandwidth, Min-Requested-Bandwidth-DL/UL, SIP-Forking-Indication or Acceptable-Service-Info by name. Left out are the four Float32 AVPs (Max-PLR-DL/UL, Desired-Max-Latency/Loss), a type the dictionary does not encode, and the Gxx, Sd and St AVPs.

DiameterAnswer

The value a handler returns via request.answer(...) / request.reject(...).

Mock DiameterAnswer — the value a handler returns / forwards.

is_error property

is_error: bool

Whether the header E bit is set. An answer a handler builds with request.answer / request.reject has it for a protocol error (3xxx) only: a permanent failure (5xxx) answered by a handler is an answer of the command (RFC 6733 section 7.1.5).

get_avp

get_avp(code: int, vendor: int = 0)

The value of the first AVP with this code and vendor, or None.

set_avp

set_avp(code_or_name, value, vendor: int = 0) -> None

Set an AVP, replacing every AVP with the same code and vendor.

insert_avp

insert_avp(code_or_name, value, vendor: int = 0) -> None

Append an AVP, keeping the ones already there with its code and vendor.

set_avp replaces: it removes every AVP of that code and vendor first, so it cannot build an answer that carries the same AVP more than once at the top level. Call insert_avp once per occurrence for those, in the order they should go on the wire. The arguments are the ones set_avp takes::

answer = req.answer(2001)
answer.set_avp("SIP-Number-Auth-Items", len(vectors))
for vector in vectors:
    answer.insert_avp("SIP-Auth-Data-Item", vector)

One exception: the first insert_avp of an AVP that request.answer() copied from the request (Vendor-Specific-Application-Id, Auth-Session-State) replaces that copy, so adding it yourself does not send it twice.

remove_avp

remove_avp(code: int, vendor: int = 0) -> int

Remove every AVP with this code and vendor; returns how many.

iter_avps

iter_avps() -> list

The top-level AVPs as (code, vendor, value) tuples, in wire order.

The members of a grouped AVP are in another order: its value is a list of (code, value, vendor) tuples, the shape set_avp takes for a child. Unpacking one in the order of the other compares a value with a vendor id and matches nothing. :meth:avps lists the same AVPs with the fields by name.

avps

avps() -> list

The top-level AVPs as :class:MockDiameterAvp objects, in wire order: .code, .vendor, .name and .value, which for a grouped AVP is a list of the same objects.

Reauthorization on a serving connection

code = await diameter.reauthorize(peer, session_id, destination_host,
                                  destination_realm, application,
                                  tenant="default", timeout_ms=5000, **avps)

Sends a Re-Auth-Request (command 258, R and P bits set) to a client on the existing connection that carried that client's requests, for example an RFC 8506 section 5.5 credit reauthorization ("Ro"), a TS 29.212 Gx RAR ("Gx") or a TS 29.214 Rx RAR ("Rx"). application is an application name as send_request takes it; an unknown name raises ValueError. The header Application-Id and the Auth-Application-Id AVP both carry that application, and Re-Auth-Request-Type is AUTHORIZE_ONLY (0). Session-Id is the supplied one and goes first, then Origin-Host and Origin-Realm of the serving connection, Destination-Host and Destination-Realm.

Extra keyword arguments are application AVPs, encoded the way send_request encodes them and appended after those, so a Gx RAR can carry Event-Trigger or Charging-Rule-Install and an Rx RAR its Specific-Action:

await diameter.reauthorize("local-pcef", session_id, "pcef.example", "example",
                           "Gx", event_trigger=2)

Passing one of the AVPs the request sets itself (session_id, origin_host, origin_realm, destination_host, destination_realm, auth_application_id, re_auth_request_type) raises ValueError, as does an empty identity.

Returns the RAA Result-Code as an int. A missing or closed connection, or a timeout, raises RuntimeError; callers should keep their pending notification and retry. For credit control a 2001 or 2002 RAA only acknowledges the request; the client's next CCR carries the actual credit update.

The connection is looked up by tenant, configured peer name and the client's Origin-Host. A connection becomes reachable for an Origin-Host once a request from that host has arrived on it, on a listener connection (diameter.listen) or an outbound serving connection. A relay can carry requests from several clients over one connection, so one connection can serve several Origin-Hosts. The entries are dropped when the connection ends, and a newer connection for the same client replaces an older one. Client-mode peers under diameter.peers are never used here; reach those with send_request(peer=...).

Requests this node originates

answer = await diameter.originate(command, application, avps, destination_realm,
                                  destination_host=None, peer=None,
                                  session_id=None, tenant="default",
                                  proxiable=True, timeout_ms=10000)

Sends a request this node starts on a connection it already has, and awaits its answer. A node in server mode needs it for the procedures that begin at the server: an HSS's Cancel-Location, Insert-Subscriber-Data, Delete-Subscriber-Data and Reset towards an MME (TS 29.272 clauses 5.2.1.2, 5.2.2.1, 5.2.2.2 and 5.2.4.1), or an Alert-Service-Centre over S6c (TS 29.338 clause 5.2.2). The same call serves a PCRF's Rx Abort-Session and Re-Auth towards the P-CSCF and Gx Re-Auth towards the PCEF (TS 29.214, TS 29.212), an HSS's Cx Registration-Termination and Sh Push-Notification (TS 29.229, TS 29.329), and a client's Gx Credit-Control.

command is a name as send_request takes it ("CLR", "Insert-Subscriber-Data") or a command code (322 for the S6a Reset, whose acronym RSR is the S6c Report-SM-Delivery-Status). application is an application name; it goes in the header. The R bit is set, and the P bit unless proxiable=False.

The request starts with Session-Id (session_id, or a new one of the connection's identity), Origin-Host and Origin-Realm of the connection, Destination-Host when given and Destination-Realm. avps follow in their order: each a (code_or_name, value), (code_or_name, value, vendor) or (code_or_name, value, vendor, mandatory) tuple, the shape set_avp takes for the members of a group, and a list as value for a grouped AVP. An AVP can repeat. mandatory sets or clears the M bit the dictionary would give, for an AVP whose table marks it "must not" that the dictionary does not know. Auth-Session-State and Vendor-Specific-Application-Id are the caller's to add or leave out, as are Auth-Application-Id, Re-Auth-Request-Type and CC-Request-Type / CC-Request-Number. Passing one of the envelope AVPs raises ValueError.

A value is checked against the dictionary type before anything is sent: a list for an AVP that is not grouped, a str or int for a grouped one, a number out of range, text for a number, a vendor that is not an int or None, or a mandatory that is not a bool raises TypeError. Bytes stay accepted for a grouped AVP, as get_avp hands back one it could not decode.

A request of an existing session passes its session_id and carries that one Session-Id and no other: a PCRF's RAR and ASR on the session its AAR or CCR-I opened, and a client's CCR-U and CCR-T with the CC-Request-Number it counts.

# PCRF: install a PCC rule on the Gx session the PGW opened (TS 29.212 5.6.4).
answer = await diameter.originate("RAR", "Gx", [
    ("Auth-Application-Id", 16777238),
    ("Re-Auth-Request-Type", 0),
    ("Event-Trigger", 2),
    ("Event-Trigger", 13),
    ("Charging-Rule-Install", [
        ("Charging-Rule-Definition", [
            ("Charging-Rule-Name", b"ims-voice"),
            ("Flow-Information", [("Flow-Description", b"permit out 17 from 198.51.100.20 50000 to 198.51.100.7 40000"),
                                  ("Flow-Direction", 1)]),
            ("Flow-Information", [("Flow-Description", b"permit out 17 from 198.51.100.7 40000 to 198.51.100.20 50000"),
                                  ("Flow-Direction", 2)]),
            ("QoS-Information", [("QoS-Class-Identifier", 1),
                                 ("Allocation-Retention-Priority", [("Priority-Level", 2)])]),
        ]),
    ]),
], destination_realm="epc.mnc001.mcc001.3gppnetwork.org",
   destination_host=gx_session.origin_host, session_id=gx_session.session_id)
answer = await diameter.originate("CLR", "S6a", [
    ("Vendor-Specific-Application-Id", [("Vendor-Id", 10415),
                                        ("Auth-Application-Id", 16777251)]),
    ("Auth-Session-State", 1),
    ("User-Name", "001010000000001"),
    (1420, 2, 10415),                                  # Cancellation-Type
    (1638, (2).to_bytes(4, "big"), 10415, False),      # CLR-Flags, no M bit
], destination_realm="epc.mnc001.mcc001.3gppnetwork.org",
   destination_host="mme1.epc.mnc001.mcc001.3gppnetwork.org")
if answer.result_code == 2001: ...

The connection: with peer, the serving connection of that configured entry that carried requests from destination_host, else the tenant's relay backend of that name, else the client-mode peer of that name (diameter.peers). Client-mode peers belong to the node, not to a tenant, so only tenant="default" reaches them: a PCEF's Gx CCR to its PCRF is originate("CCR", "Gx", ..., peer="pcrf"). Without, the serving connection that last carried a request from destination_host, else one of an agent that relayed requests from destination_realm, which routes on to the host (RFC 6733 §6.1.5). A request it relayed carries Route-Record, which is how an agent is told from a direct peer. Another direct peer of the realm is not the host and is never used for it: with no connection to the host and no agent, nothing is sent and diameter.UnableToDeliver is raised (DIAMETER_UNABLE_TO_DELIVER, RFC 6733 §6.1). Without destination_host, any connection that carried a request from a node of destination_realm. A serving connection becomes reachable once a request from that host or realm has arrived on it (diameter.listen or connect_to). All scoped to tenant.

Resolves to a DiameterAnswer (result_code, experimental_result_code, get_avp, avps()), whatever its result: a negative answer is told from a failure to get one. experimental_result_code is the code inside Experimental-Result, where the 3GPP applications put the ones they define (5001 DIAMETER_ERROR_USER_UNKNOWN of TS 29.229, for one), and None when the answer carries a Result-Code. No open connection raises LookupError (diameter.UnableToDeliver, a subclass, for a destination_host) and nothing is sent; no answer within timeout_ms raises TimeoutError; a connection that closes before the answer raises ConnectionError. siphon does not resend: the script decides whether to repeat.

scripts/validate_diameter_originate.sh reads the requests built from these AVP lists back with Wireshark's Diameter dissector: Cx RTR, Sh PNR, Rx ASR and RAR, Gx RAR with a rule installation, Gx CCR-I, -U and -T on one session, and S6a CLR, IDR and DSR.