diameter(3)

๐Ÿ“ฆ
version 29.0.6 ยท 2 versions
, in nixos 26.05, nixpkgs unstable, ubuntu stonking

๐Ÿ’– diameter(3) โ€” Erlang Module Definition ๐Ÿ’–

okay bestie, welcome to the diameter module! It's giving main-character energy for Diameter protocol stuff, and we're gonna walk through every single detail together. ๐Ÿซถ No skips, no cap. โœจ

๐Ÿ’… NAME ๐Ÿ’…

diameter โ€” Main API of the diameter application. periodt. ๐Ÿ‘‘

โœจ DESCRIPTION โœจ

This module? It's literally the interface you use to implement a Diameter node that sends and receives messages using the Diameter protocol as defined in RFC 6733. ๐Ÿ’Œ

Basic usage is a whole vibe: you create a representation of a locally implemented Diameter node and its capabilities with start_service/2, then add transport capability using add_transport/2, and then send Diameter requests and receive Diameter answers with call/4. Incoming Diameter requests are communicated as callbacks to a diameter_app callback modules as specified in the service configuration. ๐ŸŽ€

โš ๏ธ Bestie, 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? That's the Diameter protocol in the sense of RFC 6733. Don't mix them up, iykyk. ๐Ÿ’…

Also, the diameter application must be started before calling most functions in this module. No skipping that step, fr fr. ๐ŸŒŸ

๐Ÿงฌ DATA TYPES ๐Ÿงฌ

Here's the roster of types, bestie:

  • Address()
  • DiameterIdentity()
  • Grouped()
  • OctetString()
  • Time()
  • Unsigned32()

Then we got:

  • UTF8String() โ€” Types corresponding to RFC 6733 AVP Data Formats. Defined in diameter_dict(4). ๐Ÿ“–
  • elapsed_time() โ€” Elapsed time since a given time. โณ
  • application_alias() = term() โ€” Name identifying a Diameter application in service configuration. Passed to call/4 when sending requests defined by the application. ๐Ÿท๏ธ
  • application_module() = Mod | [Mod | ExtraArgs] | #diameter_callback{}
    • Mod = atom()
    • ExtraArgs = list()

