diameter(3erl)
๐ NAME ๐
diameter(3erl) โ Erlang Module Definition, and the diameter module is the Main API of the diameter application. Bestie, this is the one. โจ
๐ฆ DESCRIPTION ๐ฆ
Okay so this module gives you the interface where a user can implement a Diameter node that sends and receives messages using the Diameter protocol as defined in RFC 6733. It's giving main character energy for your whole Diameter setup. ๐
Basic usage consists of creating a representation of a locally implemented Diameter node and its capabilities with start_service/2, adding transport capability using add_transport/2, and sending Diameter requests and receiving Diameter answers with call/4. Incoming Diameter requests are communicated as callbacks to a diameter_app(3erl) callback modules as specified in the service configuration.
Beware the difference between diameter (not capitalized) and Diameter (capitalized). The former refers to the Erlang application named diameter whose main api is defined here, the latter to Diameter protocol in the sense of RFC 6733.
The diameter application must be started before calling most functions in this module.
๐ธ DATA TYPES ๐ธ
Here's the type roster, no cap. โจ
Address()DiameterIdentity()Grouped()OctetString()Time()Unsigned32()UTF8String()โ Types corresponding to RFC 6733 AVP Data Formats. Defined indiameter_dict(5).
๐ application_alias() = term() ๐
Name identifying a Diameter application in service configuration. Passed to call/4 when sending requests defined by the application.
๐ฆ application_module() ๐ฆ
application_module() = Mod | [Mod | ExtraArgs] | #diameter_callback{}
Mod = atom()
ExtraArgs = list()
Module implementing the callback interface defined in diameter_app(3erl), along with any extra arguments to be appended to those documented. Note that extra arguments specific to an outgoing request can be specified to call/4, in which case those are appended to any module-specific extra arguments.
Specifying a #diameter_callback{} record allows individual functions to be configured in place of the usual diameter_app(3erl) callbacks. See diameter_callback.erl for details.
๐
application_opt() ๐
Options defining a Diameter application. Has one of the following types.
{alias, application_alias()}โ Unique identifier for the application in the scope of the service. Defaults to the value of thedictionaryoption.{dictionary, atom()}โ Name of an encode/decode module for the Diameter messages defined by the application. These modules are generated from files whose format is documented indiameter_dict(5).{module, application_module()}โ Callback module in which messages of the Diameter application are handled. Seediameter_app(3erl)for the required interface and semantics.{state, term()}โ Initial callback state. The prevailing state is passed to somediameter_app(3erl)callbacks, which can then return a new state. Defaults to the value of thealiasoption.{call_mutates_state, true|false}โ Whether or not thepick_peer/4application callback can modify the application state. Defaults tofalse.
Warning:pick_peer/4callbacks are serialized when this option istrue, which is a potential performance bottleneck. A simple Diameter client may suffer no ill effects from using mutable state but a server or agent that responds to incoming request should probably avoid it.
{answer_errors, callback|report|discard}โ Manner in which incoming answer messages containing decode errors are handled. Ifcallbackthen errors result in ahandle_answer/4callback in the same fashion as forhandle_request/3, with errors communicated in theerrorsfield of the#diameter_packet{}passed to the callback. Ifreportthen an answer containing errors is discarded without a callback and a warning report is written to the log. Ifdiscardthen an answer containing errors is silently discarded without a callback. In both thereportanddiscardcases the return value for thecall/4invocation in question is as if a callback had taken place and returned{error, failure}. Defaults todiscard.{request_errors, answer_3xxx|answer|callback}โ Manner in which incoming requests are handled when an error other than 3007 (DIAMETER_APPLICATION_UNSUPPORTED, which cannot be associated with an application callback module), is detected. Ifanswer_3xxxthen requests are answered without ahandle_request/3callback taking place. Ifanswerthen even 5xxx errors are answered without a callback unless the connection in question has configured the RFC 3588 common dictionary as noted below. Ifcallbackthen ahandle_request/3callback always takes place and its return value determines the answer sent to the peer, if any. Defaults toanswer_3xxx.
Note: Answers sent bydiameterset the E-bit in the Diameter Header. Since RFC 3588 allows only 3xxx result codes in an answer-message,answerhas the same semantics asanswer_3xxxwhen the transport in question has been configured withdiameter_gen_base_rfc3588as its common dictionary. Since RFC 6733 allows both 3xxx and 5xxx result codes in an answer-message, a transport withdiameter_gen_base_rfc6733as its common dictionary does distinguish betweenanswer_3xxxandanswer.
๐ call_opt() ๐
Options available to call/4 when sending an outgoing Diameter request. Has one of the following types.
{extra, list()}โ Extra arguments to append to callbacks to the callback module in question. These are appended to any extra arguments configured on the callback itself. Multiple options append to the argument list.{filter, peer_filter()}โ Filter to apply to the list of available peers before passing it to thepick_peer/4callback for the application in question. Multiple options are equivalent to a single all filter on the corresponding list of filters. Defaults tonone.{peer, diameter_app:peer_ref()}โ Peer to which the request in question can be sent, preempting the selection of peers having advertised support for the Diameter application in question. Multiple options can be specified, and their order is respected in the candidate lists passed to a subsequentpick_peer/4callback.{timeout, Unsigned32()}โ Number of milliseconds after which the request should timeout. Defaults to5000.detachโ Causecall/4to returnokas soon as the request in question has been encoded, instead of waiting for and returning the result from a subsequenthandle_answer/4orhandle_error/4callback.
An invalid option will cause call/4 to fail.
โญ capability() โญ
AVP values sent in outgoing CER or CEA messages during capabilities exchange. Can be configured both on a service and a transport, values on the latter taking precedence. Has one of the following types.
{'Origin-Host', DiameterIdentity()}{'Origin-Realm', DiameterIdentity()}{'Host-IP-Address', [Address()]}โ An address list is available to the start function of a transport module, which can return a new list for use in the subsequent CER or CEA.Host-IP-Addressneed not be specified if the transport module in question communicates an address list as described indiameter_transport(3erl){'Vendor-Id', Unsigned32()}{'Product-Name', UTF8String()}{'Origin-State-Id', Unsigned32()}โOrigin-State-Idis optional but, if configured, will be included in outgoing CER/CEA and DWR/DWA messages. Setting a value of0(zero) is equivalent to not setting a value, as documented in RFC 6733. The functionorigin_state_id/0can be used as to retrieve a value that is computed when thediameterapplication is started.{'Supported-Vendor-Id', [Unsigned32()]}{'Auth-Application-Id', [Unsigned32()]}{'Inband-Security-Id', [Unsigned32()]}โInband-Security-Iddefaults to the empty list, which is equivalent to a list containing only0(NO_INBAND_SECURITY). If1(TLS) is specified then TLS is selected if the CER/CEA received from the peer offers it.{'Acct-Application-Id', [Unsigned32()]}{'Vendor-Specific-Application-Id', [Grouped()]}{'Firmware-Revision', Unsigned32()}
Note that each tuple communicates one or more AVP values. It is an error to specify duplicate tuples.
๐ง eval() ๐ง
eval() = {M,F,A} | fun() | [eval() | A]
An expression that can be evaluated as a function in the following sense.
eval([{M,F,A} | T]) ->
apply(M, F, T ++ A);
eval([[F|A] | T]) ->
eval([F | T ++ A]);
eval([F|A]) ->
apply(F, A);
eval(F) ->
eval([F]).
Applying an eval() E to an argument list A is meant in the sense of eval([E|A]).
Warning: Beware of using fun expressions of the formfun Name/Arityin situations in which the fun is not short-lived and code is to be upgraded at runtime since any processes retaining such a fun will have a reference to old code. In particular, such a value is typically inappropriate in configuration passed tostart_service/2oradd_transport/2.
๐ peer_filter() = term() ๐
Filter passed to call/4 in order to select candidate peers for a pick_peer/4 callback. Has one of the following types.
noneโ Matches any peer. This is a convenience that provides a filter equivalent to no filter.hostโ Matches only those peers whoseOrigin-Hosthas the same value asDestination-Hostin the outgoing request in question, or any peer if the request does not contain aDestination-HostAVP.realmโ Matches only those peers whoseOrigin-Realmhas the same value asDestination-Realmin the outgoing request in question, or any peer if the request does not contain aDestination-RealmAVP.{host, any|DiameterIdentity()}โ Matches only those peers whoseOrigin-Hosthas the specified value, or all peers if the atomany.{realm, any|DiameterIdentity()}โ Matches only those peers whoseOrigin-Realmhas the specified value, or all peers if the atomany.{eval, eval()}โ Matches only those peers for which the specifiedeval()returnstruewhen applied to the connection'sdiameter_capsrecord. Any other return value or exception is equivalent tofalse.{neg, peer_filter()}โ the hater-blocker ๐ it matches only those peers that the filter you hand it does not match. opposite day, but make it fashion.{all, [peer_filter()]}โ matches only those peers matched by each filter in the list you pass. everyone's gotta agree, no exceptions ๐ซถ{any, [peer_filter()]}โ matches only those peers matched by at least one filter in the list. and bestie, the resulting list comes back in match order: peers that matched the first filter sort before the ones matched by the second, and so on down the line. first come, first served energy โจ{first, [peer_filter()]}โ literallyanybut it stops at the first filter that has matches. that's it, that's the whole thing, and it's highkey so much more efficient when you've got a ton of peers. like, the following filter makes only the peers that best match both the host and realm filters show up:
{first, [{all, [host, realm]}, realm]}
An invalid filter is equivalent to {any,[]} โ which is the filter that matches no peer. so if you fumble it, you get nobody. ghosted by your own config ๐ญ
Note: thehostandrealmfilters cause the Destination-Host and Destination-Realm AVPs to be extracted from the outgoing request โ assuming it's a record- or list-valueddiameter_codec:message(), and assuming at most one of each AVP. If that's not your situation then you gotta use the{host|realm, DiameterIdentity()}filters to get the result you actually want. And an emptyDiameterIdentity()(which, like, shouldn't really be typical) matches all hosts/realms for the purposes of filtering.
Warning: a host filter is not typically desirable when setting Destination-Host, because it will remove peer agents from the candidates list. not the vibe you want ๐ฅ
service_event() = #diameter_event{service = service_name(), info = service_event_info()}:
An event message sent to processes that have subscribed to these using subscribe/1. ๐
service_event_info() = term():
The info field of a service_event() record. Can have one of the following types. ๐
start:stop:
The service is being started or stopped. No event precedes a start event. No event follows a stop event, and this event implies the termination of all transport processes. ๐
{up, Ref, Peer, Config, Pkt}:{up, Ref, Peer, Config}:{down, Ref, Peer, Config}:
Ref = transport_ref()
Peer = diameter_app:peer()
Config = {connect|listen, [transport_opt()]}
Pkt = #diameter_packet{}
The RFC 3539 watchdog state machine has transitioned into (up) or out of (down) the OKAY state. If a #diameter_packet{} is present in an up event then there has been a capabilities exchange on a newly established transport connection, and the record contains the received CER or CEA. ๐ฆ
Note: a singleupordownevent for a given peer corresponds to multiplepeer_up/3orpeer_down/3callbacks โ one for each of the Diameter applications negotiated during capabilities exchange. That is, the event communicates connectivity with the peer as a whole, while the callbacks communicate connectivity with respect to individual Diameter applications. two different zoom levels, iykyk ๐
{reconnect, Ref, Opts}:
Ref = transport_ref()
Opts = [transport_opt()]
A connecting transport is attempting to establish/reestablish a transport connection with a peer following connect_timer or watchdog_timer expiry. it's giving "we lost connection, trying again" ๐
{closed, Ref, Reason, Config}:
Ref = transport_ref()
Config = {connect|listen, [transport_opt()]}
Capabilities exchange has failed. ๐ฌ Reason can have one of the following types.
{'CER', Result, Caps, Pkt}:
Result = ResultCode | {capabilities_cb, CB, ResultCode|discard}
Caps = #diameter_caps{}
Pkt = #diameter_packet{}
ResultCode = integer()
CB = eval()
An incoming CER has been answered with the indicated result code, or discarded. Caps contains pairs of values, for the local node and remote peer respectively. Pkt contains the CER in question. In the case of rejection by a capabilities callback, the tuple contains the rejecting callback. ๐
{'CER', Caps, {ResultCode, Pkt}}:
ResultCode = integer()
Caps = #diameter_caps{}
Pkt = #diameter_packet{}
An incoming CER contained errors and has been answered with the indicated result code. Caps contains values for the local node only. Pkt contains the CER in question. local only, no peer tea ๐ซ
{'CER', timeout}:
An expected CER was not received within capx_timeout of connection establishment. they left us on read ๐
{'CEA', Result, Caps, Pkt}:
Result = ResultCode | atom() | {capabilities_cb, CB, ResultCode|discard}
Caps = #diameter_caps{}
Pkt = #diameter_packet{}
ResultCode = integer()
An incoming CEA has been rejected for the indicated reason. An integer-valued Result indicates the result code sent by the peer. Caps contains pairs of values for the local node and remote peer. Pkt contains the CEA in question. In the case of rejection by a capabilities callback, the tuple contains the rejecting callback.
{'CEA', Caps, Pkt}:
Caps = #diameter_caps{}
Pkt = #diameter_packet{}
An incoming CEA contained errors and has been rejected. Caps contains only values for the local node. Pkt contains the CEA in question.
{'CEA', timeout}:
An expected CEA was not received within capx_timeout of connection establishment.
{watchdog, Ref, PeerRef, {From, To}, Config}:
Ref = transport_ref()
PeerRef = diameter_app:peer_ref()
From, To = initial | okay | suspect | down | reopen
Config = {connect|listen, [transport_opt()]}
An RFC 3539 watchdog state machine has changed state. glowed up from one state to another โจ
any():
For forward compatibility, a subscriber should be prepared to receive info fields of forms other than the above. future-proofing, bestie ๐ง
service_name() = term():
Name of a service as passed to start_service/2 and with which the service is identified. There can be at most one service with a given name on a given node. Note that erlang:make_ref/0 can be used to generate a service name that is somewhat unique. ๐ท๏ธ
service_opt():
Option passed to start_service/2. Can be any capability() as well as the following. ๐
{application, [application_opt()]}โ A Diameter application supported by the service.
A service must configure one tuple for each Diameter application it intends to support. For an outgoing request, the relevant application_alias() is passed to call/4, while for an incoming request the application identifier in the message header determines the application, the identifier being specified in the application's dictionary file. ๐
Warning: the capabilities advertised by a node must match its configured applications. In particular, application configuration must be matched by correspondingcapability()configuration, of*-Application-IdAVPs in particular. mismatched vibes = bad vibes. keep them in sync or suffer ๐
{decode_format, record | list | map | none}โ The format of decoded messages and grouped AVPs in themsgfield ofdiameter_packetrecords andvaluefield ofdiameter_avprecords respectively. Ifrecordthen a record whose definition is generated from the dictionary file in question. Iflistormapthen a[Name | Avps]pair whereAvpsis a list of AVP name/values pairs or a map keyed on AVP names respectively. Ifnonethen the atom-value message name, orundefinedfor a Grouped AVP. See alsodiameter_codec:message().
Defaults to record. ๐
Note: AVPs are decoded into a list ofdiameter_avprecords inavpsfield ofdiameter_packetrecords independently ofdecode_format. so that part's non-negotiable, periodt.
{restrict_connections, false | node | nodes | [node()] | eval()}โ The degree to which the service allows multiple transport connections to the same peer, as identified by its Origin-Host at capabilities exchange.
If [node()] then a connection is rejected if another already exists on any of the specified nodes. Types false, node, nodes and eval() are equivalent to [], [node()], [node()|nodes()] and the evaluated value respectively, evaluation of each expression taking place whenever a new connection is to be established. Note that false allows an unlimited number of connections to be established with the same peer. ๐ช๐
Multiple connections are independent and governed by their own peer and watchdog state machines. they don't share, they don't compare ๐ซถ
Defaults to nodes.
{sequence, {H,N} | eval()}โ A constant valueHfor the topmost32-Nbits of of 32-bit End-to-End and Hop-by-Hop Identifiers generated by the service, either explicitly or as a return value of a function to be evaluated atstart_service/2. In particular, an identifierIdis mapped to a new identifier as follows. ๐ข
(H bsl N) bor (Id band ((1 bsl N) - 1))
Note that RFC 6733 requires that End-to-End Identifiers remain unique for a period of at least 4 minutes and that this and the call rate places a lower bound on appropriate values of N: at a rate of R requests per second, an N-bit counter traverses all of its values in (1 bsl N) div (R*60) minutes, so the bound is 4*R*60 =< 1 bsl N. do the math, bestie, it's mathing ๐งฎ
N must lie in the range 0..32 and H must be a non-negative integer less than 1 bsl (32-N).
Defaults to {0,32}.
Warning: multiple Erlang nodes implementing the same Diameter node should be configured with different sequence masks to ensure that each node uses a unique range of End-to-End and Hop-by-Hop Identifiers for outgoing requests. don't let your nodes twinning too hard on the ID space ๐
{share_peers, boolean() | [node()] | eval()}โ Nodes to which peer connections established on the local Erlang node are communicated. Shared peers become available in the remotecandidateslist passed topick_peer/4callbacks on remote nodes whose services are configured to use them: seeuse_shared_peersbelow. ๐ค
If false then peers are not shared. If [node()] then peers are shared with the specified list of nodes. If eval() then peers are shared with the nodes returned by the specified function, evaluated whenever a peer connection becomes available or a remote service requests information about local connections. The value true is equivalent to fun erlang:nodes/0. The value node() in a list is ignored, so a collection of services can all be configured to share with the same list of nodes. sharing is caring, literally ๐
Defaults to false.
Note ๐ซถ: Peers are only shared with services of the same name for the purpose of sending outgoing requests. Since the value of theapplication_opt()alias, passed tocall/4, is the handle for identifying a peer as a suitable candidate, services that share peers must use the same aliases to identify their supported applications. They should typically also configure identicalcapabilities(), since by sharing peer connections they're distributing the implementation of a single Diameter node across multiple Erlang nodes.
{strict_arities, boolean() | encode | decode}โ this one is the vibe check for AVP counts ๐ Should the number of AVPs in a message or grouped AVP be required to agree with what the dictionary in question says, when messages get passed todiameter_app(3erl)callbacks?- If
true, then mismatches in an outgoing message make message encoding fail ๐ญ, while mismatches in an incoming message are reported as5005/5009errors in theerrorsfield of thediameter_packetrecord passed tohandle_request/3orhandle_answer/4callbacks. - If
false, then neither error is enforced/detected. - If
encodeordecode, then errors are only enforced/detected on outgoing or incoming messages respectively. - Defaults to
true.
- If
Note ๐ง: Disabling arity checks affects the form of messages at encode/decode. In particular, decoded AVPs are represented as lists of values, regardless of the AVP's arity (ie. expected number in the message/AVP grammar in question), and values are expected to be supplied as lists at encode. This differs from the historic decode behaviour of representing AVPs of arity 1 as bare values, not wrapped in a list.
{string_decode, boolean()}โ should AVPs of typeOctetString()and its derived typesDiameterIdentity(),DiameterURI(),IPFilterRule(),QoSFilterRule(), andUTF8String()be decoded? Iftrue, AVPs of these types get decoded tostring(). Iffalse, the values stay asbinary().- Defaults to
true.
- Defaults to
Warning โ ๏ธ: This option should be set tofalsesince a sufficiently malicious peer can otherwise cause large amounts of memory to be consumed when decoded Diameter messages are passed between processes. The default value is for backwards compatibility. Not a slay. Set it tofalse, bestie ๐
{traffic_counters, boolean()}โ should application-specific messages be counted? Y'know, the ones wherediameter_app(3erl)callbacks take place. Iffalse, then only messages handled by diameter itself are counted:CER/CEA,DWR/DWA,DPR/DPA.- Defaults to
true.
- Defaults to
Note ๐ธ: Disabling counters is a performance improvement, but it means the omitted counters are not returned by service_info/2.
{use_shared_peers, boolean() | [node()] | eval()}โ which nodes make communicated peers available in the remote candidates list ofpick_peer/4callbacks? ๐ฆ- If
false, then remote peers are not used. - If
[node()], then only peers from the specified list of nodes are used. - If
eval(), then only peers returned by the specified function are used, evaluated whenever a remote service communicates information about an available peer connection. - The value
trueis equivalent tofun erlang:nodes/0. The valuenode()in a list is ignored. - Defaults to
false.
- If
Note ๐: A service that does not use shared peers will always pass the empty list as the second argument of pick_peer/4 callbacks.
Warning ๐: Sending a request over a peer connection on a remote node is less efficient than sending it over a local connection. It may be preferable to make use of theservice_opt()restrict_connectionsand maintain a dedicated connection on each node from which requests are sent.
transport_opt()โ literally any transport option exceptapplications,capabilities,transport_config, andtransport_module. Used as defaults for transport configuration; values passed toadd_transport/2override values configured on the service โจ
๐ transport_opt() ๐
Option passed to add_transport/2. It has one of the following types, bestie โ buckle up ๐
{applications, [application_alias()]}โ Diameter applications to which the transport should be restricted. Defaults to all applications configured on the service in question. Applications not configured on the service in question are ignored.
Warning ๐: The capabilities advertised by a node must match its configured applications. In particular, settingapplicationson a transport typically implies having to set matching*-Application-IdAVPs in acapabilities()tuple.
{avp_dictionaries, [module()]}โ a list of alternate dictionary modules to encode/decode AVPs that are not defined by the dictionary of the application in question ๐- At decode, such AVPs are represented as
diameter_avprecords in the'AVP'field of a decoded message or Grouped AVP, with the first alternate that succeeds in decoding the AVP setting the record's value field. - At encode, values in an
'AVP'list can be passed as AVP name/value 2-tuples, and it is an encode error for no alternate to define the AVP of such a tuple. - Defaults to the empty list.
- At decode, such AVPs are represented as
Note ๐ฆ: The motivation for alternate dictionaries is RFC 7683, Diameter Overload Indication Conveyance (DOIC), which defines AVPs to be piggybacked onto existing application messages rather than defining an application of its own. The DOIC dictionary is provided by the diameter application, as module diameter_gen_doic_rfc7683, but alternate dictionaries can be used to encode/decode any set of AVPs not known to an application dictionary.
{capabilities, [capability()]}โ AVPs used to construct outgoingCER/CEAmessages. These values take precedence over any specified on the service in question ๐ฅ- Specifying a capability as a transport option may be particularly appropriate for
Inband-Security-Id, in case TLS is desired over TCP as implemented bydiameter_tcp(3erl).
- Specifying a capability as a transport option may be particularly appropriate for
{capabilities_cb, eval()}โ callback invoked upon reception ofCER/CEAduring capabilities exchange, to ask whether or not the connection should be accepted. Applied to thetransport_ref()and#diameter_caps{}record of the connection.- The return value can have one of the following types:
okโ accept the connection โinteger()โ causes an incomingCERto be answered with the specifiedResult-Code.discardโ causes an incomingCERto be discarded withoutCEAbeing sent.unknownโ equivalent to returning3010,DIAMETER_UNKNOWN_PEER.
- Returning anything but
okor a 2xxx series result code causes the transport connection to be broken. Multiplecapabilities_cboptions can be specified, in which case the corresponding callbacks are applied until either all returnokor one does not.
- The return value can have one of the following types:
{capx_timeout, Unsigned32()}โ number of milliseconds after which a transport process having an established transport connection will be terminated if the expected capabilities exchange message (CERorCEA) is not received from the peer. For a connecting transport, the timing of connection attempts is governed byconnect_timerorwatchdog_timerexpiry. For a listening transport, the peer determines the timing.- Defaults to
10000.
- Defaults to
{connect_timer, Tc}โ for a connecting transport, the RFC 6733Tctimer, in milliseconds โจ
{connect_timer, Tc}
Tc = Unsigned32()
- This timer determines the frequency with which a transport attempts to establish an initial connection with its peer following transport configuration. Once an initial connection has been established,
watchdog_timerdetermines the frequency of reconnection attempts, as required by RFC 3539. - For a listening transport, the timer specifies the time after which a previously connected peer will be forgotten: a connection after this time is regarded as an initial connection rather than reestablishment, causing the RFC 3539 state machine to pass to state
OKAYrather thanREOPEN. Note that these semantics are not governed by the RFC, and that a listening transport'sconnect_timershould be greater than its peer'sTwplus jitter. - Defaults to
30000for a connecting transport and60000for a listening transport.
{disconnect_cb, eval()}โ callback invoked prior to terminating the transport process of a transport connection having watchdog stateOKAY. Applied toapplication|service|transportand thetransport_ref()anddiameter_app:peer()in question:applicationindicates that the diameter application is being stopped,servicethat the service in question is being stopped bystop_service/1, andtransportthat the transport in question is being removed byremove_transport/2.- The return value can have one of the following types:
{dpr, [option()]}โ sendDisconnect-Peer-Requestto the peer, with the transport process being terminated following reception ofDisconnect-Peer-Answeror timeout. Anoption()can be one of the following:{cause, 0|rebooting|1|busy|2|goaway}โ theDisconnect-Causeto send:REBOOTING,BUSYandDO_NOT_WANT_TO_TALK_TO_YOUrespectively. Defaults torebootingforReason=service|applicationandgoawayforReason=transport.{timeout, Unsigned32()}โ number of milliseconds after which the transport process is terminated ifDPAhas not been received. Defaults to the value ofdpa_timeout.
dprโ equivalent to{dpr, []}.closeโ terminate the transport process withoutDisconnect-Peer-Requestbeing sent to the peer.ignoreโ equivalent to not having configured the callback.
- Multiple
disconnect_cboptions can be specified, in which case the corresponding callbacks are applied until one of them returns a value other thanignore. All callbacks returningignoreis equivalent to not having configured them. - Defaults to a single callback returning
dpr.
- The return value can have one of the following types:
{dpa_timeout, Unsigned32()}โ number of milliseconds after which a transport connection is terminated following an outgoingDPRifDPAis not received ๐ญ- Defaults to
1000.
- Defaults to
{dpr_timeout, Unsigned32()}โ the number of milliseconds before a transport connection gets yeeted ๐ after an incoming DPR, if the peer still hasn't closed the connection.
Defaults to 5000.
{incoming_maxlen, 0..16777215}โ the cap on how big we expect incoming Diameter messages to be. Anything bigger than this many bytes gets tossed in the bin. ๐๏ธ
Defaults to 16777215, which is literally the max value the 24-bit Message Length field in a Diameter Header can hold.
{length_errors, exit|handle|discard}โ how to deal with a broken Message Length field in the Diameter Header of an incoming message. And "broken" here means: the length isn't at least 20 bytes (that's a Header, bestie), isn't a multiple of 4 (which is what a valid length looks like), or just isn't the actual length of the message in question as it came in over the transport interface documented indiameter_transport(3erl).- If
exit, the transport process in question exits. periodt. - If
handle, the message gets processed as usual, and if ahandle_request/3orhandle_answer/4callback takes place it'll flag the 5015 error (DIAMETER_INVALID_MESSAGE_LENGTH). - If
discard, the message is silently yeeted, no comment.
- If
Defaults to exit.
Note: the default is doing a lot of heavy lifting here, and for good reason โ a transport module for a stream-oriented transport like TCP might literally not be able to recover from a message length error, because that kind of transport has to use the Message Length header to chop the incoming byte stream into individual Diameter messages. An invalid length leaves it with zero reliable way to find message boundaries again, which can nuke subsequent messages too. Go read diameter_tcp(3erl) for what that module actually does. ๐ง
{pool_size, pos_integer()}โ how many transport processes to spin up. For a listening transport, this sets the size of the pool of accepting transport processes, and a bigger number is what you want for handling multiple concurrent peer connection attempts. For a connecting transport, it sets how many connections to the peer in question we'll try to establish: you should also configureservice_opt(): restrict_connectionson the service in question so multiple connections to the same peer are even allowed. ๐{spawn_opt, [term()] | {M,F,A}}โ an options list passed toerlang:spawn_opt/2to spawn a handler process for an incoming Diameter request on the local node, or an MFA that returns the pid of a handler process.
In the list-valued case, the monitor and link options are ignored. An MFA gets applied with an extra term prepended to its argument list, and it should return either the pid of the handler process โ which invokes diameter_traffic:request/1 on that argument to process the request โ or the atom discard. The handler process doesn't have to be local, and diameter doesn't need to be started on the remote node, but diameter and the relevant application callbacks must be on the code path. ๐
Defaults to the empty list.
{strict_capx, boolean()]}โ whether or not to enforce the RFC 6733 requirement that any message that shows up before capabilities exchange should close the peer connection. Iffalse, unexpected messages just get discarded instead.
Defaults to true. Changing this gives you non-standard behaviour, but it's handy when you know a peer is out here behaving badly. ๐
{strict_mbit, boolean()}โ whether or not to treat an AVP that sets the M-bit as erroneous when the command grammar in question doesn't explicitly allow that AVP. Iftrue, those AVPs are treated as 5001 errors,DIAMETER_AVP_UNSUPPORTED. Iffalse, the M-bit is straight-up ignored and policing it becomes the receiver's problem.
Defaults to true.
Warning: RFC 6733 is honestly unclear about what the M-bit even means. On the one hand, the CCF specification in section 3.2 documents AVP in a command grammar as meaning any arbitrary AVP; on the other hand, 1.3.4 says AVPs that set the M-bit can't be added to an existing command โ instead the modified command has to live in a new Diameter application.
The reason for the latter is presumably interoperability: if you allow arbitrary AVPs that set the M-bit inside a command, interpreting that command becomes implementation-dependent, because there's no guarantee every implementation will understand the same set of arbitrary AVPs in the context of a given command. But if you interpret AVP in a command grammar as any AVP regardless of M-bit, then 1.3.4 means literally nothing, since the receiver can just ignore any AVP it thinks isn't relevant, whatever the sender intended. ๐
Don't go confusing mandatory in the M-bit sense with mandatory in the command grammar sense. The first one is a semantic requirement: that the receiver understands the semantics of the AVP in the context in question. The second one is a syntactic requirement: whether or not the AVP has to occur in the message in question. Two totally different vibes.
{transport_config, term()}{transport_config, term(), Unsigned32() | infinity}โ a term passed as the third argument to thestart/3function of the relevant transport module, so it can start a transport process. Defaults to the empty list.
The 3-tuple form also specifies an interval, in milliseconds, after which a started transport process should be terminated if it hasn't established a connection yet. So like, the following options on a connecting transport ask for a connection to one peer over SCTP, or another (usually the same) peer over TCP:
{transport_module, diameter_sctp}
{transport_config, SctpOpts, 5000}
{transport_module, diameter_tcp}
{transport_config, TcpOpts}
Wanna listen on both SCTP and TCP? Define one transport for each. ๐ซถ
{transport_module, atom()}โ the module implementing a transport process as defined indiameter_transport(3erl). Defaults todiameter_tcp.
Multiple transport_module and transport_config options are allowed. In that case (and only that case) the order of these is significant: a transport_module gets paired with the first transport_config that follows it in the options list, or with the default value for any trailing modules. Transport starts will be attempted with each of the modules in order until one establishes a connection within the corresponding timeout (see below), or until they all fail. ๐ญ
{watchdog_config, [{okay|suspect, non_neg_integer()}]}โ config that alters the behaviour of the watchdog state machine. On keyokay: the non-negative number of answered DWR messages before transitioning fromREOPENtoOKAY. On keysuspect: the number of watchdog timeouts before transitioning fromOKAYtoSUSPECTwhen DWR goes unanswered, or0to never make that transition.
Defaults to [{okay, 3}, {suspect, 1}]. Not specifying a key is equivalent to specifying that key's default value.
Warning: the default value is what RFC 3539 requires, okay? Changing it gives you non-standard behaviour, and honestly it should only be used to simulate misbehaving nodes during testing. No cap.
{watchdog_timer, TwInit}
TwInit = Unsigned32()
| {M,F,A}
The RFC 3539 watchdog timer. An integer value is interpreted as the RFC's TwInit in milliseconds, and a jitter of +/- 2 seconds gets added at each rearming of the timer to compute the RFC's Tw. An MFA is expected to return the RFC's Tw directly, with jitter already applied, which lets the callback do the jitter calculation itself. โจ
An integer value must be at least 6000, as required by RFC 3539. Defaults to 30000.
Unrecognized options are silently ignored, but they come back unmodified from service_info/2 and you can refer to them in the predicate functions passed to remove_transport/2. ๐
transport_ref() = reference(): The reference returned by add_transport/2 that identifies the configuration. ๐
๐ฅ EXPORTS
add_transport(SvcName, {connect|listen, [Opt]}) -> {ok, Ref} | {error, Reason}
Types:
SvcName = service_name()
Opt = transport_opt()
Ref = transport_ref()
Reason = term()
Add transport capability to a service. Slay. ๐
The service will start transport processes as it needs them in order to establish a connection with the peer โ either by connecting to the peer (connect) or by accepting incoming connection requests (listen). A connecting transport establishes transport connections with at most one peer; a listening transport can potentially have many. ๐
The diameter application is the one taking responsibility for exchanging CER/CEA with the peer. Once capabilities exchange completes successfully, the service calls each relevant application module's peer_up/3 callback, and after that the caller can exchange Diameter messages with the peer over the transport. On top of CER/CEA, the service also handles DWR/DWA as required by RFC 3539, plus DPR/DPA. It's giving main character energy honestly. ๐
The returned reference uniquely identifies the transport within the scope of the service. Just know that the function returns before a transport connection has been established โ so don't assume it's alive yet, bestie.
Note: it is not an error to add a transport to a service that hasn't been configured yet โ you can configure its transports first and start the service later. ๐ฆ
call(SvcName, App, Request, [Opt]) -> Answer | ok | {error, Reason}
Types:
SvcName = service_name()
App = application_alias()
Request = diameter_codec:message()
Answer = term()
Opt = call_opt()
Send a Diameter request message. ๐จ
...
- App specifies the Diameter application the request lives in, and the callbacks get routed to that app's callback module โ all described below and over in
diameter_app(3erl). Unless you passed thedetachoption, the call just... waits. It returns when an answer message lands from the peer or when something goes wrong. In the answer case, the return value is literally whatever thehandle_answer/4callback gave you. In the error case, whether the error comes straight from diameter or from ahandle_error/4callback depends on whether the outgoing request actually got encoded and sent to the peer โ all the cases are documented below, don't worry bestie ๐
If there are no suitable peers, or if pick_peer/4 rejects them all by returning false, then {error,no_connection} is what you get. Otherwise, pick_peer/4 is followed by a prepare_request/3 callback, the message gets encoded, and then it's sent. Periodt.
There are a bunch of error cases that can stop an answer from ever reaching a handle_answer/4 callback โ and ngl, you need to know all of them:
- If the initial encode of the outgoing request fails, then the request process fails and
{error,encode}is returned. - If the request encodes and sends fine but the answer times out, then a
handle_error/4callback happens withReason = timeout. - If the request encodes and sends fine but the service in question is stopped before an answer comes back, then a
handle_error/4callback happens withReason = cancel. - If the transport connection to the peer goes down after the request was sent but before an answer was received, then an attempt is made to resend the request to an alternate peer. If no such peer is available, or if the subsequent
pick_peer/4callback rejects the candidates, then ahandle_error/4callback happens withReason = failover. If a peer is selected then aprepare_retransmit/3callback happens, after which everything behaves exactly like it does after an initialprepare_request/3callback. - If an encode error happens during retransmission, then the request process fails and
{error,failure}is returned. - If an application callback made while processing the request fails (
pick_peer,prepare_request,prepare_retransmit,handle_answerorhandle_error) then either{error,encode}or{error,failure}is returned, depending on whether or not there's been an attempt to send the request over the transport.
Heads up bestie: {error,encode} is the only return value that guarantees the request has not been sent over the transport connection. Like, the only one. Remember that ๐
origin_state_id() -> Unsigned32()
Returns a reasonable value to use as Origin-State-Id in outgoing messages. Slay.
The value it gives you is the number of seconds since 19680120T031408Z โ that's the first value that can be encoded as a Diameter Time() โ measured from the moment the diameter application was started. Cute, right? ๐
remove_transport(SvcName, Pred) -> ok | {error, Reason}
Types:
SvcName = service_name()
Pred = Fun | MFA | transport_ref() | list() | true | false
Fun = fun((transport_ref(), connect|listen, list()) ->
boolean())
| fun((transport_ref(), list()) -> boolean())
| fun((list()) -> boolean())
MFA = {atom(), atom(), list()}
Reason = term()
Removes transports you previously added. Bye bestie ๐
Pred decides which transports get removed. An arity-3-valued Pred removes all transports for which Pred(Ref, Type, Opts) returns true, where Type and Opts are exactly what you passed to add_transport/2, and Ref is what it gave back. All the other forms are equivalent to an arity-3 fun like this:
Pred = fun(transport_ref(), list()): fun(Ref, _, Opts) -> Pred(Ref, Opts) end
Pred = fun(list()): fun(_, _, Opts) -> Pred(Opts) end
Pred = transport_ref(): fun(Ref, _, _) -> Pred == Ref end
Pred = list(): fun(_, _, Opts) -> [] == Pred -- Opts end
Pred = true: fun(_, _, _) -> true end
Pred = false: fun(_, _, _) -> false end
Pred = {M,F,A}: fun(Ref, Type, Opts) -> apply(M, F, [Ref, Type, Opts | A]) end
Removing a transport causes the corresponding transport processes to be terminated. Whether or not a DPR message gets sent to a peer is controlled by the value of disconnect_cb configured on that transport โ so yeah, that's a thing to check before you yeet a transport ๐ญ
service_info(SvcName, Info) -> term()
Types:
SvcName = service_name()
Info = Item | [Info]
Item = atom()
Returns info about a started service. Asking for info on a service that doesn't exist gives you undefined. Asking for a list of items gives you a tagged list back. Very demure, very mindful โจ
Item can be any one of these:
'Origin-Host':'Origin-Realm':'Vendor-Id':'Product-Name':'Origin-State-Id':'Host-IP-Address':'Supported-Vendor':'Auth-Application-Id':'Inband-Security-Id':'Acct-Application-Id':'Vendor-Specific-Application-Id':'Firmware-Revision': Return a capability value as configured withstart_service/2.applications: Return the list of applications as configured withstart_service/2.capabilities: Return a tagged list of all capabilities values as configured withstart_service/2.transport: Return a list containing one entry for each of the service's transports, as configured withadd_transport/2. Each entry is a tagged list containing both configuration and information about established peer connections. An example return value for a client service withOrigin-Host"client.example.com"configured with a single transport connected to"server.example.com"might look as follows.
[[{ref,#Ref<0.0.0.93>},
{type,connect},
{options,[{transport_module,diameter_tcp},
{transport_config,[{ip,{127,0,0,1}},
{raddr,{127,0,0,1}},
{rport,3868},
{reuseaddr,true}]}]},
{watchdog,{<0.66.0>,-576460736368485571,okay}},
{peer,{<0.67.0>,-576460736357885808}},
{apps,[{0,common}]},
{caps,[{origin_host,{"client.example.com","server.example.com"}},
{origin_realm,{"example.com","example.com"}},
{host_ip_address,{[{127,0,0,1}],[{127,0,0,1}]}},
{vendor_id,{0,193}},
{product_name,{"Client","Server"}},
{origin_state_id,{[],[]}},
{supported_vendor_id,{[],[]}},
{auth_application_id,{[0],[0]}},
{inband_security_id,{[],[0]}},
{acct_application_id,{[],[]}},
{vendor_specific_application_id,{[],[]}},
{firmware_revision,{[],[]}},
{avp,{[],[]}}]},
{port,[{owner,<0.69.0>},
{module,diameter_tcp},
{socket,{{127,0,0,1},48758}},
{peer,{{127,0,0,1},3868}},
{statistics,[{recv_oct,656},
{recv_cnt,6},
{recv_max,148},
{recv_avg,109},
{recv_dvi,19},
{send_oct,836},
{send_cnt,6},
{send_max,184},
{send_avg,139},
{send_pend,0}]}]},
{statistics,[{{{0,258,0},recv},3},
{{{0,258,1},send},3},
{{{0,258,0},recv,{'Result-Code',2001}},3},
{{{0,257,0},recv},1},
{{{0,257,1},send},1},
{{{0,257,0},recv,{'Result-Code',2001}},1},
{{{0,280,1},recv},2},
{{{0,280,0},send},2},
{{{0,280,0},send,{'Result-Code',2001}},2}]}]]
Here ref is a transport_ref() and options is the corresponding transport_opt() list that was passed to add_transport/2. The watchdog entry shows the state of a connection's RFC 3539 watchdog state machine. The peer entry identifies the diameter_app:peer_ref() for which there will have been peer_up/3 callbacks for the Diameter applications identified by the apps entry โ common being the application_alias(). The caps entry identifies the capabilities sent by the local node and received from the peer during capabilities exchange. The port entry displays socket-level info about the transport connection. The statistics entry presents Diameter-level counters โ an entry like {{{0,280,1},recv},2} says that the client has received 2 DWR messages, since {0,280,1} = {Application_Id, Command_Code, R_Flag}. Iconic ๐
Note that thewatchdog,peer,apps,capsandportentries depend on connectivity with the peer and may not be present. Note also that thestatisticsentry presents values accumulated during the lifetime of the transport configuration. So like, don't panic if something's missing ๐
A listening transport presents its information slightly differently, since there may be multiple accepted connections for the same transport_ref(). The transport info returned by a server with a single client connection might look as follows.
Here's the vibe for the listening case โ the return value looks like this, and yes it's a chonky boy:
[[{ref,#Ref<0.0.0.61>},
{type,listen},
{options,[{transport_module,diameter_tcp},
{transport_config,[{reuseaddr,true},
{ip,{127,0,0,1}},
{port,3868}]}]},
{accept,[[{watchdog,{<0.56.0>,-576460739249514012,okay}},
{peer,{<0.58.0>,-576460638229179167}},
{apps,[{0,common}]},
{caps,[{origin_host,{"server.example.com","client.example.com"}},
{origin_realm,{"example.com","example.com"}},
{host_ip_address,{[{127,0,0,1}],[{127,0,0,1}]}},
{vendor_id,{193,0}},
{product_name,{"Server","Client"}},
{origin_state_id,{[],[]}},
{supported_vendor_id,{[],[]}},
{auth_application_id,{[0],[0]}},
{inband_security_id,{[],[]}},
{acct_application_id,{[],[]}},
{vendor_specific_application_id,{[],[]}},
{firmware_revision,{[],[]}},
{avp,{[],[]}}]},
{port,[{owner,<0.62.0>},
{module,diameter_tcp},
{socket,{{127,0,0,1},3868}},
{peer,{{127,0,0,1},48758}},
{statistics,[{recv_oct,1576},
{recv_cnt,16},
{recv_max,184},
{recv_avg,98},
{recv_dvi,26},
{send_oct,1396},
{send_cnt,16},
{send_max,148},
{send_avg,87},
{send_pend,0}]}]}],
[{watchdog,{<0.72.0>,-576460638229717546,initial}}]]},
{statistics,[{{{0,280,0},recv},7},
{{{0,280,1},send},7},
{{{0,280,0},recv,{'Result-Code',2001}},7},
{{{0,258,1},recv},3},
{{{0,258,0},send},3},
{{{0,258,0},send,{'Result-Code',2001}},3},
{{{0,280,1},recv},5},
{{{0,280,0},send},5},
{{{0,280,0},send,{'Result-Code',2001}},5},
{{{0,257,1},recv},1},
{{{0,257,0},send},1},
{{{0,257,0},send,{'Result-Code',2001}},1}]}]]
So basically all the deets up there are the same as the connect case, the only glow-up is that the client connections are all grouped up under an accept tuple instead. Cute, right? ๐
And lowkey one thing that changes the shape of the whole listing: whether the transport_opt() pool_size has been configured or not. For a connecting transport, a value greater than 1 means multiple transport processes hanging out under the same transport_ref() โ exactly like in the listening case. So the format there is basically the listening format, except you get a pool tuple where the accept tuple would've been. Slay, consistent. โจ
connectionsโ gimme a list with one entry for every established transport connection whose watchdog state machine is not in thedownstate. It's a flat view of transport info: only the active connections show up, and the Diameter-level statistics are only accumulated for the lifetime of that transport connection. For the server up above, a return value might look like this ๐
[[{ref,#Ref<0.0.0.61>},
{type,accept},
{options,[{transport_module,diameter_tcp},
{transport_config,[{reuseaddr,true},
{ip,{127,0,0,1}},
{port,3868}]}]},
{watchdog,{<0.56.0>,-576460739249514012,okay}},
{peer,{<0.58.0>,-576460638229179167}},
{apps,[{0,common}]},
{caps,[{origin_host,{"server.example.com","client.example.com"}},
{origin_realm,{"example.com","example.com"}},
{host_ip_address,{[{127,0,0,1}],[{127,0,0,1}]}},
{vendor_id,{193,0}},
{product_name,{"Server","Client"}},
{origin_state_id,{[],[]}},
{supported_vendor_id,{[],[]}},
{auth_application_id,{[0],[0]}},
{inband_security_id,{[],[]}},
{acct_application_id,{[],[]}},
{vendor_specific_application_id,{[],[]}},
{firmware_revision,{[],[]}},
{avp,{[],[]}}]},
{port,[{owner,<0.62.0>},
{module,diameter_tcp},
{socket,{{127,0,0,1},3868}},
{peer,{{127,0,0,1},48758}},
{statistics,[{recv_oct,10124},
{recv_cnt,132},
{recv_max,184},
{recv_avg,76},
{recv_dvi,9},
{send_oct,10016},
{send_cnt,132},
{send_max,148},
{send_avg,75},
{send_pend,0}]}]},
{statistics,[{{{0,280,0},recv},62},
{{{0,280,1},send},62},
{{{0,280,0},recv,{'Result-Code',2001}},62},
{{{0,258,1},recv},3},
{{{0,258,0},send},3},
{{{0,258,0},send,{'Result-Code',2001}},3},
{{{0,280,1},recv},66},
{{{0,280,0},send},66},
{{{0,280,0},send,{'Result-Code',2001}},66},
{{{0,257,1},recv},1},
{{{0,257,0},send},1},
{{{0,257,0},send,{'Result-Code',2001}},1}]}]]
Heads up bestie: you can totally get multiple entries with the same ref here โ that's the opposite of transport info, so don't @ me when you see duplicates. ๐
statisticsโ returns a{{Counter, Ref}, non_neg_integer()}list of counter values.Refis either atransport_ref()or adiameter_app:peer_ref(). Entries for the latter get folded into the matching entries for the former as peer connections go down. Both kinds are removed atremove_transport/2. And fun fact: the Diameter-level statistics you get back fromtransportandconnectionsinfo? They're built on top of these entries. ๐งdiameter_app:peer_ref()โ returns the transport configuration associated with a single peer, exactly as it was passed toadd_transport/2. If the peer is unknown you get an empty list. Otherwise you get theref,typeandoptionstuples just like intransportandconnectionsinfo above. For example:
[{ref,#Ref<0.0.0.61>},
{type,accept},
{options,[{transport_module,diameter_tcp},
{transport_config,[{reuseaddr,true},
{ip,{127,0,0,1}},
{port,3868}]}]}]
๐ธ services() -> [SvcName]
Types:
SvcName = service_name()
Returns the list of started services. That's it, that's the whole function, she's simple and she ate. โจ
๐ฆ session_id(Ident) -> OctetString()
Types:
Ident = DiameterIdentity()
Returns a value for a Session-Id AVP. ๐ซ
The value has the form required by section 8.8 of RFC 6733. Ident should be the Origin-Host of the peer from which the message containing the returned value will be sent โ so pls don't just shove any old identity in there, okay? ๐
๐ start() -> ok | {error, Reason}
Starts the diameter application. ๐ฅ
The diameter application must be started before you start a service. In a production system this is typically accomplished by a boot file, not by calling start/0 explicitly โ so like, you can call it, but that's not the main character move. ๐
๐ start_service(SvcName, Options) -> ok | {error, Reason}
Types:
SvcName = service_name()Options = [service_opt()]Reason = term()
Starts a diameter service. ๐
A service defines a locally-implemented Diameter node, specifying the capabilities to be advertised during capabilities exchange. Transports get added to a service using add_transport/2.
Note: a transport can both override its service's capabilities and restrict its supported Diameter applications, so "service = Diameter node as identified by Origin-Host" is not necessarily the case. Ik, ik, slightly chaotic โ but that's the tea. ๐
๐ stop() -> ok | {error, Reason}
Stops the diameter application. Short, sweet, devastating. ๐
โญ stop_service(SvcName) -> ok | {error, Reason}
Types:
SvcName = service_name()Reason = term()
Stops a diameter service. ๐ธ
Stopping a service causes all associated transport connections to be broken. A DPR message will be sent, same as in the case of remove_transport/2.
Note: stopping a service does not remove any associated transports โ you literally have to call remove_transport/2 to remove the transport configuration. No cap, the service being gone does not mean the config is gone. ๐ญ
๐ซถ subscribe(SvcName) -> true
Types:
SvcName = service_name()
Subscribe to service_event() messages from a service. ๐
It is not an error to subscribe to events from a service that doesn't exist yet โ she's just being patient. Doing so before adding transports is required to guarantee the reception of all transport-related events, so likeโฆ do it early bestie. ๐
๐ง unsubscribe(SvcName) -> true
Types:
SvcName = service_name()
Unsubscribes you from event messages from a service. Bye bye notifications, we hardly knew ye. ๐
๐ SEE ALSO ๐
diameter_app(3erl), diameter_transport(3erl), diameter_dict(5)
And that's the whole page, bestie โ go forth and Diameter ๐
This lil masterpiece is brought to you by Ericsson AB, diameter 2.2.7, dated diameter(3erl) โ periodt. ๐โจ๐
๐ฆ where this page comes from
- debian bookworm: erlang-manpages 1:25.2.3+dfsg-1+deb12u1 ยท Apache-2.0
- debian bookworm: erlang-manpages 1:25.2.3+dfsg-1+deb12u4 ยท Apache-2.0
- ubuntu noble: erlang-manpages 1:25.3.2.8+dfsg-1ubuntu4 ยท Apache-2.0
- ubuntu noble: erlang-manpages 1:25.3.2.8+dfsg-1ubuntu4.8 ยท Apache-2.0
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms