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:
- Every application named in a registered
@diameter.on_requestfilter:@diameter.on_request("S6a:AIR")makes the node an S6a server. - The
applicationslist 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.
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
¶
Read-only view of the parsed diameter config (tenants/listen).
Set it in tests with diameter.set_config({...}).
event_sink
property
¶
The generic event sink (diameter.event_sink.emit(row)).
is_connected
¶
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
|
|
peer_count
¶
Get the number of connected peers.
Returns:
| Type | Description |
|---|---|
int
|
Count of peers that are marked as connected. |
decode_isdn_address
¶
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]
|
|
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 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 |
TON_NPI_INTERNATIONAL_E164
|
Returns:
| Type | Description |
|---|---|
bytes
|
The encoded ISDN-AddressString |
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. |
required |
visited_network_id
|
Optional[str]
|
Visited network identifier. |
None
|
user_auth_type
|
Optional[int]
|
User-Authorization-Type AVP value (3GPP TS 29.229).
|
None
|
user_name
|
Optional[str]
|
The private user identity (User-Name): the |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Optional[dict]
|
|
Optional[dict]
|
|
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, |
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 |
Optional[dict]
|
SIP-Auth-Data-Item), as configured with :meth: |
Optional[dict]
|
or |
Raises:
| Type | Description |
|---|---|
ValueError
|
an identity or |
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 |
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 |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Optional[dict]
|
Experimental-Result-Code), |
Optional[dict]
|
None; 5401 is DIAMETER_ERROR_IDENTITY_UNKNOWN), |
Optional[dict]
|
|
Optional[dict]
|
|
Optional[dict]
|
meth: |
Optional[dict]
|
for the identity, which is what a script sees with no peer. |
Raises:
| Type | Description |
|---|---|
ValueError
|
neither |
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 |
None
|
destination_host
|
Optional[str]
|
Destination-Host. Defaults to the peer's. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Optional[dict]
|
Dict with |
|
Optional[dict]
|
|
|
Optional[dict]
|
DIAMETER_ERROR_TRANSACTION_IDENTIFIER_INVALID, 5402 |
|
Optional[dict]
|
DIAMETER_ERROR_NOT_AUTHORIZED), |
|
Optional[dict]
|
|
|
Optional[dict]
|
|
|
Optional[dict]
|
seconds), |
|
Optional[dict]
|
|
|
with |
Optional[dict]
|
meth: |
Optional[dict]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
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:
|
None
|
user_data_already_available
|
bool
|
|
False
|
ims_restoration
|
bool
|
|
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
|
None
|
multiple_registration
|
Optional[bool]
|
Multiple-Registration-Indication (TS
29.229 clause 6.3.51); |
None
|
pcscf_restoration_mechanism
|
bool
|
|
False
|
pcscf_restoration_indication
|
bool
|
|
False
|
failed_pcscf
|
Optional[dict]
|
Failed-PCSCF (TS 29.229 clause 6.3.74), a dict with
|
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Optional[dict]
|
|
Optional[dict]
|
|
Optional[dict]
|
or |
Optional[dict]
|
returned with the keys it lacks filled in. |
cx_lir
async
¶
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, |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Optional[dict]
|
|
Optional[dict]
|
|
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
|
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'
|
subscription_id
|
Optional[tuple]
|
Optional |
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
|
service_urn
|
Optional[str]
|
The service URN of an emergency session, e.g.
|
None
|
Returns:
| Type | Description |
|---|---|
Awaitable[Optional[dict]]
|
Dict with |
Raises:
| Type | Description |
|---|---|
ValueError
|
A |
TypeError
|
|
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
¶
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 |
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. |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
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. |
required |
xml
|
str
|
UTF-8 XML payload. |
required |
service_indication
|
Optional[str]
|
e.g. |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
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
|
|
required |
service_indication
|
Optional[str]
|
e.g. |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
add_peer
¶
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 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. |
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
|
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 |
required |
result_code
|
int
|
Result-Code (default 2001 = SUCCESS). |
2001
|
auth_items
|
Optional[list]
|
One dict per SIP-Auth-Data-Item, with the keys
|
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 |
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 |
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
|
|
False
|
scscf_restoration_info
|
Optional[list[dict]]
|
The SCSCF-Restoration-Info groups, in the
shape |
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 |
None
|
set_aar_response
¶
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
¶
Override the Result-Code returned by every Rf ACA (default 2001).
set_rf_interim_interval
¶
Configure the Acct-Interim-Interval returned in ACA-START.
captured_acrs
¶
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.
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
¶
Override the Result-Code returned by every Ro CCA (default 2001).
set_ro_granted_time
¶
Configure the granted CC-Time (seconds) returned in a successful CCA.
set_ro_final_unit_action
¶
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
¶
The open RoSession filed under key, or None.
trigger_ro_event
¶
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
¶
Return all CCRs the script has emitted via ro_ccr_* (fresh copy).
on_inbound_cer
staticmethod
¶
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
¶
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
¶
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
¶
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
¶
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
¶
Whether addr falls within cidr (mirrors the Rust helper).
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
¶
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
|
|
required |
node
|
str
|
The node the delivery was attempted through: |
'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.
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
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
¶
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
¶
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
¶
Configure a mock PUA response for a specific user (test helper).
set_snr_response
¶
Configure a mock SNA response for a specific user (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
¶
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
¶
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
¶
The AVPs the dictionary does not define, as :class:MockDiameterAvp
objects with name None and mandatory as given.
reject_unsupported_avps
¶
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
¶
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
¶
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
¶
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, |
|
name |
The dictionary name, or |
|
value |
The value as |
|
flags |
The flags octet of the AVP header: V |
|
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
¶
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
¶
The value of the first AVP with this code and vendor, or None.
set_avp
¶
Set an AVP, replacing every AVP with the same code and vendor.
insert_avp
¶
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 every AVP with this code and vendor; returns how many.
iter_avps
¶
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
¶
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.