Module implementing the callback interface defined in diameter_app, 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 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 the dictionary option. ๐Ÿท๏ธ
    • {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 in diameter_dict(4). ๐Ÿ“š
    • {module, application_module()} โ€” Callback module in which messages of the Diameter application are handled. See diameter_app for the required interface and semantics. ๐Ÿ“ž
    • {state, term()} โ€” Initial callback state. The prevailing state is passed to some diameter_app callbacks, which can then return a new state. Defaults to the value of the alias option. ๐Ÿง 
    • {call_mutates_state, true|false} โ€” Whether or not the pick_peer/4 application callback can modify the application state. Defaults to false.
๐Ÿšจ WARNING: pick_peer/4 callbacks are serialized when this option is true, 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.

If callback then errors result in a handle_answer/4 callback in the same fashion as for handle_request/3, with errors communicated in the errors field of the #diameter_packet{} passed to the callback.

If report then an answer containing errors is discarded without a callback and a warning report is written to the log.

If discard then an answer containing errors is silently discarded without a callback.

In both the report and discard cases the return value for the call/4 invocation in question is as if a callback had taken place and returned {error, failure}.

Defaults to discard. ๐Ÿ’…

  • {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.

If answer_3xxx then requests are answered without a handle_request/3 callback taking place.

If answer then even 5xxx errors are answered without a callback unless the connection in question has configured the RFC 3588 common dictionary as noted below.

If callback then a handle_request/3 callback always takes place and its return value determines the answer sent to the peer, if any.

Defaults to answer_3xxx.

๐Ÿ“ NOTE: Answers sent by diameter set the E-bit in the Diameter Header. Since RFC 3588 allows only 3xxx result codes in an answer-message, answer has the same semantics as answer_3xxx when the transport in question has been configured with diameter_gen_base_rfc3588 as its common dictionary. Since RFC 6733 allows both 3xxx and 5xxx result codes in an answer-message, a transport with diameter_gen_base_rfc6733 as its common dictionary does distinguish between answer_3xxx and answer. ๐Ÿ”
  • 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 the pick_peer/4 callback for the application in question. Multiple options are equivalent a single all filter on the corresponding list of filters. Defaults to none. ๐ŸŽฏ
    • {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 subsequent pick_peer/4 callback. ๐Ÿ‘ฏ
    • {timeout, Unsigned32()} โ€” Number of milliseconds after which the request should timeout. Defaults to 5000. โฑ๏ธ
    • detach โ€” Cause call/4 to return ok as soon as the request in question has been encoded, instead of waiting for and returning the result from a subsequent handle_answer/4 or handle_error/4 callback. ๐Ÿ•Š๏ธ

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-Address need not be specified if the transport module in question communicates an address list as described in diameter_transport. ๐Ÿ“ก
    • {'Vendor-Id', Unsigned32()} ๐Ÿข
    • {'Product-Name', UTF8String()} ๐Ÿ›๏ธ
    • {'Origin-State-Id', Unsigned32()} โ€” Origin-State-Id is optional but, if configured, will be included in outgoing CER/CEA and DWR/DWA messages. Setting a value of 0 (zero) is equivalent to not setting a value, as documented in RFC 6733. The function origin_state_id/0 can be used as to retrieve a value that is computed when the diameter application is started. ๐Ÿ”„
    • {'Supported-Vendor-Id', [Unsigned32()]} ๐Ÿค
    • {'Auth-Application-Id', [Unsigned32()]} ๐Ÿ”
    • {'Inband-Security-Id', [Unsigned32()]} โ€” Inband-Security-Id defaults to the empty list, which is equivalent to a list containing only 0 (NO_INBAND_SECURITY). If 1 (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() = {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 form fun Name/Arity in 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 to start_service/2 or add_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 whose Origin-Host has the same value as Destination-Host in the outgoing request in question, or any peer if the request does not contain a Destination-Host AVP. ๐Ÿ 
    • realm โ€” Matches only those peers whose Origin-Realm has the same value as Destination-Realm in the outgoing request in question, or any peer if the request does not contain a Destination-Realm AVP. ๐ŸŒ
    • {host, any|DiameterIdentity()} โ€” Matches only those peers whose Origin-Host has the specified value, or all peers if the atom any. ๐Ÿ 
    • {realm, any|DiameterIdentity()} โ€” Matches only those peers whose Origin-Realm has the specified value, or all peers if the atom any. ๐ŸŒ
    • {eval, eval()} โ€” Matches only those peers for which the specified eval() returns true when applied to the connection's diameter_caps record. Any other return value or exception is equivalent to false. โœ…
    • {neg, peer_filter()} โ€” Matches only those peers not matched by the specified filter. ๐Ÿšซ
    • {all, [peer_filter()]} โ€” Matches only those peers matched by each filter in the specified list. ๐Ÿงฉ
  • {any, [peer_filter()]} โ€” only the peers that match at least one filter in your list get through</p>. And the resulting list keeps match order, so the peers that matched the first filter sort before the ones matched by the second, and so on down the line. ๐Ÿ’–
  • {first, [peer_filter()]} โ€” same energy as any, except it stops dead at the first filter that actually has matches. Which is lowkey a huge glow-up in efficiency when you've got a whole crowd of peers. For example, this filter below means only the peers that best match both the host and realm filters get presented, periodt:
{first, [{all, [host, realm]}, realm]}

An invalid filter is equivalent to {any,[]}, which is a filter that matches no peer at all. ๐Ÿซฅ

NOTE: the host and realm filters make the Destination-Host and Destination-Realm AVPs get extracted from the outgoing request. It assumes that request is a record- or list-valued diameter_codec:message(), and it assumes at most one of each AVP. If that's not your situation, bestie, you must use the {host|realm, DiameterIdentity()} filters to get the result you want. An empty DiameterIdentity() (which shouldn't be typical, ngl) matches all hosts/realms as far as filtering is concerned.
WARNING: a host filter is typically not what you want when setting Destination-Host, because it will remove peer agents from the candidates list. ๐Ÿ˜ญ

๐Ÿงš service_event()

service_event() = #diameter_event{service = service_name(), info =
service_event_info()}

An event message, sent to the processes that subscribed to these with subscribe/1. ๐Ÿ’Œ

๐ŸŽ€ service_event_info()

service_event_info() = term()

The info field of a service_event() record. It can be 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's been a capabilities exchange on a newly established transport connection and the record contains the received CER or CEA. โœจ

Note that a single up or down event for a given peer corresponds to multiple peer_up/3 or peer_down/3 callbacks โ€” one for each of the Diameter applications negotiated during capabilities exchange. Like, the event communicates connectivity with the peer as a whole, while the callbacks communicate connectivity with respect to individual Diameter applications. Different vibes, same peer. ๐Ÿ’…

  • {reconnect, Ref, Opts}
Ref  = transport_ref()
Opts = [transport_opt()]

A connecting transport is trying to establish/reestablish a transport connection with a peer following connect_timer or watchdog_timer expiry. ๐Ÿ”

  • {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 โ€” local node and remote peer respectively. Pkt contains the CER in question. If a capabilities callback did the rejecting, 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. Here Caps contains values for the local node only. Pkt contains the CER in question.

  • {'CER', timeout} An expected CER was not received within capx_timeout of connection establishment. โฐ
  • {'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. If Result is an integer, that's the result code the peer sent. Caps contains pairs of values for the local node and remote peer. Pkt contains the CEA in question. If a capabilities callback did the rejecting, 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. glow up or glow down, either way it's a state change ๐Ÿ•

  • any/0 For forward compatibility, a subscriber should be prepared to receive info fields of forms other than the above. Bestie, stay flexible. ๐Ÿฆ‹

๐ŸŒธ service_name()

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. For an incoming request, the application identifier in the message header determines the application, and that identifier is 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 corresponding capability() configuration, of *-Application-Id AVPs especially. ๐Ÿšจ
  • {decode_format, record | list | map | none} The format of decoded messages and grouped AVPs in the msg field of diameter_packet records and the value field of diameter_avp records respectively.
    • record โ€” a record whose definition is generated from the dictionary file in question.
    • list or map โ€” a [Name | Avps] pair, where Avps is a list of AVP name/values pairs or a map keyed on AVP names, respectively.
    • none โ€” the atom-value message name, or undefined for a Grouped AVP.

See also diameter_codec:message().

Defaults to record. ๐Ÿ—‚๏ธ

NOTE: AVPs are decoded into a list of diameter_avp records in the avps field of diameter_packet records independently of decode_format. So don't think that setting it to something else makes those go away, bestie.
  • {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. The types false, node, nodes and eval() are equivalent to [], [node()], [node()|nodes()] and the evaluated value respectively โ€” and evaluation of each expression takes 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.

Defaults to nodes.

  • {sequence, {H,N} | eval()} A constant value H for the topmost 32-N bits 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 at start_service/2. In particular, an identifier Id is 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 plus 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. โœจ

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 be the node that duplicates everybody else, it's giving chaotic. ๐Ÿ’€
  • {share_peers, boolean() | [node()] | eval()} Nodes to which peer connections established on the local Erlang node are communicated. Shared peers become available in the remote candidates list passed to pick_peer/4 callbacks on remote nodes whose services are configured to use them: see use_shared_peers below. ๐Ÿซถ

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/0 in a list is ignored, so a collection of services can all be configured to share with the same list of nodes. ๐ŸŒˆ

Defaults to false.

  • {strict_arities, boolean() | encode | decode} โ€” do we make the number of AVPs in a message (or a grouped AVP) actually match what the dictionary says it should be, when we hand messages to the diameter_app callbacks? That's the vibe check. If you set it to true, then a mismatch in an outgoing message makes the encoding straight-up fail, and a mismatch in an incoming message gets reported as 5005/5009 errors in the errors field of the diameter_packet record that shows up at your handle_request/3 or handle_answer/4 callbacks. If you set it to false, neither of those gets enforced or detected. And if you set it to encode or decode, the errors are only enforced/detected on outgoing or incoming messages respectively. ๐Ÿ’…

Defaults to true.

NOTE: turning arity checks off changes the form of messages at encode/decode. Specifically, decoded AVPs come to you as lists of values no matter what the AVP's arity is (arity = the expected number of it in the grammar for that message/AVP), and at encode you're expected to supply values as lists too. This is not how decode historically behaved, where AVPs of arity 1 were represented as bare values โ€” no list wrapping. ๐Ÿซ 
  • {string_decode, boolean()} โ€” are we decoding AVPs of type OctetString() and all its derived besties: DiameterIdentity(), DiameterURI(), IPFilterRule(), QoSFilterRule(), and UTF8String()? If true, AVPs of these types decode to string(). If false, the values stay as binary(). โœจ

Defaults to true.

WARNING: bestie, set this to false. A sufficiently malicious peer can otherwise make you burn so much memory when decoded Diameter messages get passed between processes. The default is the way it is for backwards compatibility only. ๐Ÿ˜ญ
  • {traffic_counters, boolean()} โ€” do we count application-specific messages, i.e. the ones where diameter_app callbacks actually happen? If false, then only the messages handled by diameter itself get counted: CER/CEA, DWR/DWA, DPR/DPA. ๐ŸŒธ

Defaults to true.

NOTE: turning counters off is a performance glow-up, but it means the omitted counters are not returned by service_info/2.
  • {use_shared_peers, boolean() | [node()] | eval()} โ€” which nodes' communicated peers get made available in the remote candidates list of your pick_peer/4 callbacks. If false, remote peers aren't used at all. If [node()], only peers from that specified list of nodes are used. If eval(), only the peers returned by the specified function are used, and that function gets evaluated whenever a remote service communicates info about an available peer connection. The value true is equivalent to fun erlang:nodes/0. And the value node/0 in a list is ignored. ๐Ÿ™ˆ

Defaults to false.

NOTE: a service that doesn't 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 use the service_opt() restrict_connections and keep a dedicated connection on each node you send requests from. ๐Ÿ’€
  • transport_opt() โ€” any transport option except applications, capabilities, transport_config, and transport_module. These act as defaults for transport configuration, and values passed to add_transport/2 override values configured on the service. ๐Ÿ‘‘

โœจ transport_opt() โœจ

Option passed to add_transport/2. It has one of the following types, bestie:

  • {applications, [application_alias()]} โ€” which Diameter applications this transport should be restricted to. Defaults to every application configured on the service in question. Applications not configured on the service in question are just ignored. โœจ
WARNING: the capabilities a node advertises have to match its configured applications. In particular, setting applications on a transport typically means you also have to set matching *-Application-Id AVPs in a capabilities() tuple. ๐Ÿซถ
  • {avp_dictionaries, [module()]} โ€” a list of alternate dictionary modules to encode/decode AVPs that aren't defined by the dictionary of the application in question. At decode, such AVPs are represented as diameter_avp records in the 'AVP' field of a decoded message or Grouped AVP, and the first alternate that succeeds at decoding the AVP sets the record's value field. At encode, values in an 'AVP' list can be passed as AVP name/value 2-tuples, and it's an encode error if no alternate defines the AVP of such a tuple. ๐Ÿ’…

Defaults to the empty list.

NOTE: the motivation for alternate dictionaries is RFC 7683, Diameter Overload Indication Conveyance (DOIC), which defines AVPs meant to be piggybacked onto existing application messages instead of defining an application of its own. The DOIC dictionary ships with 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 outgoing CER/CEA messages. 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 by diameter_tcp. ๐ŸŽ€
  • {capabilities_cb, eval()} โ€” a callback invoked when CER/CEA is received during capabilities exchange, so it can be asked whether the connection should be accepted or not. It gets applied to the transport_ref() and the #diameter_caps{} record of the connection. ๐Ÿ‘‘

The return value can have one of the following types.

  • ok โ€” accept the connection. ๐Ÿซถ
  • integer/0 โ€” causes an incoming CER to be answered with the specified Result-Code.
  • discard โ€” causes an incoming CER to be discarded without CEA being sent.
  • unknown โ€” equivalent to returning 3010, DIAMETER_UNKNOWN_PEER.

Returning anything but ok or a 2xxx series result code causes the transport connection to be broken. You can specify multiple capabilities_cb options, in which case the corresponding callbacks are applied until either all of them return ok or one of them doesn't. ๐Ÿ˜ญ

  • {capx_timeout, Unsigned32()} โ€” number of milliseconds after which a transport process with an established transport connection will be terminated if the expected capabilities exchange message (CER or CEA) hasn't been received from the peer. For a connecting transport, the timing of connection attempts is governed by connect_timer or watchdog_timer expiry. For a listening transport, the peer determines the timing. โฐ

Defaults to 10000.

  • {connect_timer, Tc}
Tc = Unsigned32()

For a connecting transport, this is the RFC 6733 Tc timer, in milliseconds. This timer determines how often a transport tries to establish an initial connection with its peer following transport configuration. Once an initial connection has been established, watchdog_timer determines 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 counts as an initial connection rather than a reestablishment, which makes the RFC 3539 state machine pass to state OKAY rather than REOPEN. Heads up though, these semantics are not governed by the RFC, and a listening transport's connect_timer should be greater than its peer's Tw plus jitter.

Defaults to 30000 for a connecting transport and 60000 for a listening transport. โœจ

  • {disconnect_cb, eval()} โ€” a callback invoked before terminating the transport process of a transport connection having watchdog state OKAY. Applied to application|service|transport and the transport_ref() and diameter_app:peer() in question: application means the diameter application is being stopped, service means the service in question is being stopped by stop_service/1, and transport means the transport in question is being removed by remove_transport/2. ๐Ÿฆ‹

The return value can have one of the following types.

  • {dpr, [option()]} โ€” send Disconnect-Peer-Request to the peer, and the transport process is terminated once Disconnect-Peer-Answer is received or on timeout. An option() can be one of the following.
    • {cause, 0|rebooting|1|busy|2|goaway} โ€” the Disconnect-Cause to send: REBOOTING, BUSY and DO_NOT_WANT_TO_TALK_TO_YOU respectively. Defaults to rebooting for Reason=service|application and goaway for Reason=transport.
    • {timeout, Unsigned32()} โ€” number of milliseconds after which the transport process is terminated if DPA hasn't been received. Defaults to the value of dpa_timeout.
  • dpr โ€” equivalent to {dpr, []}.
  • close โ€” terminate the transport process without Disconnect-Peer-Request being sent to the peer. ๐Ÿ’€
  • ignore โ€” equivalent to not having configured the callback at all.

You can specify multiple disconnect_cb options, in which case the corresponding callbacks are applied until one of them returns a value other than ignore. All callbacks returning ignore is equivalent to not having configured them. ๐Ÿ™ˆ

Defaults to a single callback returning dpr.

  • {dpa_timeout, Unsigned32()} โ€” number of milliseconds after which a transport connection is terminated following an outgoing DPR if DPA isn't received.

Defaults to 1000. โณ

  • {dpr_timeout, Unsigned32()} โ€” number of milliseconds after which a transport connection is terminated following an incoming DPR if the peer does not close the connection.

Defaults to 5000. โณ

  • {incoming_maxlen, 0..16777215} โ€” a bound on the expected size of incoming Diameter messages. Messages bigger than the specified number of bytes get discarded. ๐Ÿšซ

Defaults to 16777215, the maximum value of the 24-bit Message Length field in a Diameter Header. โœจ

  • {length_errors, exit|handle|discard} โ€” bestie, what do we DO when the Message Length field in an incoming Diameter Header is giving wrong? ๐Ÿ˜ญ An error here = the length is not at least 20 bytes (that's the length of a Header btw), OR it's not a multiple of 4 (which is what a valid length looks like), OR it's just not the actual length of the message in question as it came in over the transport interface documented in diameter_transport.

So your three choices:

  • exit โ†’ the transport process in question literally exits ๐Ÿ’€
  • handle โ†’ the message gets processed like normal, and if a handle_request/3 or handle_answer/4 callback happens to fire, it'll flag the 5015 error (DIAMETER_INVALID_MESSAGE_LENGTH)
  • discard โ†’ the message in question is silently yeeted, no note, no nothing ๐Ÿ™ˆ

Defaults to exit.

NOTE: the default is like that on purpose, because a transport module for a stream-oriented transport like TCP might not be able to recover from a message length error at all. Why? 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 no reliable way to rediscover message boundaries, which may result in the failure of subsequent messages. See diameter_tcp for the behaviour of that module. ๐Ÿซถ
  • {pool_size, pos_integer()} โ€” how many transport processes to start. For a listening transport, this sets the size of the pool of accepting transport processes โ€” and a bigger number is lowkey desirable when you're processing multiple concurrent peer connection attempts. For a connecting transport, it's the number of connections to the peer in question that will be attempted to be established. Heads up though: service_opt(): restrict_connections should also be configured on the service in question if you want to allow multiple connections to the same peer. ๐Ÿ‘‘
  • {spawn_opt, [term()] | {M,F,A}} โ€” an options list passed to erlang:spawn_opt/2 to spawn a handler process for an incoming Diameter request on the local node, or an MFA that returns the pid of a handler process. ๐Ÿฃ

The monitor and link options are ignored in the list-valued case, jsyk. An MFA is applied with an additional term prepended to its argument list, and it should return either the pid of the handler process that invokes diameter_traffic:request/1 on the argument in order to process the request, or the atom discard. The handler process need not be local, and diameter need not be started on the remote node โ€” but diameter and relevant application callbacks must be on the code path. No cap. ๐Ÿ’…

Defaults to the empty list.

  • {strict_capx, boolean()]} โ€” whether or not to enforce the RFC 6733 requirement that any message before capabilities exchange should close the peer connection. If false then unexpected messages are just discarded. ๐Ÿšฎ

Defaults to true. Changing this = non-standard behaviour, but it can be useful in case peers are known to behave badly. ๐Ÿ™ƒ

  • {strict_mbit, boolean()} โ€” whether or not to regard an AVP setting the M-bit as erroneous when the command grammar in question does not explicitly allow the AVP. If true then such AVPs are regarded as 5001 errors, DIAMETER_AVP_UNSUPPORTED. If false then the M-bit is ignored and policing it becomes the receiver's responsibility. โœจ

Defaults to true.

WARNING: RFC 6733 is unclear about the semantics of the M-bit. 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 states that AVPs setting the M-bit cannot be added to an existing command: the modified command must instead be placed in a new Diameter application. The reason for the latter is presumably interoperability: allowing arbitrary AVPs setting the M-bit in a command makes its interpretation implementation-dependent, since there's no guarantee that all implementations will understand the same set of arbitrary AVPs in the context of a given command. However, interpreting AVP in a command grammar as any AVP, regardless of M-bit, renders 1.3.4 meaningless, since the receiver can simply ignore any AVP it thinks isn't relevant, regardless of the sender's intent. Beware of confusing mandatory in the sense of the M-bit with mandatory in the sense of the command grammar. The former is a semantic requirement: that the receiver understand the semantics of the AVP in the context in question. The latter is a syntactic requirement: whether or not the AVP must occur in the message in question. ๐Ÿ˜ตโ€๐Ÿ’ซ
  • {transport_config, term()}
  • {transport_config, term(), Unsigned32() | infinity} โ€” a term passed as the third argument to the start/3 function of the relevant transport module in order to start a transport process. Defaults to the empty list. ๐ŸŒธ

The 3-tuple form additionally specifies an interval, in milliseconds, after which a started transport process should be terminated if it has not yet established a connection. For example, the following options on a connecting transport request a connection with one peer over SCTP or another (typically the same) over TCP.

{transport_module, diameter_sctp}
{transport_config, SctpOpts, 5000}
{transport_module, diameter_tcp}
{transport_config, TcpOpts}

To listen on both SCTP and TCP, define one transport for each. ๐Ÿฆ‹

  • {transport_module, atom()} โ€” module implementing a transport process as defined in diameter_transport. Defaults to diameter_tcp. ๐ŸŒˆ

Multiple transport_module and transport_config options are allowed. The order of these is significant in this case (and only in this case), a transport_module being paired with the first transport_config following it in the options list, or the default value for 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 all fail. ๐Ÿ”ฅ

  • {watchdog_config, [{okay|suspect, non_neg_integer()}]} โ€” configuration that alters the behaviour of the watchdog state machine. On key okay, the non-negative number of answered DWR messages before transitioning from REOPEN to OKAY. On key suspect, the number of watchdog timeouts before transitioning from OKAY to SUSPECT when DWR is unanswered, or 0 to not make the transition. โฐ

Defaults to [{okay, 3}, {suspect, 1}]. Not specifying a key is equivalent to specifying the default value for that key. ๐Ÿ’œ

WARNING: The default value is as required by RFC 3539: changing it results in non-standard behaviour that should only be used to simulate misbehaving nodes during test.
  • {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, a jitter of ยฑ 2 seconds being 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 applied, allowing the jitter calculation to be performed by the callback. ๐ŸŽ€

An integer value must be at least 6000 as required by RFC 3539. Defaults to 30000.

Unrecognized options are silently ignored but are returned unmodified by service_info/2 and can be referred to in predicate functions passed to remove_transport/2. ๐Ÿ™ˆ


transport_ref() = reference()

Reference returned by add_transport/2 that identifies the configuration. โญ


๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

diameter_app, diameter_transport, diameter_dict(4) ๐Ÿซถ๐Ÿ’–

โœจ DATA TYPES โœจ

okay bestie, before we get into the functions, here's the type tea โ˜•. These are the exact Erlang type definitions, so don't @ me if they look a little formal. They're the blueprint for everything. Keep 'em close. ๐Ÿ’…

'Address'() :: inet:ip_address() | string().

'DiameterIdentity'() :: 'OctetString'().

'DiameterURI'() :: 'OctetString'().

'Enumerated'() :: 'Integer32'().

'Float32'() :: '-infinity' | float() | infinity.

'Float64'() :: '-infinity' | float() | infinity.

'Grouped'() :: list() | tuple().

'IPFilterRule'() :: 'OctetString'().

'Integer32'() :: -2147483647..2147483647.

'Integer64'() :: -9223372036854775807..9223372036854775807.

'OctetString'() :: iolist().

'QoSFilterRule'() :: 'OctetString'().

'Time'() :: {{integer(), 1..12, 1..31}, {0..23, 0..59, 0..59}}.

'UTF8String'() :: iolist().

'Unsigned32'() :: 0..4294967295.

'Unsigned64'() :: 0..18446744073709551615.

app_alias() :: any().

app_module() ::
          module() |
          maybe_improper_list(module(), list()) |
          #diameter_callback{}.

application_opt() ::
          {alias, app_alias()} |
          {answer_errors, callback | report | discard} |
          {call_mutates_state, boolean()} |
          {dictionary, module()} |
          {module, app_module()} |
          {request_errors, answer_3xxx | answer | callback} |
          {state, any()}.

call_opt() ::
          detach |
          {extra, list()} |
          {filter, peer_filter()} |
          {peer, peer_ref()} |
          {timeout, 'Unsigned32'()}.

capability() ::
          {'Origin-Host', 'DiameterIdentity'()} |
          {'Origin-Realm', 'DiameterIdentity'()} |
          {'Host-IP-Address', ['Address'()]} |
          {'Vendor-Id', 'Unsigned32'()} |
          {'Product-Name', 'UTF8String'()} |
          {'Supported-Vendor-Id', ['Unsigned32'()]} |
          {'Auth-Application-Id', ['Unsigned32'()]} |
          {'Vendor-Specific-Application-Id', ['Grouped'()]} |
          {'Firmware-Revision', 'Unsigned32'()}.

common_opt() ::
          {avp_dictionaries, [module()]} |
          {capabilities_cb, eval()} |
          {capx_timeout, 'Unsigned32'()} |
          {connect_timer, 'Unsigned32'()} |
          {disconnect_cb, eval()} |
          {dpa_timeout, 'Unsigned32'()} |
          {dpr_timeout, 'Unsigned32'()} |
          {incoming_maxlen, message_length()} |
          {length_errors, exit | handle | discard} |
          {pool_size, pos_integer()} |
          {spawn_opt, list() | mfa()} |
          {strict_capx, boolean()} |
          {strict_mbit, boolean()} |
          {watchdog_config, [{okay | suspect, non_neg_integer()}]} |
          {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}}.

decode_format() :: record | list | map | none | record_from_map.

elapsed_time() ::
          {Hours :: non_neg_integer(),
           Mins :: 0..59,
           Secs :: 0..59,
           MicroSecs :: 0..999999}.

eval() ::
          {module(), atom(), list()} |
          fun() |
          maybe_improper_list(eval(), list()).

evaluable() :: eval().

message_length() :: 0..16777215.

peer_filter() ::
          none | host | realm |
          {host, any | 'DiameterIdentity'()} |
          {realm, any | 'DiameterIdentity'()} |
          {eval, eval()} |
          {neg, peer_filter()} |
          {first, [peer_filter()]} |
          {all, [peer_filter()]} |
          {any, [peer_filter()]}.

-opaque peer_ref()

remotes() :: boolean() | [node()] | eval().

restriction() :: false | node | nodes | [node()] | eval().

sequence() :: {'Unsigned32'(), 0..32}.

service_name() :: any().

service_opt() ::
          capability() |
          {application, [application_opt()]} |
          {decode_format, decode_format()} |
          {restrict_connections, restriction()} |
          {sequence, sequence() | eval()} |
          {share_peers, remotes()} |
          {strict_arities, true | strict_arities()} |
          {string_decode, boolean()} |
          {traffic_counters, boolean()} |
          {use_shared_peers, remotes()} |
          {bins_info, boolean() | non_neg_integer()} |
          common_opt().

strict_arities() :: false | encode | decode.

transport_opt() ::
          {applications, [app_alias()]} |
          {capabilities, [capability()]} |
          {transport_config, any()} |
          {transport_config, any(), 'Unsigned32'() | infinity} |
          {transport_module, atom()} |
          common_opt() |
          {private, any()}.

transport_pred() ::
          fun((transport_ref(), connect | listen, list()) -> boolean())
|
          fun((transport_ref(), list()) -> boolean()) |
          fun((list()) -> boolean()) |
          transport_ref() |
          boolean() |
          list() |
          {connect | listen, transport_pred()} |
          {atom(), atom(), list()}.

transport_ref() :: reference().

That's a whole lot of types, but they're lowkey iconic. โœจ


โœจ FUNCTIONS โœจ

add_transport/2 โ€” let's get connected ๐Ÿ’…

add_transport(SvcName, Transport) -> {ok, TRef} | {error, Reason}
                       when
                           SvcName :: service_name(),
                           Transport :: {T, Opts},
                           T :: listen | connect,
                           Opts :: [transport_opt()],
                           TRef :: transport_ref(),
                           Reason :: term().

okay so this is how you add a transport to a service. The service will spin up transport procs as needed to connect to the peer โ€” either by reaching out (connect) or by accepting incoming requests (listen). A connecting transport links up with at most one peer, while a listening transport can handle many. ๐Ÿ’…

The diameter app handles CER/CEA exchange with the peer. Once capabilities exchange succeeds, the service calls each app module's peer_up/3 callback, and after that you can send Diameter messages to the peer over the transport. Besides CER/CEA, the service also takes care of DWR/DWA as required by RFC 3539, and DPR/DPA. ๐Ÿ”ฅ

The returned reference is your unique ticket for that transport within the service. Heads up: the function returns before any transport connection is actually established. So don't expect instant connection, bestie. ๐Ÿ˜ญ

NOTE: It is not an error to add a transport to a service that has not yet been configured: a service can be started after configuring its transports.

call/4 โ€” slide into their DMs with a request ๐Ÿ’Œ

call(SvcName, App, Request, CallOpts) -> Result
              when
                  SvcName :: service_name(),
                  App :: app_alias(),
                  Request ::
                      diameter_codec:message() |
diameter_codec:packet(),
                  CallOpts :: [call_opt()],
                  Result :: ok | {error, Reason} | Answer,
                  Answer :: term(),
                  Reason :: term().

okay so you wanna send a Diameter request? Say less. ๐Ÿ’Œ

App tells you which Diameter application the request is defined in, and callbacks go to the corresponding callback module as described below and in diameter_app. Unless you pass the detach option, the call returns either when an answer message comes back from the peer or when an error happens. If you get an answer, the return value is whatever a handle_answer/4 callback returns. If it's an error, whether that error comes straight from diameter or from a handle_error/4 callback depends on if the outgoing request was successfully encoded for transmission to the peer โ€” the cases are below, bestie.

If there are no suitable peers, or if pick_peer/4 rejects them by returning false, then you get {error,no_connection}. Otherwise pick_peer/4 is followed by a prepare_request/3 callback, the message gets encoded, and then it's sent. ๐Ÿš€

There are several error cases that can stop an answer from being received and passed to a handle_answer/4 callback:

  • If the initial encode of the outgoing request fails, then the request process fails and {error,encode} is returned.
  • If the request is successfully encoded and sent but the answer times out then a handle_error/4 callback takes place with Reason = timeout.
  • If the request is successfully encoded and sent but the service in question is stopped before an answer is received then a handle_error/4 callback takes place with Reason = cancel.
  • If the transport connection with the peer goes down after the request has been sent but before an answer has been 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/4 callback rejects the candidates, then a handle_error/4 callback takes place with Reason = failover. If a peer is selected then a prepare_retransmit/3 callback takes place, after which the semantics are the same as following an initial prepare_request/3 callback.
  • If an encode error takes place during retransmission then the request process fails and {error,failure} is returned.
  • If an application callback made in processing the request fails (pick_peer, prepare_request, prepare_retransmit, handle_answer or handle_error) then either {error,encode} or {error,failure} is returned depending on whether or not there has been an attempt to send the request over the transport.
Note that {error,encode} is the only return value which guarantees that the request has not been sent over the transport connection.

origin_state_id/0 โ€” time stamp flex โฐ

origin_state_id() -> 'Unsigned32'().

This one returns a reasonable value to use as Origin-State-Id in outgoing messages. ๐Ÿ’…

The value is literally the number of seconds since 19680120T031408Z โ€” that's the first value that can be encoded as a Diameter Time() โ€” counted from when the diameter application was started. Time is fake but this timestamp is real. โฐ

remove_transport/2 โ€” bye bye transport ๐Ÿ‘‹

remove_transport(SvcName, Pred) -> ok | {error, Reason}
                          when
                              SvcName :: service_name(),
                              Pred :: transport_pred(),
                              Reason :: term().

Remove previously added transports. It's giving cleanup era. ๐Ÿงน

Okay so Pred is the whole vibe-check of "which transports are we yeeting?" ๐Ÿ‘‹๐Ÿ’… An arity-3 Pred removes every transport where Pred(Ref, Type, Opts) comes back true, where Type and Opts are literally the ones you passed to add_transport/2 and Ref is the one it handed back to you. Every other form of Pred is just a glow-up of 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 means the transport processes that go with it get terminated, periodt. ๐Ÿ’€ Whether or not a DPR message gets shipped off to the peer is fully controlled by whatever disconnect_cb you configured on that transport โ€” so yes, that setting is the main character here. ๐Ÿ‘‘


service_info(SvcName, Item | [Item]) -> term()

when
    SvcName :: service_name(),
    Item :: atom() | peer_ref().

Gives you the tea โ˜• on a started service. Ask for info about a service that doesn't exist and you just get undefined back โ€” no drama, no crash, just nothing. ๐Ÿซฅ Ask for a list of items instead of one, and you get a tagged list back.

Item can be any one of these, bestie:

  • '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'

All of these ones just hand you back a capability value as configured with start_service/2. โœจ

  • applications โ€” Returns the list of applications as configured with start_service/2.
  • capabilities โ€” Returns a tagged list of all the capabilities values as configured with start_service/2.
  • transport โ€” Returns a list with one entry for each of the service's transport as configured with add_transport/2. Every entry is a tagged list holding both the config and the info about peer connections that actually got established. A client service with Origin-Host "client.example.com" configured with a single transport connected to "server.example.com" might spit out something like this:
[[{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}]]]

Okay so lemme break down every single one of those keys, because there's a lot going on and it all eats. ๐Ÿ”ฅ

  • ref is a transport_ref(), and options is the matching transport_opt() list you passed into 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() โ€” the one for which there will have been peer_up/3 callbacks for the Diameter applications named in the apps entry, with common being the application_alias().
  • The caps entry identifies the capabilities the local node sent and the ones it received from the peer during capabilities exchange.
  • The port entry is the socket-level tea about the transport connection. ๐Ÿต
  • The statistics entry is Diameter-level counters. An entry like {{{0,280,1},recv},2} is saying the client has received 2 DWR messages, because {0,280,1} = {Application_Id, Command_Code, R_Flag}. Math queen. ๐Ÿงฎโœจ
Heads up bestie: the watchdog, peer, apps, caps and port entries all depend on whether you've actually got connectivity with the peer, so they might just not be there. Also, the statistics entry shows values accumulated over the lifetime of the transport configuration โ€” not a fresh count, an eternal one. โณ

A listening transport shows its info slightly differently, because there can be multiple accepted connections hanging off the same transport_ref(). The transport info a server returns with a single client connection might look like:

[[{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}]]]

Everything in there is exactly like the connect case, fr fr โ€” the only plot twist is that the client connections get grouped up under an accept tuple. ๐Ÿซถ

And one last thing that changes the shape of your listing: whether or not the transport_opt() pool_size has been configured. If that value is greater than 1, that implies multiple transport processes for the same transport_ref(), just like in the listening case. ๐Ÿ™ˆ The format there is similar to the listening case too โ€” it just has a pool tuple where the accept tuple would be. Swap and slay. ๐Ÿ’…

connections ๐Ÿ’–

okay bestie, so connections gives you a list with one entry for every established transport connection whose watchdog state machine is not in the down state. It's a flat view of transport info that lists only active connections, and Diameter-level statistics are accumulated only for the lifetime of the transport connection. A return value for the server above 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}]}]]
๐Ÿ‘€ lowkey important: there may be multiple entries with the same ref, in contrast to transport info.

statistics ๐Ÿ“Š

Return a {Ref, [{Counter, non_neg_integer()}]} list of references and corresponding counter values. Ref can be either a transport_ref() or a diameter_app:peer_ref(). Entries for the latter are folded into corresponding entries for the former as peer connections go down. Entries for both are removed at remove_transport/2. The Diameter-level statistics returned by transport and connections info are based upon these entries.

diameter_app:peer_ref() ๐Ÿงš

Return transport configuration associated with a single peer, as passed to add_transport/2. The returned list is empty if the peer is unknown. Otherwise it contains the ref, type and options tuples as in transport and connections info 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() โœจ

       services() -> [SvcName] when SvcName :: service_name().

Return the list of started services.

session_id(Ident) ๐ŸŒธ

       session_id(Ident) -> SessionId
                           when
                               Ident :: 'DiameterIdentity'(),
                               SessionId :: 'OctetString'().

Return 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.

start() ๐Ÿ”ฅ

       start() -> ok | {error, Reason} when Reason :: term().

Start the diameter application.

The diameter application must be started before starting a service. In a production system this is typically accomplished by a boot file, not by calling start/0 explicitly.

start_service(SvcName, Opts) ๐Ÿ‘‘

       start_service(SvcName, Opts) -> ok | {error, Reason}
                              when
                                  SvcName :: service_name(),
                                  Opts :: [service_opt()],
                                  Reason :: term().

Start a diameter service.

A service defines a locally-implemented Diameter node, specifying the capabilities to be advertised during capabilities exchange. Transports are 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.

stop() ๐Ÿ›‘

       stop() -> ok | {error, Reason} when Reason :: term().

Stop the diameter application.

stop_service(SvcName) ๐Ÿ’…

       stop_service(SvcName) -> ok | {error, Reason}
                             when SvcName :: service_name(), Reason :: term().

Stop a diameter service.

Stopping a service causes all associated transport connections to be broken. A DPR message will be sent as in the case of remove_transport/2.

NOTE: Stopping a service does not remove any associated transports: remove_transport/2 must be called to remove transport configuration.

subscribe(SvcName) ๐Ÿซถ

       subscribe(SvcName) -> true when SvcName :: service_name().

Subscribe to service_event() messages from a service.

It is not an error to subscribe to events from a service that does not yet exist. Doing so before adding transports is required to guarantee the reception of all transport-related events.

unsubscribe(SvcName) ๐Ÿ™ˆ

       unsubscribe(SvcName) -> true when SvcName :: service_name().

Unsubscribe to event messages from a service.

which_connections() ๐ŸŒˆ

       which_connections() ->
                                  [{SvcName,
                                    [#{peer := PeerInfo,
                                       wd := WDInfo,
                                       peername :=
                                           {inet:ip_address(),
                                            inet:port_number()},
                                       sockname :=
                                           {inet:ip_address(),
                                            inet:port_number()}}]}]
                                  when
                                      SvcName :: string(),
                                      PeerInfo ::
                                          #{pid := pid(),
                                            uptime := elapsed_time()},
                                      WDInfo ::
                                          #{ref := reference(),
                                            type := atom(),
                                            pid := pid(),
                                            state :=
                                                diameter_service:wd_state(),
                                            uptime := elapsed_time()}.

Return a list of all connections, grouped by the service they are associated with.

which_connections(SvcName) ๐Ÿ’œ

       which_connections(SvcName) ->
                                  [#{peer := PeerInfo,
                                     wd := WDInfo,
                                     peername :=
                                         {inet:ip_address(),
                                          inet:port_number()},
                                     sockname :=
                                         {inet:ip_address(),
                                          inet:port_number()}}]
                                  when
                                      SvcName :: string(),
                                      PeerInfo ::
                                          #{pid := pid(),
                                            uptime := elapsed_time()},
                                      WDInfo ::
                                          #{ref := reference(),
                                            type := atom(),
                                            pid := pid(),
                                            state :=
                                                diameter_service:wd_state(),
                                            uptime := elapsed_time()}.

Return a list of connections associated with the service 'SvcName'.

which_transports() โญ

       which_transports() ->
                                 [#{ref := reference(),
                                    type := atom(),
                                    service := string()}].

Return a list of all transports.

which_transports(SvcName) ๐ŸŽ€

       which_transports(SvcName) ->
                                 [#{ref := reference(), type := atom()}]
                                 when SvcName :: string().

Return a list of transports associated with the service 'SvcName'.

which_watchdogs() ๐Ÿฆ‹

       which_watchdogs() ->
                                [#{ref := reference(),
                                   type := atom(),
                                   pid := pid(),
                                   state := diameter_service:wd_state(),
                                   peer := boolean() | pid(),
                                   uptime := elapsed_time(),
                                   service := SvcName}]
                                when SvcName :: string().

Return a list of all watchdogs.

which_watchdogs(SvcName) ->
                         [#{ref := reference(),
                            type := atom(),
                            pid := pid(),
                            state := diameter_service:wd_state(),
                            peer := boolean() | pid(),
                            uptime := elapsed_time()}]
                         when SvcName :: string().

okay bestie, this one's a little peek behind the curtain ๐Ÿ‘€ โ€” which_watchdogs(SvcName) hands you back a list of all the watchdogs chillin' with the service SvcName. so if you ever wanted to vibe check which lil guardians are on duty, this is your girlie ๐Ÿ’…

each entry in that list is a map, and every map is giving the same iconic shape โœจ:

  • ref โ€” a reference(), its little unique ID ๐ŸŽ€
  • type โ€” an atom(), so you know what kind of watchdog it is
  • pid โ€” the pid() doing the actual work ๐Ÿ”ฅ
  • state โ€” a diameter_service:wd_state(), i.e. what headspace it's currently in ๐Ÿง 
  • peer โ€” a boolean() or a pid(), so it's either a yes/no situation or an actual process
  • uptime โ€” an elapsed_time(), aka how long it's been clocked in and thriving โฐ

and remember, SvcName has to be a string() โ€” no cap, the guard says so ๐Ÿ™ˆ

so yeah, that's the whole function. which_watchdogs/1 โ€” short, sweet, and lowkey essential whenever you need to see who's holding down the fort ๐Ÿซถ


aaaand that's a wrap on diameter(3) bestie!! ๐ŸŒˆโœจ you made it all the way to the end, which is giving main character energy fr fr ๐Ÿ‘‘ this whole page was brought to you by Ericsson AB, and it's diameter 2.7.2, living in section diameter(3) of the manual ๐Ÿ’– go forth and slay your distributed Erlang adventures, ily, mwah ๐Ÿ’…๐ŸŽ€๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from