diff options
Diffstat (limited to 'lib/diameter')
41 files changed, 5177 insertions, 1803 deletions
diff --git a/lib/diameter/doc/src/diameter.xml b/lib/diameter/doc/src/diameter.xml index 2cbe48ecce..6b84b22eb5 100644 --- a/lib/diameter/doc/src/diameter.xml +++ b/lib/diameter/doc/src/diameter.xml @@ -397,10 +397,10 @@ from the peer offers it.</p> Note that each tuple communicates one or more AVP values. It is an error to specify duplicate tuples.</p> -<marker id="evaluable"/> +<marker id="eval"/> </item> -<tag><c>evaluable() = {M,F,A} | fun() | [evaluable() | A]</c></tag> +<tag><c>eval() = {M,F,A} | fun() | [eval() | A]</c></tag> <item> <p> An expression that can be evaluated as a function in the following @@ -418,7 +418,7 @@ eval(F) -> </pre> <p> -Applying an <c>&evaluable;</c> +Applying an <c>&eval;</c> <c>E</c> to an argument list <c>A</c> is meant in the sense of <c>eval([E|A])</c>.</p> @@ -484,11 +484,11 @@ Matches only those peers whose Origin-Realm has the specified value, or all peers if the atom <c>any</c>.</p> </item> -<tag><c>{eval, &evaluable;}</c></tag> +<tag><c>{eval, &eval;}</c></tag> <item> <p> Matches only those peers for which the specified -<c>&evaluable;</c> returns +<c>&eval;</c> returns <c>true</c> when applied to the connection's <c>diameter_caps</c> record. Any other return value or exception is equivalent to <c>false</c>.</p> @@ -650,7 +650,7 @@ Result = ResultCode | {capabilities_cb, CB, ResultCode|discard} Caps = #diameter_caps{} Pkt = #diameter_packet{} ResultCode = integer() -CB = &evaluable; +CB = &eval; </pre> <p> @@ -798,15 +798,31 @@ be matched by corresponding &capability; configuration, of </item> <tag> -<marker id="incoming_maxlen"/><c>{incoming_maxlen, 0..16777215}</c></tag> +<marker id="decode_format"/> +<c>{decode_format, record | list | map | none}</c></tag> <item> <p> -Bound on the expected size of incoming Diameter messages. -Messages larger than the specified number of bytes are discarded.</p> +The format of decoded messages and grouped AVPs in the <c>msg</c> field +of diameter_packet records and <c>value</c> field of diameter_avp +records respectively. +If <c>record</c> then a record whose definition is generated from the +dictionary file in question. +If <c>list</c> or <c>map</c> then a <c>[Name | Avps]</c> pair where +<c>Avps</c> is a list of AVP name/values pairs or a map keyed on +AVP names respectively. +If <c>none</c> then the atom-value message name, or <c>undefined</c> +for a Grouped AVP. +See also &codec_message;.</p> <p> -Defaults to <c>16777215</c>, the maximum value of the 24-bit Message -Length field in a Diameter Header.</p> +Defaults to <c>record</c>.</p> + +<note> +<p> +AVPs are decoded into a list of diameter_avp records in <c>avps</c> +field of diameter_packet records independently of +<c>decode_format</c>.</p> +</note> </item> @@ -814,7 +830,7 @@ Length field in a Diameter Header.</p> | node | nodes | [node()] - | evaluable()}</c></tag> + | eval()}</c></tag> <item> <p> The degree to which the service allows multiple transport @@ -825,7 +841,7 @@ at capabilities exchange.</p> If <c>[node()]</c> then a connection is rejected if another already exists on any of the specified nodes. Types <c>false</c>, <c>node</c>, <c>nodes</c> and -&evaluable; are equivalent to +&eval; are equivalent to <c>[]</c>, <c>[node()]</c>, <c>[node()|nodes()]</c> and the evaluated value respectively, evaluation of each expression taking place whenever a new connection is to be established. @@ -840,7 +856,7 @@ by their own peer and watchdog state machines.</p> Defaults to <c>nodes</c>.</p> </item> -<tag><c>{sequence, {H,N} | &evaluable;}</c></tag> +<tag><c>{sequence, {H,N} | &eval;}</c></tag> <item> <p> A constant value <c>H</c> for the topmost <c>32-N</c> bits of @@ -875,7 +891,7 @@ outgoing requests.</p> </warning> </item> -<tag><c>{share_peers, boolean() | [node()] | evaluable()}</c></tag> +<tag><c>{share_peers, boolean() | [node()] | eval()}</c></tag> <item> <p> Nodes to which peer connections established on the local @@ -888,7 +904,7 @@ configured to use them: see <c>use_shared_peers</c> below.</p> If <c>false</c> then peers are not shared. If <c>[node()]</c> then peers are shared with the specified list of nodes. -If <c>evaluable()</c> then peers are shared with the nodes returned +If <c>eval()</c> 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. @@ -914,59 +930,36 @@ of a single Diameter node across multiple Erlang nodes.</p> </note> </item> -<tag><c>{spawn_opt, [term()]}</c></tag> -<item> -<p> -Options list passed to &spawn_opt; when spawning a process for an -incoming Diameter request, unless the transport in question -specifies another value. -Options <c>monitor</c> and <c>link</c> are ignored.</p> - -<p> -Defaults to the empty list.</p> -</item> - <tag> -<marker id="strict_mbit"/><c>{strict_mbit, boolean()}</c></tag> +<marker id="strict_arities"/><c>{strict_arities, boolean() + | encode + | decode}</c></tag> <item> <p> -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 <c>true</c> then such AVPs are regarded as 5001 errors, -DIAMETER_AVP_UNSUPPORTED. -If <c>false</c> then the M-bit is ignored and policing -it becomes the receiver's responsibility.</p> +Whether or not to require that the number of AVPs in a message or +grouped AVP agree with those specified in the dictionary in question +when passing messages to &man_app; callbacks. +If <c>true</c> then mismatches in an outgoing messages cause message +encoding to fail, while mismatches in an incoming message are reported +as 5005/5009 errors in the errors field of the diameter_packet record +passed to &app_handle_request; or &app_handle_answer; callbacks. +If <c>false</c> then neither error is enforced/detected. +If <c>encode</c> or <c>decode</c> then errors are only +enforced/detected on outgoing or incoming messages respectively.</p> <p> Defaults to <c>true</c>.</p> -<warning> -<p> -RFC 6733 is unclear about the semantics of the M-bit. -One the one hand, the CCF specification in section 3.2 documents AVP -in a command grammar as meaning <em>any</em> 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.</p> -<p> -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 <c>AVP</c> 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.</p> +<note> <p> -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.</p> -</warning> - +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.</p> +</note> </item> <tag> @@ -993,7 +986,27 @@ The default value is for backwards compatibility.</p> </item> -<tag><c>{use_shared_peers, boolean() | [node()] | evaluable()}</c></tag> +<tag> +<marker id="traffic_counters"/><c>{traffic_counters, boolean()}</c></tag> +<item> +<p> +Whether or not to count application-specific messages; those for which +&man_app; callbacks take place. +If false then only messages handled by diameter itself are counted: +CER/CEA, DWR/DWA, DPR/DPA.</p> + +<p> +Defaults to <c>true</c>.</p> + +<note> +<p> +Disabling counters is a performance improvement, but means that the +omitted counters are not returned by &service_info;.</p> +</note> + +</item> + +<tag><c>{use_shared_peers, boolean() | [node()] | eval()}</c></tag> <item> <p> Nodes from which communicated peers are made available in @@ -1003,7 +1016,7 @@ the remote candidates list of &app_pick_peer; callbacks.</p> If <c>false</c> then remote peers are not used. If <c>[node()]</c> then only peers from the specified list of nodes are used. -If <c>evaluable()</c> then only peers returned by the specified +If <c>eval()</c> then only peers returned by the specified function are used, evaluated whenever a remote service communicates information about an available peer connection. The value <c>true</c> is equivalent to <c>fun &nodes;</c>. @@ -1028,6 +1041,15 @@ each node from which requests are sent.</p> </warning> </item> +<tag><c>&transport_opt;</c></tag> +<item> +<p> +Any transport option except <c>applications</c> or +<c>capabilities</c>. +Used as defaults for transport configuration, values passed to +&add_transport; overriding values configured on the service.</p> +</item> + </taglist> <marker id="transport_opt"/> @@ -1061,6 +1083,37 @@ implies having to set matching *-Application-Id AVPs in a </item> <tag> +<marker id="avp_dictionaries"/><c>{avp_dictionaries, [module()]}</c></tag> +<item> +<p> +A list of alternate dictionary modules with which to encode/decode +AVPs that are not defined by the dictionary of the application in +question. +At decode, such AVPs are represented as diameter_avp records in the +<c>'AVP'</c> field of a decoded message or Grouped AVP, the first +alternate that succeeds in decoding the AVP setting the record's value +field. +At encode, values in an <c>'AVP'</c> 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.</p> + +<p> +Defaults to the empty list.</p> + +<note> +<p> +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 +<c>diameter_gen_doic_rfc7683</c>, but alternate dictionaries can be +used to encode/decode any set of AVPs not known to an application +dictionary.</p> +</note> +</item> + +<tag> <marker id="capabilities"/><c>{capabilities, [&capability;]}</c></tag> <item> <p> @@ -1075,7 +1128,7 @@ TLS is desired over TCP as implemented by &man_tcp;.</p> </item> <tag> -<marker id="capabilities_cb"/><c>{capabilities_cb, &evaluable;}</c></tag> +<marker id="capabilities_cb"/><c>{capabilities_cb, &eval;}</c></tag> <item> <p> Callback invoked upon reception of CER/CEA during capabilities @@ -1169,7 +1222,7 @@ transport.</p> </item> <tag> -<marker id="disconnect_cb"/><c>{disconnect_cb, &evaluable;}</c></tag> +<marker id="disconnect_cb"/><c>{disconnect_cb, &eval;}</c></tag> <item> <p> Callback invoked prior to terminating the transport process of a @@ -1269,6 +1322,19 @@ Defaults to 5000.</p> </item> <tag> +<marker id="incoming_maxlen"/><c>{incoming_maxlen, 0..16777215}</c></tag> +<item> +<p> +Bound on the expected size of incoming Diameter messages. +Messages larger than the specified number of bytes are discarded.</p> + +<p> +Defaults to <c>16777215</c>, the maximum value of the 24-bit Message +Length field in a Diameter Header.</p> + +</item> + +<tag> <marker id="length_errors"/><c>{length_errors, exit|handle|discard}</c></tag> <item> <p> @@ -1326,7 +1392,64 @@ incoming Diameter request. Options <c>monitor</c> and <c>link</c> are ignored.</p> <p> -Defaults to the list configured on the service if not specified.</p> +Defaults to the empty list.</p> +</item> + +<tag> +<marker id="strict_capx"/><c>{strict_capx, boolean()]}</c></tag> +<item> +<p> +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 discarded.</p> + +<p> +Defaults to true. +Changing this results in non-standard behaviour, but can be useful in +case peers are known to be behave badly.</p> +</item> + +<tag> +<marker id="strict_mbit"/><c>{strict_mbit, boolean()}</c></tag> +<item> +<p> +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 <c>true</c> then such AVPs are regarded as 5001 errors, +DIAMETER_AVP_UNSUPPORTED. +If <c>false</c> then the M-bit is ignored and policing +it becomes the receiver's responsibility.</p> + +<p> +Defaults to <c>true</c>.</p> + +<warning> +<p> +RFC 6733 is unclear about the semantics of the M-bit. +One the one hand, the CCF specification in section 3.2 documents AVP +in a command grammar as meaning <em>any</em> 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.</p> +<p> +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 <c>AVP</c> 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.</p> +<p> +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.</p> +</warning> + </item> <tag> diff --git a/lib/diameter/doc/src/diameter_app.xml b/lib/diameter/doc/src/diameter_app.xml index dfcd00975b..aa334beb21 100644 --- a/lib/diameter/doc/src/diameter_app.xml +++ b/lib/diameter/doc/src/diameter_app.xml @@ -13,7 +13,8 @@ <header> <copyright> -<year>2011</year><year>2016</year> +<year>2011</year> +<year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -319,7 +320,7 @@ or &peer_down; callback.</p> <v>Action = Send | Discard | {eval_packet, Action, PostF}</v> <v>Send = {send, &packet; | &message;}</v> <v>Discard = {discard, Reason} | discard</v> -<v>PostF = &mod_evaluable;}</v> +<v>PostF = &mod_eval;}</v> </type> <desc> <p> @@ -371,7 +372,7 @@ discarded}</c>.</p> <v>Action = Send | Discard | {eval_packet, Action, PostF}</v> <v>Send = {send, &packet; | &message;}</v> <v>Discard = {discard, Reason} | discard</v> -<v>PostF = &mod_evaluable;}</v> +<v>PostF = &mod_eval;}</v> </type> <desc> <p> @@ -478,7 +479,7 @@ not selected.</p> | {answer_message, 3000..3999|5000..5999} | {protocol_error, 3000..3999}</v> <v>Opt = &mod_call_opt;</v> -<v>PostF = &mod_evaluable;</v> +<v>PostF = &mod_eval;</v> </type> <desc> <p> diff --git a/lib/diameter/doc/src/diameter_codec.xml b/lib/diameter/doc/src/diameter_codec.xml index 0117c1c88a..5124b49484 100644 --- a/lib/diameter/doc/src/diameter_codec.xml +++ b/lib/diameter/doc/src/diameter_codec.xml @@ -4,7 +4,10 @@ '<seealso marker="diameter_dict#MESSAGE_RECORDS">diameter_dict(4)</seealso>'> <!ENTITY types '<seealso marker="diameter_dict#DATA_TYPES">diameter_dict(4)</seealso>'> - <!ENTITY % also SYSTEM "seealso.ent" > + <!ENTITY decode_format + '<seealso marker="diameter#decode_format">decode format</seealso>'> + +<!ENTITY % also SYSTEM "seealso.ent" > <!ENTITY % here SYSTEM "seehere.ent" > %also; %here; @@ -145,7 +148,8 @@ question.</p> <p> The decoded value of an AVP. Will be <c>undefined</c> on decode if the data bytes could -not be decoded or the AVP is unknown. +not be decoded, the AVP is unknown, or if the &decode_format; is +<c>none</c>. The type of a decoded value is as document in &types;.</p> </item> @@ -230,7 +234,8 @@ header.</p> </item> <tag> -<marker id="message"/><c>message() = record() | list()</c></tag> +<marker id="message"/><c>message() = record() + | maybe_improper_list()</c></tag> <item> <p> The representation of a Diameter message as passed to @@ -240,7 +245,10 @@ a message as defined in a dictionary file is encoded as a record with one field for each component AVP. Equivalently, a message can also be encoded as a list whose head is the atom-valued message name (as specified in the relevant dictionary -file) and whose tail is a list of <c>{AvpName, AvpValue}</c> pairs.</p> +file) and whose tail is either a list of AVP name/values +pairs or a map with values keyed on AVP names. +The format at decode is determined by &mod_decode_format;. +Any of the formats is accepted at encode.</p> <p> Another list-valued representation allows a message to be specified @@ -283,15 +291,16 @@ value other than <c>undefined</c>.</p> <item> <p> The incoming/outgoing message. -For an incoming message, a record if the message can be -decoded in a non-relay application, <c>undefined</c> otherwise. +For an incoming message, a term corresponding to the configured +&decode_format; if the message can be decoded in a non-relay +application, <c>undefined</c> otherwise. For an outgoing message, setting a <c>[&header; | &avp;]</c> list is equivalent to setting the <c>header</c> and <c>avps</c> fields to the corresponding values.</p> <warning> <p> -A record-valued <c>msg</c> field does <em>not</em> imply an absence of +A value in the <c>msg</c> field does <em>not</em> imply an absence of decode errors. The <c>errors</c> field should also be examined.</p> </warning> diff --git a/lib/diameter/doc/src/diameter_sctp.xml b/lib/diameter/doc/src/diameter_sctp.xml index 9b6d629f79..c9b74a9ec5 100644 --- a/lib/diameter/doc/src/diameter_sctp.xml +++ b/lib/diameter/doc/src/diameter_sctp.xml @@ -16,7 +16,7 @@ <header> <copyright> <year>2011</year> -<year>2016</year> +<year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -116,7 +116,6 @@ and port respectively.</p> Multiple <c>ip</c> options can be specified for a multihomed peer. If none are specified then the values of <c>Host-IP-Address</c> in the <c>diameter_service</c> record are used. -(In particular, one of these must be specified.) Option <c>port</c> defaults to 3868 for a listening transport and 0 for a connecting transport.</p> diff --git a/lib/diameter/doc/src/diameter_tcp.xml b/lib/diameter/doc/src/diameter_tcp.xml index 6ca280c52b..1d65d14257 100644 --- a/lib/diameter/doc/src/diameter_tcp.xml +++ b/lib/diameter/doc/src/diameter_tcp.xml @@ -170,14 +170,11 @@ that will not be forthcoming, which will eventually cause the RFC 3539 watchdog to take down the connection.</p> <p> -If an <c>ip</c> option is not specified then the first element of a -non-empty <c>Host-IP-Address</c> list in <c>Svc</c> provides the local -IP address. -If neither is specified then the default address selected by &gen_tcp; -is used. -In all cases, the selected address is either returned from -&start; or passed in a <c>connected</c> message over the transport -interface.</p> +The first element of a non-empty <c>Host-IP-Address</c> list in +<c>Svc</c> provides the local IP address if an <c>ip</c> option is not +specified. +The local address is either returned from&start; or passed in a +<c>connected</c> message over the transport interface.</p> </desc> </func> diff --git a/lib/diameter/doc/src/seealso.ent b/lib/diameter/doc/src/seealso.ent index e5c284c6e8..c5a53670d0 100644 --- a/lib/diameter/doc/src/seealso.ent +++ b/lib/diameter/doc/src/seealso.ent @@ -4,7 +4,7 @@ %CopyrightBegin% -Copyright Ericsson AB 2012-2015. All Rights Reserved. +Copyright Ericsson AB 2012-2017. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ significant. <!ENTITY mod_application_opt '<seealso marker="diameter#application_opt">diameter:application_opt()</seealso>'> <!ENTITY mod_call_opt '<seealso marker="diameter#call_opt">diameter:call_opt()</seealso>'> <!ENTITY mod_capability '<seealso marker="diameter#capability">diameter:capability()</seealso>'> -<!ENTITY mod_evaluable '<seealso marker="diameter#evaluable">diameter:evaluable()</seealso>'> +<!ENTITY mod_eval '<seealso marker="diameter#eval">diameter:eval()</seealso>'> <!ENTITY mod_peer_filter '<seealso marker="diameter#peer_filter">diameter:peer_filter()</seealso>'> <!ENTITY mod_service_event '<seealso marker="diameter#service_event">diameter:service_event()</seealso>'> <!ENTITY mod_service_event_info '<seealso marker="diameter#service_event_info">diameter:service_event_info()</seealso>'> @@ -72,6 +72,7 @@ significant. <!ENTITY watchdog_timer '<seealso marker="#watchdog_timer">watchdog_timer</seealso>'> <!ENTITY mod_string_decode '<seealso marker="diameter#service_opt">diameter:service_opt()</seealso> <seealso marker="diameter#string_decode">string_decode</seealso>'> +<!ENTITY mod_decode_format '<seealso marker="diameter#service_opt">diameter:service_opt()</seealso> <seealso marker="diameter#decode_format">decode_format</seealso>'> <!-- diameter_app --> diff --git a/lib/diameter/doc/standard/rfc7683.txt b/lib/diameter/doc/standard/rfc7683.txt new file mode 100644 index 0000000000..ab2392c6c0 --- /dev/null +++ b/lib/diameter/doc/standard/rfc7683.txt @@ -0,0 +1,2355 @@ + + + + + + +Internet Engineering Task Force (IETF) J. Korhonen, Ed. +Request for Comments: 7683 Broadcom Corporation +Category: Standards Track S. Donovan, Ed. +ISSN: 2070-1721 B. Campbell + Oracle + L. Morand + Orange Labs + October 2015 + + + Diameter Overload Indication Conveyance + +Abstract + + This specification defines a base solution for Diameter overload + control, referred to as Diameter Overload Indication Conveyance + (DOIC). + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc7683. + +Copyright Notice + + Copyright (c) 2015 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + +Korhonen, et al. Standards Track [Page 1] + +RFC 7683 DOIC October 2015 + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. Terminology and Abbreviations . . . . . . . . . . . . . . . . 3 + 3. Conventions Used in This Document . . . . . . . . . . . . . . 5 + 4. Solution Overview . . . . . . . . . . . . . . . . . . . . . . 5 + 4.1. Piggybacking . . . . . . . . . . . . . . . . . . . . . . 6 + 4.2. DOIC Capability Announcement . . . . . . . . . . . . . . 7 + 4.3. DOIC Overload Condition Reporting . . . . . . . . . . . . 9 + 4.4. DOIC Extensibility . . . . . . . . . . . . . . . . . . . 11 + 4.5. Simplified Example Architecture . . . . . . . . . . . . . 12 + 5. Solution Procedures . . . . . . . . . . . . . . . . . . . . . 12 + 5.1. Capability Announcement . . . . . . . . . . . . . . . . . 12 + 5.1.1. Reacting Node Behavior . . . . . . . . . . . . . . . 13 + 5.1.2. Reporting Node Behavior . . . . . . . . . . . . . . . 13 + 5.1.3. Agent Behavior . . . . . . . . . . . . . . . . . . . 14 + 5.2. Overload Report Processing . . . . . . . . . . . . . . . 15 + 5.2.1. Overload Control State . . . . . . . . . . . . . . . 15 + 5.2.2. Reacting Node Behavior . . . . . . . . . . . . . . . 19 + 5.2.3. Reporting Node Behavior . . . . . . . . . . . . . . . 20 + 5.3. Protocol Extensibility . . . . . . . . . . . . . . . . . 22 + 6. Loss Algorithm . . . . . . . . . . . . . . . . . . . . . . . 23 + 6.1. Overview . . . . . . . . . . . . . . . . . . . . . . . . 23 + 6.2. Reporting Node Behavior . . . . . . . . . . . . . . . . . 24 + 6.3. Reacting Node Behavior . . . . . . . . . . . . . . . . . 24 + 7. Attribute Value Pairs . . . . . . . . . . . . . . . . . . . . 25 + 7.1. OC-Supported-Features AVP . . . . . . . . . . . . . . . . 25 + 7.2. OC-Feature-Vector AVP . . . . . . . . . . . . . . . . . . 25 + 7.3. OC-OLR AVP . . . . . . . . . . . . . . . . . . . . . . . 26 + 7.4. OC-Sequence-Number AVP . . . . . . . . . . . . . . . . . 26 + 7.5. OC-Validity-Duration AVP . . . . . . . . . . . . . . . . 26 + 7.6. OC-Report-Type AVP . . . . . . . . . . . . . . . . . . . 27 + 7.7. OC-Reduction-Percentage AVP . . . . . . . . . . . . . . . 27 + 7.8. AVP Flag Rules . . . . . . . . . . . . . . . . . . . . . 28 + 8. Error Response Codes . . . . . . . . . . . . . . . . . . . . 28 + 9. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 29 + 9.1. AVP Codes . . . . . . . . . . . . . . . . . . . . . . . . 29 + 9.2. New Registries . . . . . . . . . . . . . . . . . . . . . 29 + 10. Security Considerations . . . . . . . . . . . . . . . . . . . 30 + 10.1. Potential Threat Modes . . . . . . . . . . . . . . . . . 30 + 10.2. Denial-of-Service Attacks . . . . . . . . . . . . . . . 31 + 10.3. Noncompliant Nodes . . . . . . . . . . . . . . . . . . . 32 + 10.4. End-to-End Security Issues . . . . . . . . . . . . . . . 32 + 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 34 + 11.1. Normative References . . . . . . . . . . . . . . . . . . 34 + 11.2. Informative References . . . . . . . . . . . . . . . . . 34 + + + + + +Korhonen, et al. Standards Track [Page 2] + +RFC 7683 DOIC October 2015 + + + Appendix A. Issues Left for Future Specifications . . . . . . . 35 + A.1. Additional Traffic Abatement Algorithms . . . . . . . . . 35 + A.2. Agent Overload . . . . . . . . . . . . . . . . . . . . . 35 + A.3. New Error Diagnostic AVP . . . . . . . . . . . . . . . . 35 + Appendix B. Deployment Considerations . . . . . . . . . . . . . 35 + Appendix C. Considerations for Applications Integrating the DOIC + Solution . . . . . . . . . . . . . . . . . . . . . . 36 + C.1. Application Classification . . . . . . . . . . . . . . . 36 + C.2. Implications of Application Type Overload . . . . . . . . 37 + C.3. Request Transaction Classification . . . . . . . . . . . 38 + C.4. Request Type Overload Implications . . . . . . . . . . . 39 + Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . 41 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 42 + +1. Introduction + + This specification defines a base solution for Diameter overload + control, referred to as Diameter Overload Indication Conveyance + (DOIC), based on the requirements identified in [RFC7068]. + + This specification addresses Diameter overload control between + Diameter nodes that support the DOIC solution. The solution, which + is designed to apply to existing and future Diameter applications, + requires no changes to the Diameter base protocol [RFC6733] and is + deployable in environments where some Diameter nodes do not implement + the Diameter overload control solution defined in this specification. + + A new application specification can incorporate the overload control + mechanism specified in this document by making it mandatory to + implement for the application and referencing this specification + normatively. It is the responsibility of the Diameter application + designers to define how overload control mechanisms work on that + application. + + Note that the overload control solution defined in this specification + does not address all the requirements listed in [RFC7068]. A number + of features related to overload control are left for future + specifications. See Appendix A for a list of extensions that are + currently being considered. + +2. Terminology and Abbreviations + + Abatement + + Reaction to receipt of an overload report resulting in a reduction + in traffic sent to the reporting node. Abatement actions include + diversion and throttling. + + + + +Korhonen, et al. Standards Track [Page 3] + +RFC 7683 DOIC October 2015 + + + Abatement Algorithm + + An extensible method requested by reporting nodes and used by + reacting nodes to reduce the amount of traffic sent during an + occurrence of overload control. + + Diversion + + An overload abatement treatment where the reacting node selects + alternate destinations or paths for requests. + + Host-Routed Requests + + Requests that a reacting node knows will be served by a particular + host, either due to the presence of a Destination-Host Attribute + Value Pair (AVP) or by some other local knowledge on the part of + the reacting node. + + Overload Control State (OCS) + + Internal state maintained by a reporting or reacting node + describing occurrences of overload control. + + Overload Report (OLR) + + Overload control information for a particular overload occurrence + sent by a reporting node. + + Reacting Node + + A Diameter node that acts upon an overload report. + + Realm-Routed Requests + + Requests sent by a reacting node where the reacting node does not + know to which host the request will be routed. + + Reporting Node + + A Diameter node that generates an overload report. (This may or + may not be the overloaded node.) + + + + + + + + + + +Korhonen, et al. Standards Track [Page 4] + +RFC 7683 DOIC October 2015 + + + Throttling + + An abatement treatment that limits the number of requests sent by + the reacting node. Throttling can include a Diameter Client + choosing to not send requests, or a Diameter Agent or Server + rejecting requests with appropriate error responses. In both + cases, the result of the throttling is a permanent rejection of + the transaction. + +3. Conventions Used in This Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in RFC 2119 [RFC2119]. + + The interpretation from RFC 2119 [RFC2119] does not apply for the + above listed words when they are not used in all caps. + +4. Solution Overview + + The Diameter Overload Information Conveyance (DOIC) solution allows + Diameter nodes to request that other Diameter nodes perform overload + abatement actions, that is, actions to reduce the load offered to the + overloaded node or realm. + + A Diameter node that supports DOIC is known as a "DOIC node". Any + Diameter node can act as a DOIC node, including Diameter Clients, + Diameter Servers, and Diameter Agents. DOIC nodes are further + divided into "Reporting Nodes" and "Reacting Nodes." A reporting + node requests overload abatement by sending Overload Reports (OLRs). + + A reacting node acts upon OLRs and performs whatever actions are + needed to fulfill the abatement requests included in the OLRs. A + reporting node may report overload on its own behalf or on behalf of + other nodes. Likewise, a reacting node may perform overload + abatement on its own behalf or on behalf of other nodes. + + A Diameter node's role as a DOIC node is independent of its Diameter + role. For example, Diameter Agents may act as DOIC nodes, even + though they are not endpoints in the Diameter sense. Since Diameter + enables bidirectional applications, where Diameter Servers can send + requests towards Diameter Clients, a given Diameter node can + simultaneously act as both a reporting node and a reacting node. + + Likewise, a Diameter Agent may act as a reacting node from the + perspective of upstream nodes, and a reporting node from the + perspective of downstream nodes. + + + + +Korhonen, et al. Standards Track [Page 5] + +RFC 7683 DOIC October 2015 + + + DOIC nodes do not generate new messages to carry DOIC-related + information. Rather, they "piggyback" DOIC information over existing + Diameter messages by inserting new AVPs into existing Diameter + requests and responses. Nodes indicate support for DOIC, and any + needed DOIC parameters, by inserting an OC-Supported-Features AVP + (Section 7.1) into existing requests and responses. Reporting nodes + send OLRs by inserting OC-OLR AVPs (Section 7.3). + + A given OLR applies to the Diameter realm and application of the + Diameter message that carries it. If a reporting node supports more + than one realm and/or application, it reports independently for each + combination of realm and application. Similarly, the OC-Supported- + Features AVP applies to the realm and application of the enclosing + message. This implies that a node may support DOIC for one + application and/or realm, but not another, and may indicate different + DOIC parameters for each application and realm for which it supports + DOIC. + + Reacting nodes perform overload abatement according to an agreed-upon + abatement algorithm. An abatement algorithm defines the meaning of + some of the parameters of an OLR and the procedures required for + overload abatement. An overload abatement algorithm separates + Diameter requests into two sets. The first set contains the requests + that are to undergo overload abatement treatment of either throttling + or diversion. The second set contains the requests that are to be + given normal routing treatment. This document specifies a single + "must-support" algorithm, namely, the "loss" algorithm (Section 6). + Future specifications may introduce new algorithms. + + Overload conditions may vary in scope. For example, a single + Diameter node may be overloaded, in which case, reacting nodes may + attempt to send requests to other destinations. On the other hand, + an entire Diameter realm may be overloaded, in which case, such + attempts would do harm. DOIC OLRs have a concept of "report type" + (Section 7.6), where the type defines such behaviors. Report types + are extensible. This document defines report types for overload of a + specific host and for overload of an entire realm. + + DOIC works through non-supporting Diameter Agents that properly pass + unknown AVPs unchanged. + +4.1. Piggybacking + + There is no new Diameter application defined to carry overload- + related AVPs. The overload control AVPs defined in this + specification have been designed to be piggybacked on top of existing + + + + + +Korhonen, et al. Standards Track [Page 6] + +RFC 7683 DOIC October 2015 + + + application messages. This is made possible by adding the optional + overload control AVPs OC-OLR and OC-Supported-Features into existing + commands. + + Reacting nodes indicate support for DOIC by including the + OC-Supported-Features AVP in all request messages originated or + relayed by the reacting node. + + Reporting nodes indicate support for DOIC by including the + OC-Supported-Features AVP in all answer messages that are originated + or relayed by the reporting node and that are in response to a + request that contained the OC-Supported-Features AVP. Reporting + nodes may include overload reports using the OC-OLR AVP in answer + messages. + + Note that the overload control solution does not have fixed server + and client roles. The DOIC node role is determined based on the + message type: whether the message is a request (i.e., sent by a + "reacting node") or an answer (i.e., sent by a "reporting node"). + Therefore, in a typical client-server deployment, the Diameter Client + may report its overload condition to the Diameter Server for any + Diameter-Server-initiated message exchange. An example of such is + the Diameter Server requesting a re-authentication from a Diameter + Client. + +4.2. DOIC Capability Announcement + + The DOIC solution supports the ability for Diameter nodes to + determine if other nodes in the path of a request support the + solution. This capability is referred to as DOIC Capability + Announcement (DCA) and is separate from the Diameter Capability + Exchange. + + The DCA mechanism uses the OC-Supported-Features AVPs to indicate the + Diameter overload features supported. + + The first node in the path of a Diameter request that supports the + DOIC solution inserts the OC-Supported-Features AVP in the request + message. + + The individual features supported by the DOIC nodes are indicated in + the OC-Feature-Vector AVP. Any semantics associated with the + features will be defined in extension specifications that introduce + the features. + + Note: As discussed elsewhere in the document, agents in the path + of the request can modify the OC-Supported-Features AVP. + + + + +Korhonen, et al. Standards Track [Page 7] + +RFC 7683 DOIC October 2015 + + + Note: The DOIC solution must support deployments where Diameter + Clients and/or Diameter Servers do not support the DOIC solution. + In this scenario, Diameter Agents that support the DOIC solution + may handle overload abatement for the non-supporting Diameter + nodes. In this case, the DOIC agent will insert the OC-Supported- + Features AVP in requests that do not already contain one, telling + the reporting node that there is a DOIC node that will handle + overload abatement. For transactions where there was an + OC-Supporting-Features AVP in the request, the agent will insert + the OC-Supported-Features AVP in answers, telling the reacting + node that there is a reporting node. + + The OC-Feature-Vector AVP will always contain an indication of + support for the loss overload abatement algorithm defined in this + specification (see Section 6). This ensures that a reporting node + always supports at least one of the advertised abatement algorithms + received in a request messages. + + The reporting node inserts the OC-Supported-Features AVP in all + answer messages to requests that contained the OC-Supported-Features + AVP. The contents of the reporting node's OC-Supported-Features AVP + indicate the set of Diameter overload features supported by the + reporting node. This specification defines one exception -- the + reporting node only includes an indication of support for one + overload abatement algorithm, independent of the number of overload + abatement algorithms actually supported by the reacting node. The + overload abatement algorithm indicated is the algorithm that the + reporting node intends to use should it enter an overload condition. + Reacting nodes can use the indicated overload abatement algorithm to + prepare for possible overload reports and must use the indicated + overload abatement algorithm if traffic reduction is actually + requested. + + Note that the loss algorithm defined in this document is a + stateless abatement algorithm. As a result, it does not require + any actions by reacting nodes prior to the receipt of an overload + report. Stateful abatement algorithms that base the abatement + logic on a history of request messages sent might require reacting + nodes to maintain state in advance of receiving an overload report + to ensure that the overload reports can be properly handled. + + While it should only be done in exceptional circumstances and not + during an active occurrence of overload, a reacting node that wishes + to transition to a different abatement algorithm can stop advertising + support for the algorithm indicated by the reporting node, as long as + support for the loss algorithm is always advertised. + + + + + +Korhonen, et al. Standards Track [Page 8] + +RFC 7683 DOIC October 2015 + + + The DCA mechanism must also allow the scenario where the set of + features supported by the sender of a request and by agents in the + path of a request differ. In this case, the agent can update the + OC-Supported-Features AVP to reflect the mixture of the two sets of + supported features. + + Note: The logic to determine if the content of the OC-Supported- + Features AVP should be changed is out of scope for this document, + as is the logic to determine the content of a modified + OC-Supported-Features AVP. These are left to implementation + decisions. Care must be taken not to introduce interoperability + issues for downstream or upstream DOIC nodes. As such, the agent + must act as a fully compliant reporting node to the downstream + reacting node and as a fully compliant reacting node to the + upstream reporting node. + +4.3. DOIC Overload Condition Reporting + + As with DOIC capability announcement, overload condition reporting + uses new AVPs (Section 7.3) to indicate an overload condition. + + The OC-OLR AVP is referred to as an overload report. The OC-OLR AVP + includes the type of report, a sequence number, the length of time + that the report is valid, and AVPs specific to the abatement + algorithm. + + Two types of overload reports are defined in this document: host + reports and realm reports. + + A report of type "HOST_REPORT" is sent to indicate the overload of a + specific host, identified by the Origin-Host AVP of the message + containing the OLR, for the Application-ID indicated in the + transaction. When receiving an OLR of type "HOST_REPORT", a reacting + node applies overload abatement treatment to the host-routed requests + identified by the overload abatement algorithm (as defined in + Section 2) sent for this application to the overloaded host. + + A report of type "REALM_REPORT" is sent to indicate the overload of a + realm for the Application-ID indicated in the transaction. The + overloaded realm is identified by the Destination-Realm AVP of the + message containing the OLR. When receiving an OLR of type + "REALM_REPORT", a reacting node applies overload abatement treatment + to realm-routed requests identified by the overload abatement + algorithm (as defined in Section 2) sent for this application to the + overloaded realm. + + + + + + +Korhonen, et al. Standards Track [Page 9] + +RFC 7683 DOIC October 2015 + + + This document assumes that there is a single source for realm reports + for a given realm, or that if multiple nodes can send realm reports, + that each such node has full knowledge of the overload state of the + entire realm. A reacting node cannot distinguish between receiving + realm reports from a single node or from multiple nodes. + + Note: Known issues exist if there are multiple sources for + overload reports that apply to the same Diameter entity. Reacting + nodes have no way of determining the source and, as such, will + treat them as coming from a single source. Variance in sequence + numbers between the two sources can then cause incorrect overload + abatement treatment to be applied for indeterminate periods of + time. + + Reporting nodes are responsible for determining the need for a + reduction of traffic. The method for making this determination is + implementation specific and depends on the type of overload report + being generated. A host report might be generated by tracking use of + resources required by the host to handle transactions for the + Diameter application. A realm report generally impacts the traffic + sent to multiple hosts and, as such, requires tracking the capacity + of all servers able to handle realm-routed requests for the + application and realm. + + Once a reporting node determines the need for a reduction in traffic, + it uses the DOIC-defined AVPs to report on the condition. These AVPs + are included in answer messages sent or relayed by the reporting + node. The reporting node indicates the overload abatement algorithm + that is to be used to handle the traffic reduction in the + OC-Supported-Features AVP. The OC-OLR AVP is used to communicate + information about the requested reduction. + + Reacting nodes, upon receipt of an overload report, apply the + overload abatement algorithm to traffic impacted by the overload + report. The method used to determine the requests that are to + receive overload abatement treatment is dependent on the abatement + algorithm. The loss abatement algorithm is defined in this document + (Section 6). Other abatement algorithms can be defined in extensions + to the DOIC solution. + + Two types of overload abatement treatment are defined, diversion and + throttling. Reacting nodes are responsible for determining which + treatment is appropriate for individual requests. + + As the conditions that lead to the generation of the overload report + change, the reporting node can send new overload reports requesting + greater reduction if the condition gets worse or less reduction if + the condition improves. The reporting node sends an overload report + + + +Korhonen, et al. Standards Track [Page 10] + +RFC 7683 DOIC October 2015 + + + with a duration of zero to indicate that the overload condition has + ended and abatement is no longer needed. + + The reacting node also determines when the overload report expires + based on the OC-Validity-Duration AVP in the overload report and + stops applying the abatement algorithm when the report expires. + + Note that erroneous overload reports can be used for DoS attacks. + This includes the ability to indicate that a significant reduction in + traffic, up to and including a request for no traffic, should be sent + to a reporting node. As such, care should be taken to verify the + sender of overload reports. + +4.4. DOIC Extensibility + + The DOIC solution is designed to be extensible. This extensibility + is based on existing Diameter-based extensibility mechanisms, along + with the DOIC capability announcement mechanism. + + There are multiple categories of extensions that are expected. This + includes the definition of new overload abatement algorithms, the + definition of new report types, and the definition of new scopes of + messages impacted by an overload report. + + A DOIC node communicates supported features by including them in the + OC-Feature-Vector AVP, as a sub-AVP of OC-Supported-Features. Any + non-backwards-compatible DOIC extensions define new values for the + OC-Feature-Vector AVP. DOIC extensions also have the ability to add + new AVPs to the OC-Supported-Features AVP, if additional information + about the new feature is required. + + Overload reports can also be extended by adding new sub-AVPs to the + OC-OLR AVP, allowing reporting nodes to communicate additional + information about handling an overload condition. + + If necessary, new extensions can also define new AVPs that are not + part of the OC-Supported-Features and OC-OLR group AVPs. It is, + however, recommended that DOIC extensions use the OC-Supported- + Features AVP and OC-OLR AVP to carry all DOIC-related AVPs. + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 11] + +RFC 7683 DOIC October 2015 + + +4.5. Simplified Example Architecture + + Figure 1 illustrates the simplified architecture for Diameter + overload information conveyance. + + Realm X Same or other Realms + <--------------------------------------> <----------------------> + + + +--------+ : (optional) : + |Diameter| : : + |Server A|--+ .--. : +--------+ : .--. + +--------+ | _( `. : |Diameter| : _( `. +--------+ + +--( )--:-| Agent |-:--( )--|Diameter| + +--------+ | ( ` . ) ) : +--------+ : ( ` . ) ) | Client | + |Diameter|--+ `--(___.-' : : `--(___.-' +--------+ + |Server B| : : + +--------+ : : + + End-to-end Overload Indication + 1) <-----------------------------------------------> + Diameter Application Y + + Overload Indication A Overload Indication A' + 2) <----------------------> <----------------------> + Diameter Application Y Diameter Application Y + + Figure 1: Simplified Architecture Choices for Overload Indication + Delivery + + In Figure 1, the Diameter overload indication can be conveyed (1) + end-to-end between servers and clients or (2) between servers and the + Diameter Agent inside the realm and then between the Diameter Agent + and the clients. + +5. Solution Procedures + + This section outlines the normative behavior for the DOIC solution. + +5.1. Capability Announcement + + This section defines DOIC Capability Announcement (DCA) behavior. + + Note: This specification assumes that changes in DOIC node + capabilities are relatively rare events that occur as a result of + administrative action. Reacting nodes ought to minimize changes + that force the reporting node to change the features being used, + especially during active overload conditions. But even if + + + +Korhonen, et al. Standards Track [Page 12] + +RFC 7683 DOIC October 2015 + + + reacting nodes avoid such changes, reporting nodes still have to + be prepared for them to occur. For example, differing + capabilities between multiple reacting nodes may still force a + reporting node to select different features on a per-transaction + basis. + +5.1.1. Reacting Node Behavior + + A reacting node MUST include the OC-Supported-Features AVP in all + requests. It MAY include the OC-Feature-Vector AVP, as a sub-AVP of + OC-Supported-Features. If it does so, it MUST indicate support for + the "loss" algorithm. If the reacting node is configured to support + features (including other algorithms) in addition to the loss + algorithm, it MUST indicate such support in an OC-Feature-Vector AVP. + + An OC-Supported-Features AVP in answer messages indicates there is a + reporting node for the transaction. The reacting node MAY take + action, for example, creating state for some stateful abatement + algorithm, based on the features indicated in the OC-Feature-Vector + AVP. + + Note: The loss abatement algorithm does not require stateful + behavior when there is no active overload report. + + Reacting nodes need to be prepared for the reporting node to change + selected algorithms. This can happen at any time, including when the + reporting node has sent an active overload report. The reacting node + can minimize the potential for changes by modifying the advertised + abatement algorithms sent to an overloaded reporting node to the + currently selected algorithm and loss (or just loss if it is the + currently selected algorithm). This has the effect of limiting the + potential change in abatement algorithm from the currently selected + algorithm to loss, avoiding changes to more complex abatement + algorithms that require state to operate properly. + +5.1.2. Reporting Node Behavior + + Upon receipt of a request message, a reporting node determines if + there is a reacting node for the transaction based on the presence of + the OC-Supported-Features AVP in the request message. + + If the request message contains an OC-Supported-Features AVP, then a + reporting node MUST include the OC-Supported-Features AVP in the + answer message for that transaction. + + Note: Capability announcement is done on a per-transaction basis. + The reporting node cannot assume that the capabilities announced + by a reacting node will be the same between transactions. + + + +Korhonen, et al. Standards Track [Page 13] + +RFC 7683 DOIC October 2015 + + + A reporting node MUST NOT include the OC-Supported-Features AVP, + OC-OLR AVP, or any other overload control AVPs defined in extension + documents in response messages for transactions where the request + message does not include the OC-Supported-Features AVP. Lack of the + OC-Supported-Features AVP in the request message indicates that there + is no reacting node for the transaction. + + A reporting node knows what overload control functionality is + supported by the reacting node based on the content or absence of the + OC-Feature-Vector AVP within the OC-Supported-Features AVP in the + request message. + + A reporting node MUST select a single abatement algorithm in the + OC-Feature-Vector AVP. The abatement algorithm selected MUST + indicate the abatement algorithm the reporting node wants the + reacting node to use when the reporting node enters an overload + condition. + + The abatement algorithm selected MUST be from the set of abatement + algorithms contained in the request message's OC-Feature-Vector AVP. + + A reporting node that selects the loss algorithm may do so by + including the OC-Feature-Vector AVP with an explicit indication of + the loss algorithm, or it MAY omit the OC-Feature-Vector AVP. If it + selects a different algorithm, it MUST include the OC-Feature-Vector + AVP with an explicit indication of the selected algorithm. + + The reporting node SHOULD indicate support for other DOIC features + defined in extension documents that it supports and that apply to the + transaction. It does so using the OC-Feature-Vector AVP. + + Note: Not all DOIC features will apply to all Diameter + applications or deployment scenarios. The features included in + the OC-Feature-Vector AVP are based on local policy of the + reporting node. + +5.1.3. Agent Behavior + + Diameter Agents that support DOIC can ensure that all messages + relayed by the agent contain the OC-Supported-Features AVP. + + A Diameter Agent MAY take on reacting node behavior for Diameter + endpoints that do not support the DOIC solution. A Diameter Agent + detects that a Diameter endpoint does not support DOIC reacting node + behavior when there is no OC-Supported-Features AVP in a request + message. + + + + + +Korhonen, et al. Standards Track [Page 14] + +RFC 7683 DOIC October 2015 + + + For a Diameter Agent to be a reacting node for a non-supporting + Diameter endpoint, the Diameter Agent MUST include the OC-Supported- + Features AVP in request messages it relays that do not contain the + OC-Supported-Features AVP. + + A Diameter Agent MAY take on reporting node behavior for Diameter + endpoints that do not support the DOIC solution. The Diameter Agent + MUST have visibility to all traffic destined for the non-supporting + host in order to become the reporting node for the Diameter endpoint. + A Diameter Agent detects that a Diameter endpoint does not support + DOIC reporting node behavior when there is no OC-Supported-Features + AVP in an answer message for a transaction that contained the + OC-Supported-Features AVP in the request message. + + If a request already has the OC-Supported-Features AVP, a Diameter + Agent MAY modify it to reflect the features appropriate for the + transaction. Otherwise, the agent relays the OC-Supported-Features + AVP without change. + + Example: If the agent supports a superset of the features reported + by the reacting node, then the agent might choose, based on local + policy, to advertise that superset of features to the reporting + node. + + If the Diameter Agent changes the OC-Supported-Features AVP in a + request message, then it is likely it will also need to modify the + OC-Supported-Features AVP in the answer message for the transaction. + A Diameter Agent MAY modify the OC-Supported-Features AVP carried in + answer messages. + + When making changes to the OC-Supported-Features or OC-OLR AVPs, the + Diameter Agent needs to ensure consistency in its behavior with both + upstream and downstream DOIC nodes. + +5.2. Overload Report Processing + +5.2.1. Overload Control State + + Both reacting and reporting nodes maintain Overload Control State + (OCS) for active overload conditions. The following sections define + behavior associated with that OCS. + + The contents of the OCS in the reporting node and in the reacting + node represent logical constructs. The actual internal physical + structure of the state included in the OCS is an implementation + decision. + + + + + +Korhonen, et al. Standards Track [Page 15] + +RFC 7683 DOIC October 2015 + + +5.2.1.1. Overload Control State for Reacting Nodes + + A reacting node maintains the following OCS per supported Diameter + application: + + o a host-type OCS entry for each Destination-Host to which it sends + host-type requests and + + o a realm-type OCS entry for each Destination-Realm to which it + sends realm-type requests. + + A host-type OCS entry is identified by the pair of Application-ID and + the node's DiameterIdentity. + + A realm-type OCS entry is identified by the pair of Application-ID + and realm. + + The host-type and realm-type OCS entries include the following + information (the actual information stored is an implementation + decision): + + o Sequence number (as received in OC-OLR; see Section 7.3) + + o Time of expiry (derived from OC-Validity-Duration AVP received in + the OC-OLR AVP and time of reception of the message carrying + OC-OLR AVP) + + o Selected abatement algorithm (as received in the OC-Supported- + Features AVP) + + o Input data that is abatement algorithm specific (as received in + the OC-OLR AVP -- for example, OC-Reduction-Percentage for the + loss abatement algorithm) + +5.2.1.2. Overload Control State for Reporting Nodes + + A reporting node maintains OCS entries per supported Diameter + application, per supported (and eventually selected) abatement + algorithm, and per report type. + + An OCS entry is identified by the tuple of Application-ID, report + type, and abatement algorithm, and it includes the following + information (the actual information stored is an implementation + decision): + + o Sequence number + + o Validity duration + + + +Korhonen, et al. Standards Track [Page 16] + +RFC 7683 DOIC October 2015 + + + o Expiration time + + o Input data that is algorithm specific (for example, the reduction + percentage for the loss abatement algorithm) + +5.2.1.3. Reacting Node's Maintenance of Overload Control State + + When a reacting node receives an OC-OLR AVP, it MUST determine if it + is for an existing or new overload condition. + + Note: For the remainder of this section, the term "OLR" refers to + the combination of the contents of the received OC-OLR AVP and the + abatement algorithm indicated in the received OC-Supported- + Features AVP. + + When receiving an answer message with multiple OLRs of different + supported report types, a reacting node MUST process each received + OLR. + + The OLR is for an existing overload condition if a reacting node has + an OCS that matches the received OLR. + + For a host report, this means it matches the Application-ID and the + host's DiameterIdentity in an existing host OCS entry. + + For a realm report, this means it matches the Application-ID and the + realm in an existing realm OCS entry. + + If the OLR is for an existing overload condition, then a reacting + node MUST determine if the OLR is a retransmission or an update to + the existing OLR. + + If the sequence number for the received OLR is greater than the + sequence number stored in the matching OCS entry, then a reacting + node MUST update the matching OCS entry. + + If the sequence number for the received OLR is less than or equal to + the sequence number in the matching OCS entry, then a reacting node + MUST silently ignore the received OLR. The matching OCS MUST NOT be + updated in this case. + + If the reacting node determines that the sequence number has rolled + over, then the reacting node MUST update the matching OCS entry. + This can be determined by recognizing that the number has changed + from a value within 1% of the maximum value in the OC-Sequence-Number + AVP to a value within 1% of the minimum value in the OC-Sequence- + Number AVP. + + + + +Korhonen, et al. Standards Track [Page 17] + +RFC 7683 DOIC October 2015 + + + If the received OLR is for a new overload condition, then a reacting + node MUST generate a new OCS entry for the overload condition. + + For a host report, this means a reacting node creates an OCS entry + with the Application-ID in the received message and DiameterIdentity + of the Origin-Host in the received message. + + Note: This solution assumes that the Origin-Host AVP in the answer + message included by the reporting node is not changed along the + path to the reacting node. + + For a realm report, this means a reacting node creates an OCS entry + with the Application-ID in the received message and realm of the + Origin-Realm in the received message. + + If the received OLR contains a validity duration of zero ("0"), then + a reacting node MUST update the OCS entry as being expired. + + Note: It is not necessarily appropriate to delete the OCS entry, + as the recommended behavior is that the reacting node slowly + returns to full traffic when ending an overload abatement period. + + The reacting node does not delete an OCS when receiving an answer + message that does not contain an OC-OLR AVP (i.e., absence of OLR + means "no change"). + +5.2.1.4. Reporting Node's Maintenance of Overload Control State + + A reporting node SHOULD create a new OCS entry when entering an + overload condition. + + Note: If a reporting node knows through absence of the + OC-Supported-Features AVP in received messages that there are no + reacting nodes supporting DOIC, then the reporting node can choose + to not create OCS entries. + + When generating a new OCS entry, the sequence number SHOULD be set to + zero ("0"). + + When generating sequence numbers for new overload conditions, the new + sequence number MUST be greater than any sequence number in an active + (unexpired) overload report for the same application and report type + previously sent by the reporting node. This property MUST hold over + a reboot of the reporting node. + + + + + + + +Korhonen, et al. Standards Track [Page 18] + +RFC 7683 DOIC October 2015 + + + Note: One way of addressing this over a reboot of a reporting node + is to use a timestamp for the first overload condition that occurs + after the report and to start using sequences beginning with zero + for subsequent overload conditions. + + A reporting node MUST update an OCS entry when it needs to adjust the + validity duration of the overload condition at reacting nodes. + + Example: If a reporting node wishes to instruct reacting nodes to + continue overload abatement for a longer period of time than + originally communicated. This also applies if the reporting node + wishes to shorten the period of time that overload abatement is to + continue. + + A reporting node MUST update an OCS entry when it wishes to adjust + any parameters specific to the abatement algorithm, including, for + example, the reduction percentage used for the loss abatement + algorithm. + + Example: If a reporting node wishes to change the reduction + percentage either higher (if the overload condition has worsened) + or lower (if the overload condition has improved), then the + reporting node would update the appropriate OCS entry. + + A reporting node MUST increment the sequence number associated with + the OCS entry anytime the contents of the OCS entry are changed. + This will result in a new sequence number being sent to reacting + nodes, instructing them to process the OC-OLR AVP. + + A reporting node SHOULD update an OCS entry with a validity duration + of zero ("0") when the overload condition ends. + + Note: If a reporting node knows that the OCS entries in the + reacting nodes are near expiration, then the reporting node might + decide not to send an OLR with a validity duration of zero. + + A reporting node MUST keep an OCS entry with a validity duration of + zero ("0") for a period of time long enough to ensure that any + unexpired reacting node's OCS entry created as a result of the + overload condition in the reporting node is deleted. + +5.2.2. Reacting Node Behavior + + When a reacting node sends a request, it MUST determine if that + request matches an active OCS. + + + + + + +Korhonen, et al. Standards Track [Page 19] + +RFC 7683 DOIC October 2015 + + + If the request matches an active OCS, then the reacting node MUST use + the overload abatement algorithm indicated in the OCS to determine if + the request is to receive overload abatement treatment. + + For the loss abatement algorithm defined in this specification, see + Section 6 for the overload abatement algorithm logic applied. + + If the overload abatement algorithm selects the request for overload + abatement treatment, then the reacting node MUST apply overload + abatement treatment on the request. The abatement treatment applied + depends on the context of the request. + + If diversion abatement treatment is possible (i.e., a different path + for the request can be selected where the overloaded node is not part + of the different path), then the reacting node SHOULD apply diversion + abatement treatment to the request. The reacting node MUST apply + throttling abatement treatment to requests identified for abatement + treatment when diversion treatment is not possible or was not + applied. + + Note: This only addresses the case where there are two defined + abatement treatments, diversion and throttling. Any extension + that defines a new abatement treatment must also define its + interaction with existing treatments. + + If the overload abatement treatment results in throttling of the + request and if the reacting node is an agent, then the agent MUST + send an appropriate error as defined in Section 8. + + Diameter endpoints that throttle requests need to do so according to + the rules of the client application. Those rules will vary by + application and are beyond the scope of this document. + + In the case that the OCS entry indicated no traffic was to be sent to + the overloaded entity and the validity duration expires, then + overload abatement associated with the overload report MUST be ended + in a controlled fashion. + +5.2.3. Reporting Node Behavior + + If there is an active OCS entry, then a reporting node SHOULD include + the OC-OLR AVP in all answers to requests that contain the + OC-Supported-Features AVP and that match the active OCS entry. + + Note: A request matches 1) if the Application-ID in the request + matches the Application-ID in any active OCS entry and 2) if the + report type in the OCS entry matches a report type supported by + the reporting node as indicated in the OC-Supported-Features AVP. + + + +Korhonen, et al. Standards Track [Page 20] + +RFC 7683 DOIC October 2015 + + + The contents of the OC-OLR AVP depend on the selected algorithm. + + A reporting node MAY choose to not resend an overload report to a + reacting node if it can guarantee that this overload report is + already active in the reacting node. + + Note: In some cases (e.g., when there are one or more agents in + the path between reporting and reacting nodes, or when overload + reports are discarded by reacting nodes), a reporting node may not + be able to guarantee that the reacting node has received the + report. + + A reporting node MUST NOT send overload reports of a type that has + not been advertised as supported by the reacting node. + + Note: A reacting node implicitly advertises support for the host + and realm report types by including the OC-Supported-Features AVP + in the request. Support for other report types will be explicitly + indicated by new feature bits in the OC-Feature-Vector AVP. + + A reporting node SHOULD explicitly indicate the end of an overload + occurrence by sending a new OLR with OC-Validity-Duration set to a + value of zero ("0"). The reporting node SHOULD ensure that all + reacting nodes receive the updated overload report. + + A reporting node MAY rely on the OC-Validity-Duration AVP values for + the implicit cleanup of overload control state on the reacting node. + + Note: All OLRs sent have an expiration time calculated by adding + the validity duration contained in the OLR to the time the message + was sent. Transit time for the OLR can be safely ignored. The + reporting node can ensure that all reacting nodes have received + the OLR by continuing to send it in answer messages until the + expiration time for all OLRs sent for that overload condition have + expired. + + When a reporting node sends an OLR, it effectively delegates any + necessary throttling to downstream nodes. If the reporting node also + locally throttles the same set of messages, the overall number of + throttled requests may be higher than intended. Therefore, before + applying local message throttling, a reporting node needs to check if + these messages match existing OCS entries, indicating that these + messages have survived throttling applied by downstream nodes that + have received the related OLR. + + However, even if the set of messages match existing OCS entries, the + reporting node can still apply other abatement methods such as + diversion. The reporting node might also need to throttle requests + + + +Korhonen, et al. Standards Track [Page 21] + +RFC 7683 DOIC October 2015 + + + for reasons other than overload. For example, an agent or server + might have a configured rate limit for each client and might throttle + requests that exceed that limit, even if such requests had already + been candidates for throttling by downstream nodes. The reporting + node also has the option to send new OLRs requesting greater + reductions in traffic, reducing the need for local throttling. + + A reporting node SHOULD decrease requested overload abatement + treatment in a controlled fashion to avoid oscillations in traffic. + + Example: A reporting node might wait some period of time after + overload ends before terminating the OLR, or it might send a + series of OLRs indicating progressively less overload severity. + +5.3. Protocol Extensibility + + The DOIC solution can be extended. Types of potential extensions + include new traffic abatement algorithms, new report types, or other + new functionality. + + When defining a new extension that requires new normative behavior, + the specification must define a new feature for the OC-Feature-Vector + AVP. This feature bit is used to communicate support for the new + feature. + + The extension may define new AVPs for use in the DOIC Capability + Announcement and for use in DOIC overload reporting. These new AVPs + SHOULD be defined to be extensions to the OC-Supported-Features or + OC-OLR AVPs defined in this document. + + The Grouped AVP extension mechanisms defined in [RFC6733] apply. + This allows, for example, defining a new feature that is mandatory to + be understood even when piggybacked on an existing application. + + When defining new report type values, the corresponding specification + must define the semantics of the new report types and how they affect + the OC-OLR AVP handling. + + The OC-Supported-Feature and OC-OLR AVPs can be expanded with + optional sub-AVPs only if a legacy DOIC implementation can safely + ignore them without breaking backward compatibility for the given + OC-Report-Type AVP value. Any new sub-AVPs must not require that the + M-bit be set. + + Documents that introduce new report types must describe any + limitations on their use across non-supporting agents. + + + + + +Korhonen, et al. Standards Track [Page 22] + +RFC 7683 DOIC October 2015 + + + As with any Diameter specification, RFC 6733 requires all new AVPs to + be registered with IANA. See Section 9 for the required procedures. + New features (feature bits in the OC-Feature-Vector AVP) and report + types (in the OC-Report-Type AVP) MUST be registered with IANA. + +6. Loss Algorithm + + This section documents the Diameter overload loss abatement + algorithm. + +6.1. Overview + + The DOIC specification supports the ability for multiple overload + abatement algorithms to be specified. The abatement algorithm used + for any instance of overload is determined by the DOIC Capability + Announcement process documented in Section 5.1. + + The loss algorithm described in this section is the default algorithm + that must be supported by all Diameter nodes that support DOIC. + + The loss algorithm is designed to be a straightforward and stateless + overload abatement algorithm. It is used by reporting nodes to + request a percentage reduction in the amount of traffic sent. The + traffic impacted by the requested reduction depends on the type of + overload report. + + Reporting nodes request the stateless reduction of the number of + requests by an indicated percentage. This percentage reduction is in + comparison to the number of messages the node otherwise would send, + regardless of how many requests the node might have sent in the past. + + From a conceptual level, the logic at the reacting node could be + outlined as follows. + + 1. An overload report is received, and the associated OCS is either + saved or updated (if required) by the reacting node. + + 2. A new Diameter request is generated by the application running on + the reacting node. + + 3. The reacting node determines that an active overload report + applies to the request, as indicated by the corresponding OCS + entry. + + 4. The reacting node determines if overload abatement treatment + should be applied to the request. One approach that could be + taken for each request is to select a uniformly selected random + number between 1 and 100. If the random number is less than or + + + +Korhonen, et al. Standards Track [Page 23] + +RFC 7683 DOIC October 2015 + + + equal to the indicated reduction percentage, then the request is + given abatement treatment; otherwise, the request is given normal + routing treatment. + +6.2. Reporting Node Behavior + + The method a reporting node uses to determine the amount of traffic + reduction required to address an overload condition is an + implementation decision. + + When a reporting node that has selected the loss abatement algorithm + determines the need to request a reduction in traffic, it includes an + OC-OLR AVP in answer messages as described in Section 5.2.3. + + When sending the OC-OLR AVP, the reporting node MUST indicate a + percentage reduction in the OC-Reduction-Percentage AVP. + + The reporting node MAY change the reduction percentage in subsequent + overload reports. When doing so, the reporting node must conform to + overload report handling specified in Section 5.2.3. + +6.3. Reacting Node Behavior + + The method a reacting node uses to determine which request messages + are given abatement treatment is an implementation decision. + + When receiving an OC-OLR in an answer message where the algorithm + indicated in the OC-Supported-Features AVP is the loss algorithm, the + reacting node MUST apply abatement treatment to the requested + percentage of request messages sent. + + Note: The loss algorithm is a stateless algorithm. As a result, + the reacting node does not guarantee that there will be an + absolute reduction in traffic sent. Rather, it guarantees that + the requested percentage of new requests will be given abatement + treatment. + + If the reacting node comes out of the 100% traffic reduction + (meaning, it has received an OLR indicating that no traffic should be + sent, as a result of the overload report timing out), the reacting + node sending the traffic SHOULD be conservative and, for example, + first send "probe" messages to learn the overload condition of the + overloaded node before converging to any traffic amount/rate decided + by the sender. Similar concerns apply in all cases when the overload + report times out, unless the previous overload report stated 0% + reduction. + + + + + +Korhonen, et al. Standards Track [Page 24] + +RFC 7683 DOIC October 2015 + + + Note: The goal of this behavior is to reduce the probability of + overload condition thrashing where an immediate transition from + 100% reduction to 0% reduction results in the reporting node + moving quickly back into an overload condition. + +7. Attribute Value Pairs + + This section describes the encoding and semantics of the Diameter + Overload Indication Attribute Value Pairs (AVPs) defined in this + document. + + Refer to Section 4 of [RFC6733] for more information on AVPs and AVP + data types. + +7.1. OC-Supported-Features AVP + + The OC-Supported-Features AVP (AVP Code 621) is of type Grouped and + serves two purposes. First, it announces a node's support for the + DOIC solution in general. Second, it contains the description of the + supported DOIC features of the sending node. The OC-Supported- + Features AVP MUST be included in every Diameter request message a + DOIC supporting node sends. + + OC-Supported-Features ::= < AVP Header: 621 > + [ OC-Feature-Vector ] + * [ AVP ] + +7.2. OC-Feature-Vector AVP + + The OC-Feature-Vector AVP (AVP Code 622) is of type Unsigned64 and + contains a 64-bit flags field of announced capabilities of a DOIC + node. The value of zero (0) is reserved. + + The OC-Feature-Vector sub-AVP is used to announce the DOIC features + supported by the DOIC node, in the form of a flag-bits field in which + each bit announces one feature or capability supported by the node. + The absence of the OC-Feature-Vector AVP in request messages + indicates that only the default traffic abatement algorithm described + in this specification is supported. The absence of the OC-Feature- + Vector AVP in answer messages indicates that the default traffic + abatement algorithm described in this specification is selected + (while other traffic abatement algorithms may be supported), and no + features other than abatement algorithms are supported. + + + + + + + + +Korhonen, et al. Standards Track [Page 25] + +RFC 7683 DOIC October 2015 + + + The following capability is defined in this document: + + OLR_DEFAULT_ALGO (0x0000000000000001) + + When this flag is set by the a DOIC reacting node, it means that + the default traffic abatement (loss) algorithm is supported. When + this flag is set by a DOIC reporting node, it means that the loss + algorithm will be used for requested overload abatement. + +7.3. OC-OLR AVP + + The OC-OLR AVP (AVP Code 623) is of type Grouped and contains the + information necessary to convey an overload report on an overload + condition at the reporting node. The application the OC-OLR AVP + applies to is identified by the Application-ID found in the Diameter + message header. The host or realm the OC-OLR AVP concerns is + determined from the Origin-Host AVP and/or Origin-Realm AVP found in + the encapsulating Diameter command. The OC-OLR AVP is intended to be + sent only by a reporting node. + + OC-OLR ::= < AVP Header: 623 > + < OC-Sequence-Number > + < OC-Report-Type > + [ OC-Reduction-Percentage ] + [ OC-Validity-Duration ] + * [ AVP ] + +7.4. OC-Sequence-Number AVP + + The OC-Sequence-Number AVP (AVP Code 624) is of type Unsigned64. Its + usage in the context of overload control is described in Section 5.2. + + From the functionality point of view, the OC-Sequence-Number AVP is + used as a nonvolatile increasing counter for a sequence of overload + reports between two DOIC nodes for the same overload occurrence. + Sequence numbers are treated in a unidirectional manner, i.e., two + sequence numbers in each direction between two DOIC nodes are not + related or correlated. + +7.5. OC-Validity-Duration AVP + + The OC-Validity-Duration AVP (AVP Code 625) is of type Unsigned32 and + indicates in seconds the validity time of the overload report. The + number of seconds is measured after reception of the first OC-OLR AVP + with a given value of OC-Sequence-Number AVP. The default value for + the OC-Validity-Duration AVP is 30 seconds. When the OC-Validity- + Duration AVP is not present in the OC-OLR AVP, the default value + applies. The maximum value for the OC-Validity-Duration AVP is + + + +Korhonen, et al. Standards Track [Page 26] + +RFC 7683 DOIC October 2015 + + + 86,400 seconds (24 hours). If the value received in the OC-Validity- + Duration is greater than the maximum value, then the default value + applies. + +7.6. OC-Report-Type AVP + + The OC-Report-Type AVP (AVP Code 626) is of type Enumerated. The + value of the AVP describes what the overload report concerns. The + following values are initially defined: + + HOST_REPORT 0 + The overload report is for a host. Overload abatement treatment + applies to host-routed requests. + + REALM_REPORT 1 + The overload report is for a realm. Overload abatement treatment + applies to realm-routed requests. + + The values 2-4294967295 are unassigned. + +7.7. OC-Reduction-Percentage AVP + + The OC-Reduction-Percentage AVP (AVP Code 627) is of type Unsigned32 + and describes the percentage of the traffic that the sender is + requested to reduce, compared to what it otherwise would send. The + OC-Reduction-Percentage AVP applies to the default (loss) algorithm + specified in this specification. However, the AVP can be reused for + future abatement algorithms, if its semantics fit into the new + algorithm. + + The value of the Reduction-Percentage AVP is between zero (0) and one + hundred (100). Values greater than 100 are ignored. The value of + 100 means that all traffic is to be throttled, i.e., the reporting + node is under a severe load and ceases to process any new messages. + The value of 0 means that the reporting node is in a stable state and + has no need for the reacting node to apply any traffic abatement. + + + + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 27] + +RFC 7683 DOIC October 2015 + + +7.8. AVP Flag Rules + + +---------+ + |AVP flag | + |rules | + +----+----+ + AVP Section | |MUST| + Attribute Name Code Defined Value Type |MUST| NOT| + +--------------------------------------------------+----+----+ + |OC-Supported-Features 621 7.1 Grouped | | V | + +--------------------------------------------------+----+----+ + |OC-Feature-Vector 622 7.2 Unsigned64 | | V | + +--------------------------------------------------+----+----+ + |OC-OLR 623 7.3 Grouped | | V | + +--------------------------------------------------+----+----+ + |OC-Sequence-Number 624 7.4 Unsigned64 | | V | + +--------------------------------------------------+----+----+ + |OC-Validity-Duration 625 7.5 Unsigned32 | | V | + +--------------------------------------------------+----+----+ + |OC-Report-Type 626 7.6 Enumerated | | V | + +--------------------------------------------------+----+----+ + |OC-Reduction | | | + | -Percentage 627 7.7 Unsigned32 | | V | + +--------------------------------------------------+----+----+ + + As described in the Diameter base protocol [RFC6733], the M-bit usage + for a given AVP in a given command may be defined by the application. + +8. Error Response Codes + + When a DOIC node rejects a Diameter request due to overload, the DOIC + node MUST select an appropriate error response code. This + determination is made based on the probability of the request + succeeding if retried on a different path. + + Note: This only applies for DOIC nodes that are not the originator + of the request. + + A reporting node rejecting a Diameter request due to an overload + condition SHOULD send a DIAMETER_TOO_BUSY error response, if it can + assume that the same request may succeed on a different path. + + If a reporting node knows or assumes that the same request will not + succeed on a different path, the DIAMETER_UNABLE_TO_COMPLY error + response SHOULD be used. Retrying would consume valuable resources + during an occurrence of overload. + + + + + +Korhonen, et al. Standards Track [Page 28] + +RFC 7683 DOIC October 2015 + + + For instance, if the request arrived at the reporting node without + a Destination-Host AVP, then the reporting node might determine + that there is an alternative Diameter node that could successfully + process the request and that retrying the transaction would not + negatively impact the reporting node. DIAMETER_TOO_BUSY would be + sent in this case. + + If the request arrived at the reporting node with a Destination- + Host AVP populated with its own Diameter identity, then the + reporting node can assume that retrying the request would result + in it coming to the same reporting node. + DIAMETER_UNABLE_TO_COMPLY would be sent in this case. + + A second example is when an agent that supports the DOIC solution + is performing the role of a reacting node for a non-supporting + client. Requests that are rejected as a result of DOIC throttling + by the agent in this scenario would generally be rejected with a + DIAMETER_UNABLE_TO_COMPLY response code. + +9. IANA Considerations + +9.1. AVP Codes + + New AVPs defined by this specification are listed in Section 7. All + AVP codes are allocated from the "AVP Codes" sub-registry under the + "Authentication, Authorization, and Accounting (AAA) Parameters" + registry. + +9.2. New Registries + + Two new registries have been created in the "AVP Specific Values" + sub-registry under the "Authentication, Authorization, and Accounting + (AAA) Parameters" registry. + + A new "OC-Feature-Vector AVP Values (code 622)" registry has been + created. This registry contains the following: + + Feature Vector Value Name + + Feature Vector Value + + Specification defining the new value + + See Section 7.2 for the initial Feature Vector Value in the registry. + This specification defines the value. New values can be added to the + registry using the Specification Required policy [RFC5226]. + + + + + +Korhonen, et al. Standards Track [Page 29] + +RFC 7683 DOIC October 2015 + + + A new "OC-Report-Type AVP Values (code 626)" registry has been + created. This registry contains the following: + + Report Type Value Name + + Report Type Value + + Specification defining the new value + + See Section 7.6 for the initial assignment in the registry. New + types can be added using the Specification Required policy [RFC5226]. + +10. Security Considerations + + DOIC gives Diameter nodes the ability to request that downstream + nodes send fewer Diameter requests. Nodes do this by exchanging + overload reports that directly effect this reduction. This exchange + is potentially subject to multiple methods of attack and has the + potential to be used as a denial-of-service (DoS) attack vector. For + instance, a series of injected realm OLRs with a requested reduction + percentage of 100% could be used to completely eliminate any traffic + from being sent to that realm. + + Overload reports may contain information about the topology and + current status of a Diameter network. This information is + potentially sensitive. Network operators may wish to control + disclosure of overload reports to unauthorized parties to avoid their + use for competitive intelligence or to target attacks. + + Diameter does not include features to provide end-to-end + authentication, integrity protection, or confidentiality. This may + cause complications when sending overload reports between non- + adjacent nodes. + +10.1. Potential Threat Modes + + The Diameter protocol involves transactions in the form of requests + and answers exchanged between clients and servers. These clients and + servers may be peers, that is, they may share a direct transport + (e.g., TCP or SCTP) connection, or the messages may traverse one or + more intermediaries, known as Diameter Agents. Diameter nodes use + TLS, DTLS, or IPsec to authenticate peers and to provide + confidentiality and integrity protection of traffic between peers. + Nodes can make authorization decisions based on the peer identities + authenticated at the transport layer. + + + + + + +Korhonen, et al. Standards Track [Page 30] + +RFC 7683 DOIC October 2015 + + + When agents are involved, this presents an effectively transitive + trust model. That is, a Diameter client or server can authorize an + agent for certain actions, but it must trust that agent to make + appropriate authorization decisions about its peers, and so on. + Since confidentiality and integrity protection occur at the transport + layer, agents can read, and perhaps modify, any part of a Diameter + message, including an overload report. + + There are several ways an attacker might attempt to exploit the + overload control mechanism. An unauthorized third party might inject + an overload report into the network. If this third party is upstream + of an agent, and that agent fails to apply proper authorization + policies, downstream nodes may mistakenly trust the report. This + attack is at least partially mitigated by the assumption that nodes + include overload reports in Diameter answers but not in requests. + This requires an attacker to have knowledge of the original request + in order to construct an answer. Such an answer would also need to + arrive at a Diameter node via a protected transport connection. + Therefore, implementations MUST validate that an answer containing an + overload report is a properly constructed response to a pending + request prior to acting on the overload report, and that the answer + was received via an appropriate transport connection. + + A similar attack involves a compromised but otherwise authorized node + that sends an inappropriate overload report. For example, a server + for the realm "example.com" might send an overload report indicating + that a competitor's realm "example.net" is overloaded. If other + nodes act on the report, they may falsely believe that "example.net" + is overloaded, effectively reducing that realm's capacity. + Therefore, it's critical that nodes validate that an overload report + received from a peer actually falls within that peer's responsibility + before acting on the report or forwarding the report to other peers. + For example, an overload report from a peer that applies to a realm + not handled by that peer is suspect. This may require out-of-band, + non-Diameter agreements and/or mechanisms. + + This attack is partially mitigated by the fact that the + application, as well as host and realm, for a given OLR is + determined implicitly by respective AVPs in the enclosing answer. + If a reporting node modifies any of those AVPs, the enclosing + transaction will also be affected. + +10.2. Denial-of-Service Attacks + + Diameter overload reports, especially realm reports, can cause a node + to cease sending some or all Diameter requests for an extended + period. This makes them a tempting vector for DoS attacks. + Furthermore, since Diameter is almost always used in support of other + + + +Korhonen, et al. Standards Track [Page 31] + +RFC 7683 DOIC October 2015 + + + protocols, a DoS attack on Diameter is likely to impact those + protocols as well. In the worst case, where the Diameter application + is being used for access control into an IP network, a coordinated + DoS attack could result in the blockage of all traffic into that + network. Therefore, Diameter nodes MUST NOT honor or forward OLRs + received from peers that are not trusted to send them. + + An attacker might use the information in an OLR to assist in DoS + attacks. For example, an attacker could use information about + current overload conditions to time an attack for maximum effect, or + use subsequent overload reports as a feedback mechanism to learn the + results of a previous or ongoing attack. Operators need the ability + to ensure that OLRs are not leaked to untrusted parties. + +10.3. Noncompliant Nodes + + In the absence of an overload control mechanism, Diameter nodes need + to implement strategies to protect themselves from floods of + requests, and to make sure that a disproportionate load from one + source does not prevent other sources from receiving service. For + example, a Diameter server might throttle a certain percentage of + requests from sources that exceed certain limits. Overload control + can be thought of as an optimization for such strategies, where + downstream nodes never send the excess requests in the first place. + However, the presence of an overload control mechanism does not + remove the need for these other protection strategies. + + When a Diameter node sends an overload report, it cannot assume that + all nodes will comply, even if they indicate support for DOIC. A + noncompliant node might continue to send requests with no reduction + in load. Such noncompliance could be done accidentally or + maliciously to gain an unfair advantage over compliant nodes. + Requirement 28 in [RFC7068] indicates that the overload control + solution cannot assume that all Diameter nodes in a network are + trusted. It also requires that malicious nodes not be allowed to + take advantage of the overload control mechanism to get more than + their fair share of service. + +10.4. End-to-End Security Issues + + The lack of end-to-end integrity features makes it difficult to + establish trust in overload reports received from non-adjacent nodes. + Any agents in the message path may insert or modify overload reports. + Nodes must trust that their adjacent peers perform proper checks on + overload reports from their peers, and so on, creating a transitive- + trust requirement extending for potentially long chains of nodes. + Network operators must determine if this transitive trust requirement + is acceptable for their deployments. Nodes supporting Diameter + + + +Korhonen, et al. Standards Track [Page 32] + +RFC 7683 DOIC October 2015 + + + overload control MUST give operators the ability to select which + peers are trusted to deliver overload reports and whether they are + trusted to forward overload reports from non-adjacent nodes. DOIC + nodes MUST strip DOIC AVPs from messages received from peers that are + not trusted for DOIC purposes. + + The lack of end-to-end confidentiality protection means that any + Diameter Agent in the path of an overload report can view the + contents of that report. In addition to the requirement to select + which peers are trusted to send overload reports, operators MUST be + able to select which peers are authorized to receive reports. A node + MUST NOT send an overload report to a peer not authorized to receive + it. Furthermore, an agent MUST remove any overload reports that + might have been inserted by other nodes before forwarding a Diameter + message to a peer that is not authorized to receive overload reports. + + A DOIC node cannot always automatically detect that a peer also + supports DOIC. For example, a node might have a peer that is a + non-supporting agent. If nodes on the other side of that agent + send OC-Supported-Features AVPs, the agent is likely to forward + them as unknown AVPs. Messages received across the non-supporting + agent may be indistinguishable from messages received across a + DOIC supporting agent, giving the false impression that the non- + supporting agent actually supports DOIC. This complicates the + transitive-trust nature of DOIC. Operators need to be careful to + avoid situations where a non-supporting agent is mistakenly + trusted to enforce DOIC-related authorization policies. + + It is expected that work on end-to-end Diameter security might make + it easier to establish trust in non-adjacent nodes for overload + control purposes. Readers should be reminded, however, that the + overload control mechanism allows Diameter Agents to modify AVPs in, + or insert additional AVPs into, existing messages that are originated + by other nodes. If end-to-end security is enabled, there is a risk + that such modification could violate integrity protection. The + details of using any future Diameter end-to-end security mechanism + with overload control will require careful consideration, and are + beyond the scope of this document. + + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 33] + +RFC 7683 DOIC October 2015 + + +11. References + +11.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + <http://www.rfc-editor.org/info/rfc2119>. + + [RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an + IANA Considerations Section in RFCs", BCP 26, RFC 5226, + DOI 10.17487/RFC5226, May 2008, + <http://www.rfc-editor.org/info/rfc5226>. + + [RFC6733] Fajardo, V., Ed., Arkko, J., Loughney, J., and G. Zorn, + Ed., "Diameter Base Protocol", RFC 6733, + DOI 10.17487/RFC6733, October 2012, + <http://www.rfc-editor.org/info/rfc6733>. + +11.2. Informative References + + [Cx] 3GPP, "Cx and Dx interfaces based on the Diameter + protocol; Protocol details", 3GPP TS 29.229 12.7.0, + September 2015. + + [PCC] 3GPP, "Policy and charging control architecture", 3GPP + TS 23.203 12.10.0, September 2015. + + [RFC4006] Hakala, H., Mattila, L., Koskinen, J-P., Stura, M., and J. + Loughney, "Diameter Credit-Control Application", RFC 4006, + DOI 10.17487/RFC4006, August 2005, + <http://www.rfc-editor.org/info/rfc4006>. + + [RFC7068] McMurry, E. and B. Campbell, "Diameter Overload Control + Requirements", RFC 7068, DOI 10.17487/RFC7068, November + 2013, <http://www.rfc-editor.org/info/rfc7068>. + + [S13] 3GPP, "Evolved Packet System (EPS); Mobility Management + Entity (MME) and Serving GPRS Support Node (SGSN) related + interfaces based on Diameter protocol", 3GPP TS 29.272 + 12.8.0, September 2015. + + + + + + + + + + +Korhonen, et al. Standards Track [Page 34] + +RFC 7683 DOIC October 2015 + + +Appendix A. Issues Left for Future Specifications + + The base solution for overload control does not cover all possible + use cases. A number of solution aspects were intentionally left for + future specification and protocol work. The following subsections + define some of the potential extensions to the DOIC solution. + +A.1. Additional Traffic Abatement Algorithms + + This specification describes only means for a simple loss-based + algorithm. Future algorithms can be added using the designed + solution extension mechanism. The new algorithms need to be + registered with IANA. See Sections 7.2 and 9 for the required IANA + steps. + +A.2. Agent Overload + + This specification focuses on Diameter endpoint (server or client) + overload. A separate extension will be required to outline the + handling of the case of agent overload. + +A.3. New Error Diagnostic AVP + + This specification indicates the use of existing error messages when + nodes reject requests due to overload. There is an expectation that + additional error codes or AVPs will be defined in a separate + specification to indicate that overload was the reason for the + rejection of the message. + +Appendix B. Deployment Considerations + + Non-supporting Agents + + Due to the way that realm-routed requests are handled in Diameter + networks with the server selection for the request done by an + agent, network operators should enable DOIC at agents that perform + server selection first. + + Topology-Hiding Interactions + + There exist proxies that implement what is referred to as Topology + Hiding. This can include cases where the agent modifies the + Origin-Host in answer messages. The behavior of the DOIC solution + is not well understood when this happens. As such, the DOIC + solution does not address this scenario. + + + + + + +Korhonen, et al. Standards Track [Page 35] + +RFC 7683 DOIC October 2015 + + + Inter-Realm/Administrative Domain Considerations + + There are likely to be special considerations for handling DOIC + signaling across administrative boundaries. This includes + considerations for whether or not information included in the DOIC + signaling should be sent across those boundaries. In addition, + consideration should be taken as to whether or not a reacting node + in one realm can be trusted to implement the requested overload + abatement handling for overload reports received from a separately + administered realm. + +Appendix C. Considerations for Applications Integrating the DOIC + Solution + + This section outlines considerations to be taken into account when + integrating the DOIC solution into Diameter applications. + +C.1. Application Classification + + The following is a classification of Diameter applications and + request types. This discussion is meant to document factors that + play into decisions made by the Diameter entity responsible for + handling overload reports. + + Section 8.1 of [RFC6733] defines two state machines that imply two + types of applications, session-less and session-based applications. + The primary difference between these types of applications is the + lifetime of Session-Ids. + + For session-based applications, the Session-Id is used to tie + multiple requests into a single session. + + The Credit-Control application defined in [RFC4006] is an example of + a Diameter session-based application. + + In session-less applications, the lifetime of the Session-Id is a + single Diameter transaction, i.e., the session is implicitly + terminated after a single Diameter transaction and a new Session-Id + is generated for each Diameter request. + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 36] + +RFC 7683 DOIC October 2015 + + + For the purposes of this discussion, session-less applications are + further divided into two types of applications: + + Stateless Applications: + + Requests within a stateless application have no relationship to + each other. The 3GPP-defined S13 application is an example of a + stateless application [S13], where only a Diameter command is + defined between a client and a server and no state is maintained + between two consecutive transactions. + + Pseudo-Session Applications: + + Applications that do not rely on the Session-Id AVP for + correlation of application messages related to the same session + but use other session-related information in the Diameter requests + for this purpose. The 3GPP-defined Cx application [Cx] is an + example of a pseudo-session application. + + The handling of overload reports must take the type of application + into consideration, as discussed in Appendix C.2. + +C.2. Implications of Application Type Overload + + This section discusses considerations for mitigating overload + reported by a Diameter entity. This discussion focuses on the type + of application. Appendix C.3 discusses considerations for handling + various request types when the target server is known to be in an + overloaded state. + + These discussions assume that the strategy for mitigating the + reported overload is to reduce the overall workload sent to the + overloaded entity. The concept of applying overload treatment to + requests targeted for an overloaded Diameter entity is inherent to + this discussion. The method used to reduce offered load is not + specified here, but it could include routing requests to another + Diameter entity known to be able to handle them, or it could mean + rejecting certain requests. For a Diameter Agent, rejecting requests + will usually mean generating appropriate Diameter error responses. + For a Diameter client, rejecting requests will depend upon the + application. For example, it could mean giving an indication to the + entity requesting the Diameter service that the network is busy and + to try again later. + + + + + + + + +Korhonen, et al. Standards Track [Page 37] + +RFC 7683 DOIC October 2015 + + + Stateless Applications: + + By definition, there is no relationship between individual + requests in a stateless application. As a result, when a request + is sent or relayed to an overloaded Diameter entity -- either a + Diameter Server or a Diameter Agent -- the sending or relaying + entity can choose to apply the overload treatment to any request + targeted for the overloaded entity. + + Pseudo-session Applications: + + For pseudo-session applications, there is an implied ordering of + requests. As a result, decisions about which requests towards an + overloaded entity to reject could take the command code of the + request into consideration. This generally means that + transactions later in the sequence of transactions should be given + more favorable treatment than messages earlier in the sequence. + This is because more work has already been done by the Diameter + network for those transactions that occur later in the sequence. + Rejecting them could result in increasing the load on the network + as the transactions earlier in the sequence might also need to be + repeated. + + Session-Based Applications: + + Overload handling for session-based applications must take into + consideration the work load associated with setting up and + maintaining a session. As such, the entity sending requests + towards an overloaded Diameter entity for a session-based + application might tend to reject new session requests prior to + rejecting intra-session requests. In addition, session-ending + requests might be given a lower probability of being rejected, as + rejecting session-ending requests could result in session status + being out of sync between the Diameter clients and servers. + Application designers that would decide to reject mid-session + requests will need to consider whether the rejection invalidates + the session and any resulting session cleanup procedures. + +C.3. Request Transaction Classification + + Independent Request: + + An independent request is not correlated to any other requests, + and, as such, the lifetime of the Session-Id is constrained to an + individual transaction. + + + + + + +Korhonen, et al. Standards Track [Page 38] + +RFC 7683 DOIC October 2015 + + + Session-Initiating Request: + + A session-initiating request is the initial message that + establishes a Diameter session. The ACR message defined in + [RFC6733] is an example of a session-initiating request. + + Correlated Session-Initiating Request: + + There are cases when multiple session-initiated requests must be + correlated and managed by the same Diameter server. It is notably + the case in the 3GPP Policy and Charging Control (PCC) + architecture [PCC], where multiple apparently independent Diameter + application sessions are actually correlated and must be handled + by the same Diameter server. + + Intra-session Request: + + An intra-session request is a request that uses the same Session- + Id as the one used in a previous request. An intra-session + request generally needs to be delivered to the server that handled + the session-creating request for the session. The STR message + defined in [RFC6733] is an example of an intra-session request. + + Pseudo-session Requests: + + Pseudo-session requests are independent requests and do not use + the same Session-Id but are correlated by other session-related + information contained in the request. There exist Diameter + applications that define an expected ordering of transactions. + This sequencing of independent transactions results in a pseudo- + session. The AIR, MAR, and SAR requests in the 3GPP-defined Cx + [Cx] application are examples of pseudo-session requests. + +C.4. Request Type Overload Implications + + The request classes identified in Appendix C.3 have implications on + decisions about which requests should be throttled first. The + following list of request treatments regarding throttling is provided + as guidelines for application designers when implementing the + Diameter overload control mechanism described in this document. The + exact behavior regarding throttling is a matter of local policy, + unless specifically defined for the application. + + Independent Requests: + + Independent requests can generally be given equal treatment when + making throttling decisions, unless otherwise indicated by + application requirements or local policy. + + + +Korhonen, et al. Standards Track [Page 39] + +RFC 7683 DOIC October 2015 + + + Session-Initiating Requests: + + Session-initiating requests often represent more work than + independent or intra-session requests. Moreover, session- + initiating requests are typically followed by other session- + related requests. Since the main objective of overload control is + to reduce the total number of requests sent to the overloaded + entity, throttling decisions might favor allowing intra-session + requests over session-initiating requests. In the absence of + local policies or application-specific requirements to the + contrary, individual session-initiating requests can be given + equal treatment when making throttling decisions. + + Correlated Session-Initiating Requests: + + A request that results in a new binding; where the binding is used + for routing of subsequent session-initiating requests to the same + server, it represents more work load than other requests. As + such, these requests might be throttled more frequently than other + request types. + + Pseudo-session Requests: + + Throttling decisions for pseudo-session requests can take into + consideration where individual requests fit into the overall + sequence of requests within the pseudo-session. Requests that are + earlier in the sequence might be throttled more aggressively than + requests that occur later in the sequence. + + Intra-session Requests: + + There are two types of intra-sessions requests, requests that + terminate a session and the remainder of intra-session requests. + Implementers and operators may choose to throttle session- + terminating requests less aggressively in order to gracefully + terminate sessions, allow cleanup of the related resources (e.g., + session state), and avoid the need for additional intra-session + requests. Favoring session termination requests may reduce the + session management impact on the overloaded entity. The default + handling of other intra-session requests might be to treat them + equally when making throttling decisions. There might also be + application-level considerations whether some request types are + favored over others. + + + + + + + + +Korhonen, et al. Standards Track [Page 40] + +RFC 7683 DOIC October 2015 + + +Contributors + + The following people contributed substantial ideas, feedback, and + discussion to this document: + + o Eric McMurry + + o Hannes Tschofenig + + o Ulrich Wiehe + + o Jean-Jacques Trottin + + o Maria Cruz Bartolome + + o Martin Dolly + + o Nirav Salot + + o Susan Shishufeng + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 41] + +RFC 7683 DOIC October 2015 + + +Authors' Addresses + + Jouni Korhonen (editor) + Broadcom Corporation + 3151 Zanker Road + San Jose, CA 95134 + United States + + Email: [email protected] + + + Steve Donovan (editor) + Oracle + 7460 Warren Parkway + Frisco, Texas 75034 + United States + + Email: [email protected] + + + Ben Campbell + Oracle + 7460 Warren Parkway + Frisco, Texas 75034 + United States + + Email: [email protected] + + + Lionel Morand + Orange Labs + 38/40 rue du General Leclerc + Issy-Les-Moulineaux Cedex 9 92794 + France + + Phone: +33145296257 + Email: [email protected] + + + + + + + + + + + + + + +Korhonen, et al. Standards Track [Page 42] + diff --git a/lib/diameter/examples/code/client.erl b/lib/diameter/examples/code/client.erl index 6fb90b1c09..0864919cdd 100644 --- a/lib/diameter/examples/code/client.erl +++ b/lib/diameter/examples/code/client.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2015. All Rights Reserved. +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ -module(client). -include_lib("diameter/include/diameter.hrl"). --include_lib("diameter/include/diameter_gen_base_rfc6733.hrl"). -export([start/1, %% start a service start/2, %% @@ -71,6 +70,7 @@ {'Product-Name', "Client"}, {'Auth-Application-Id', [0]}, {string_decode, false}, + {decode_format, map}, {application, [{alias, common}, {dictionary, diameter_gen_base_rfc6733}, {module, client_cb}]}]). @@ -108,9 +108,9 @@ connect(T) -> call(Name) -> SId = diameter:session_id(?L(Name)), - RAR = #diameter_base_RAR{'Session-Id' = SId, - 'Auth-Application-Id' = 0, - 'Re-Auth-Request-Type' = 0}, + RAR = ['RAR' | #{'Session-Id' => SId, + 'Auth-Application-Id' => 0, + 'Re-Auth-Request-Type' => 0}], diameter:call(Name, common, RAR, []). call() -> diff --git a/lib/diameter/examples/code/client_cb.erl b/lib/diameter/examples/code/client_cb.erl index ed1d3b9b7b..af2d4d6da7 100644 --- a/lib/diameter/examples/code/client_cb.erl +++ b/lib/diameter/examples/code/client_cb.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2016. All Rights Reserved. +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -55,21 +55,18 @@ prepare_request(#diameter_packet{msg = ['RAR' = T | Avps]}, _, {_, Caps}) -> origin_realm = {OR, DR}} = Caps, - {send, [T, {'Origin-Host', OH}, - {'Origin-Realm', OR}, - {'Destination-Host', DH}, - {'Destination-Realm', DR} - | Avps]}; - -prepare_request(#diameter_packet{msg = Rec}, _, {_, Caps}) -> - #diameter_caps{origin_host = {OH, DH}, - origin_realm = {OR, DR}} - = Caps, - - {send, Rec#diameter_base_RAR{'Origin-Host' = OH, - 'Origin-Realm' = OR, - 'Destination-Host' = DH, - 'Destination-Realm' = DR}}. + {send, [T | if is_map(Avps) -> + Avps#{'Origin-Host' => OH, + 'Origin-Realm' => OR, + 'Destination-Host' => DH, + 'Destination-Realm' => DR}; + is_list(Avps) -> + [{'Origin-Host', OH}, + {'Origin-Realm', OR}, + {'Destination-Host', DH}, + {'Destination-Realm', DR} + | Avps] + end]}. %% prepare_retransmit/3 diff --git a/lib/diameter/examples/code/node.erl b/lib/diameter/examples/code/node.erl index 246be4194b..fc5830f8e2 100644 --- a/lib/diameter/examples/code/node.erl +++ b/lib/diameter/examples/code/node.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2015. All Rights Reserved. +%% Copyright Ericsson AB 2010-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -30,6 +30,8 @@ connect/2, stop/1]). +-export([message/3]). + -type protocol() :: tcp | sctp. @@ -128,6 +130,8 @@ stop(Name) -> server_opts({T, Addr, Port}) -> [{transport_module, tmod(T)}, {transport_config, [{reuseaddr, true}, + {sender, true}, + {message_cb, [fun ?MODULE:message/3, 0]}, {ip, addr(Addr)}, {port, Port}]}]; @@ -173,3 +177,26 @@ addr(loopback) -> {127,0,0,1}; addr(A) -> A. + +%% --------------------------------------------------------------------------- + +%% message/3 +%% +%% Simple message callback that limits the number of concurrent +%% requests on the peer connection in question. + +%% Incoming request. +message(recv, <<_:32, 1:1, _/bits>> = Bin, N) -> + [Bin, N < 32, fun ?MODULE:message/3, N+1]; + +%% Outgoing request. +message(ack, <<_:32, 1:1, _/bits>>, _) -> + []; + +%% Incoming answer or request discarded. +message(ack, _, N) -> + [N =< 32, fun ?MODULE:message/3, N-1]; + +%% Outgoing message or incoming answer. +message(_, Bin, _) -> + [Bin]. diff --git a/lib/diameter/include/diameter_gen.hrl b/lib/diameter/include/diameter_gen.hrl index fb6370fe54..548763ec7d 100644 --- a/lib/diameter/include/diameter_gen.hrl +++ b/lib/diameter/include/diameter_gen.hrl @@ -26,13 +26,13 @@ %% encode_avps/3 -encode_avps(Name, Vals, Opts) -> - diameter_gen:encode_avps(Name, Vals, Opts#{module => ?MODULE}). +encode_avps(Name, Avps, Opts) -> + diameter_gen:encode_avps(Name, Avps, Opts#{module => ?MODULE}). %% decode_avps/2 -decode_avps(Name, Recs, Opts) -> - diameter_gen:decode_avps(Name, Recs, Opts#{module => ?MODULE}). +decode_avps(Name, Avps, Opts) -> + diameter_gen:decode_avps(Name, Avps, Opts#{module => ?MODULE}). %% avp/5 diff --git a/lib/diameter/src/Makefile b/lib/diameter/src/Makefile index 6bf748a727..3af856f63e 100644 --- a/lib/diameter/src/Makefile +++ b/lib/diameter/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2010-2016. All Rights Reserved. +# Copyright Ericsson AB 2010-2017. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -274,9 +274,7 @@ gen/diameter_gen_base_accounting.erl gen/diameter_gen_base_accounting.hrl: \ gen/diameter_gen_acct_rfc6733.erl gen/diameter_gen_acct_rfc6733.hrl: \ $(EBIN)/diameter_gen_base_rfc6733.$(EMULATOR) -gen/diameter_gen_relay.erl gen/diameter_gen_relay.hrl \ -gen/diameter_gen_base_rfc3588.erl gen/diameter_gen_base_rfc3588.hrl \ -gen/diameter_gen_base_rfc6733.erl gen/diameter_gen_base_rfc6733.hrl: \ +$(DICT_ERLS) $(DICT_HRLS): \ $(COMPILER_MODULES:%=$(EBIN)/%.$(EMULATOR)) $(DICT_MODULES:gen/%=$(EBIN)/%.$(EMULATOR)): \ diff --git a/lib/diameter/src/base/diameter.erl b/lib/diameter/src/base/diameter.erl index bd92e16fba..b90b794611 100644 --- a/lib/diameter/src/base/diameter.erl +++ b/lib/diameter/src/base/diameter.erl @@ -46,7 +46,10 @@ -export([start/0, stop/0]). --export_type([evaluable/0, +-export_type([eval/0, + evaluable/0, %% deprecated + decode_format/0, + strict_arities/0, restriction/0, message_length/0, remotes/0, @@ -299,7 +302,7 @@ call(SvcName, App, Message) -> | realm | {host, any|'DiameterIdentity'()} | {realm, any|'DiameterIdentity'()} - | {eval, evaluable()} + | {eval, eval()} | {neg, peer_filter()} | {all, [peer_filter()]} | {any, [peer_filter()]}. @@ -307,10 +310,13 @@ call(SvcName, App, Message) -> -opaque peer_ref() :: pid(). --type evaluable() +-type eval() :: {module(), atom(), list()} | fun() - | maybe_improper_list(evaluable(), list()). + | maybe_improper_list(eval(), list()). + +-type evaluable() + :: eval(). -type sequence() :: {'Unsigned32'(), 0..32}. @@ -320,29 +326,61 @@ call(SvcName, App, Message) -> | node | nodes | [node()] - | evaluable(). + | eval(). -type remotes() :: boolean() | [node()] - | evaluable(). + | eval(). -type message_length() :: 0..16#FFFFFF. +-type decode_format() + :: record + | list + | map + | none + | record_from_map. + +-type strict_arities() + :: false + | encode + | decode. + +%% Options common to both start_service/2 and add_transport/2. + +-type common_opt() + :: {pool_size, pos_integer()} + | {capabilities_cb, eval()} + | {capx_timeout, 'Unsigned32'()} + | {strict_capx, boolean()} + | {strict_mbit, boolean()} + | {avp_dictionaries, [module()]} + | {disconnect_cb, eval()} + | {dpr_timeout, 'Unsigned32'()} + | {dpa_timeout, 'Unsigned32'()} + | {incoming_maxlen, message_length()} + | {length_errors, exit | handle | discard} + | {connect_timer, 'Unsigned32'()} + | {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}} + | {watchdog_config, [{okay|suspect, non_neg_integer()}]} + | {spawn_opt, list()}. + %% Options passed to start_service/2 -type service_opt() :: capability() | {application, [application_opt()]} | {restrict_connections, restriction()} - | {sequence, sequence() | evaluable()} + | {sequence, sequence() | eval()} | {share_peers, remotes()} + | {decode_format, decode_format()} + | {traffic_counters, boolean()} | {string_decode, boolean()} - | {strict_mbit, boolean()} - | {incoming_maxlen, message_length()} + | {strict_arities, true | strict_arities()} | {use_shared_peers, remotes()} - | {spawn_opt, list()}. + | common_opt(). -type application_opt() :: {alias, app_alias()} @@ -372,20 +410,9 @@ call(SvcName, App, Message) -> :: {transport_module, atom()} | {transport_config, any()} | {transport_config, any(), 'Unsigned32'() | infinity} - | {pool_size, pos_integer()} | {applications, [app_alias()]} | {capabilities, [capability()]} - | {capabilities_cb, evaluable()} - | {capx_timeout, 'Unsigned32'()} - | {capx_strictness, boolean()} - | {disconnect_cb, evaluable()} - | {dpr_timeout, 'Unsigned32'()} - | {dpa_timeout, 'Unsigned32'()} - | {length_errors, exit | handle | discard} - | {connect_timer, 'Unsigned32'()} - | {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}} - | {watchdog_config, [{okay|suspect, non_neg_integer()}]} - | {spawn_opt, list()} + | common_opt() | {private, any()}. %% Predicate passed to remove_transport/2 diff --git a/lib/diameter/src/base/diameter_callback.erl b/lib/diameter/src/base/diameter_callback.erl index f9cdc66c70..d04a416bef 100644 --- a/lib/diameter/src/base/diameter_callback.erl +++ b/lib/diameter/src/base/diameter_callback.erl @@ -26,16 +26,16 @@ %% as the Diameter application callback in question. The record has %% one field for each callback function as well as 'default' and %% 'extra' fields. A function-specific field can be set to a -%% diameter:evaluable() in order to redirect the callback +%% diameter:eval() in order to redirect the callback %% corresponding to that field, or to 'false' to request the default %% callback implemented in this module. If neither of these fields are %% set then the 'default' field determines the form of the callback: a %% module name results in the usual callback as if the module had been -%% configured directly as the callback module, a diameter_evaluable() +%% configured directly as the callback module, a diameter_eval() %% in a callback applied to the atom-valued callback name and argument %% list. For all callbacks not to this module, the 'extra' field is a %% list of additional arguments, following arguments supplied by -%% diameter but preceding those of the diameter:evaluable() being +%% diameter but preceding those of the diameter:eval() being %% applied. %% %% For example, the following config to diameter:start_service/2, in diff --git a/lib/diameter/src/base/diameter_codec.erl b/lib/diameter/src/base/diameter_codec.erl index 82fa796e69..2dd2c906a2 100644 --- a/lib/diameter/src/base/diameter_codec.erl +++ b/lib/diameter/src/base/diameter_codec.erl @@ -29,7 +29,7 @@ msg_name/2, msg_id/1]). -%% Towards generated encoders (from diameter_gen.hrl). +%% towards diameter_gen -export([pack_data/2, pack_avp/2]). @@ -110,7 +110,7 @@ encode(Mod, Opts, Msg) -> enc(_, Opts, #diameter_packet{msg = [#diameter_header{} = Hdr | As]} = Pkt) -> - try encode_avps(reorder(As), Opts) of + try encode_avps(As, Opts) of Avps -> Bin = list_to_binary(Avps), Len = 20 + size(Bin), @@ -206,51 +206,12 @@ values(Avps) -> %% Message as a list of #diameter_avp{} ... encode_avps(_, _, [#diameter_avp{} | _] = Avps, Opts) -> - encode_avps(reorder(Avps), Opts); + encode_avps(Avps, Opts); %% ... or as a tuple list or record. encode_avps(Mod, MsgName, Values, Opts) -> Mod:encode_avps(MsgName, Values, Opts). -%% reorder/1 -%% -%% Reorder AVPs for the relay case using the index field of -%% diameter_avp records. Decode populates this field in collect_avps -%% and presents AVPs in reverse order. A relay then sends the reversed -%% list with a Route-Record AVP prepended. The goal here is just to do -%% lists:reverse/1 in Grouped AVPs and the outer list, but only in the -%% case there are indexed AVPs at all, so as not to reverse lists that -%% have been explicilty sent (unindexed, in the desired order) as a -%% diameter_avp list. The effect is the same as lists:keysort/2, but -%% only on the cases we expect, not a general sort. - -reorder(Avps) -> - case reorder(Avps, []) of - false -> - Avps; - Sorted -> - Sorted - end. - -%% reorder/3 - -%% In case someone has reversed the list already. (Not likely.) -reorder([#diameter_avp{index = 0} | _] = Avps, Acc) -> - Avps ++ Acc; - -%% Assume indexed AVPs are in reverse order. -reorder([#diameter_avp{index = N} = A | Avps], Acc) - when is_integer(N) -> - lists:reverse(Avps, [A | Acc]); - -%% An unindexed AVP. -reorder([H | T], Acc) -> - reorder(T, [H | Acc]); - -%% No indexed members. -reorder([], _) -> - false. - %% encode_avps/2 encode_avps(Avps, Opts) -> @@ -287,7 +248,8 @@ rec2msg(Mod, Rec) -> %% longer *the* decode. decode(Mod, Pkt) -> - Opts = #{string_decode => true, + Opts = #{decode_format => record, + string_decode => true, strict_mbit => true, rfc => 6733}, decode(Mod, Opts, Pkt). @@ -326,13 +288,7 @@ decode(Mod, AppMod, Opts, Pkt) -> %% Relay application: just extract the avp's without any decoding of %% their data since we don't know the application in question. decode(?APP_ID_RELAY, _, _, _, #diameter_packet{} = Pkt) -> - case collect_avps(Pkt) of - {E, As} -> - Pkt#diameter_packet{avps = As, - errors = [E]}; - As -> - Pkt#diameter_packet{avps = As} - end; + collect_avps(Pkt); %% Otherwise decode using the dictionary. decode(_, Mod, AppMod, Opts, #diameter_packet{header = Hdr} = Pkt) -> @@ -341,44 +297,50 @@ decode(_, Mod, AppMod, Opts, #diameter_packet{header = Hdr} = Pkt) -> is_error = IsError} = Hdr, - MsgName = if IsError andalso not IsRequest -> + MsgName = if IsError, not IsRequest -> 'answer-message'; true -> Mod:msg_name(CmdCode, IsRequest) end, - decode_avps(MsgName, Mod, AppMod, Opts, Pkt, collect_avps(Pkt)); + decode_avps(MsgName, Mod, AppMod, Opts, Pkt); decode(Id, Mod, AppMod, Opts, Bin) when is_binary(Bin) -> decode(Id, Mod, AppMod, Opts, #diameter_packet{header = decode_header(Bin), bin = Bin}). -%% decode_avps/6 - -decode_avps(MsgName, Mod, AppMod, Opts, Pkt, {E, Avps}) -> - ?LOG(invalid_avp_length, Pkt#diameter_packet.header), - #diameter_packet{errors = Failed} - = P - = decode_avps(MsgName, Mod, AppMod, Opts, Pkt, Avps), - P#diameter_packet{errors = [E | Failed]}; +%% decode_avps/5 -decode_avps('', _, _, _, Pkt, Avps) -> %% unknown message ... - ?LOG(unknown_message, Pkt#diameter_packet.header), - Pkt#diameter_packet{avps = lists:reverse(Avps), - errors = [3001]}; %% DIAMETER_COMMAND_UNSUPPORTED +decode_avps('', _, _, _, #diameter_packet{header = H, %% unknown message + bin = Bin} + = Pkt) -> + ?LOG(unknown_message, H), + Pkt#diameter_packet{avps = collect_avps(Bin), + errors = [3001]}; %% DIAMETER_COMMAND_UNSUPPORTED %% msg = undefined identifies this case. -decode_avps(MsgName, Mod, AppMod, Opts, Pkt, Avps) -> %% ... or not +decode_avps(MsgName, Mod, AppMod, Opts, #diameter_packet{bin = Bin} = Pkt) -> + {_, Avps} = split_binary(Bin, 20), {Rec, As, Errors} = Mod:decode_avps(MsgName, Avps, - Opts#{dictionary => AppMod, + Opts#{app_dictionary => AppMod, failed_avp => false}), ?LOGC([] /= Errors, decode_errors, Pkt#diameter_packet.header), - Pkt#diameter_packet{msg = Rec, + Pkt#diameter_packet{msg = reformat(MsgName, Rec, Opts), errors = Errors, avps = As}. +%% reformat/3 + +reformat(MsgName, Avps, #{decode_format := T}) + when T == map; + T == list -> + [MsgName | Avps]; + +reformat(_, Msg, _) -> + Msg. + %%% --------------------------------------------------------------------------- %%% # decode_header/1 %%% --------------------------------------------------------------------------- @@ -515,24 +477,21 @@ msg_id(<<_:32, Rbit:1, _:7, CmdCode:24, ApplId:32, _/binary>>) -> %%% # collect_avps/1 %%% --------------------------------------------------------------------------- -%% Note that the returned list of AVP's is reversed relative to their -%% order in the binary. Note also that grouped avp's aren't unraveled, -%% only those at the top level. +%% This is only used for the relay decode. Note that grouped avp's +%% aren't unraveled, only those at the top level. --spec collect_avps(#diameter_packet{} | binary()) - -> [Avp] - | {Error, [Avp]} - when Avp :: #diameter_avp{}, - Error :: {5014, #diameter_avp{}}. +-spec collect_avps(#diameter_packet{}) + -> #diameter_packet{}; + (binary()) + -> [#diameter_avp{}]. -collect_avps(#diameter_packet{bin = <<_:20/binary, Avps/binary>>}) -> - collect_avps(Avps, 0, []); +collect_avps(#diameter_packet{bin = Bin} = Pkt) -> + Pkt#diameter_packet{avps = collect_avps(Bin)}; -collect_avps(Bin) - when is_binary(Bin) -> - collect_avps(Bin, 0, []). +collect_avps(<<_:20/binary, Avps/binary>>) -> + collect(Avps). -%% collect_avps/3 +%% collect/1 %% 0 1 2 3 %% 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 @@ -546,66 +505,48 @@ collect_avps(Bin) %% | Data ... %% +-+-+-+-+-+-+-+-+ -collect_avps(<<Code:32, V:1, M:1, P:1, _:5, Len:24, I:V/unit:32, Rest/binary>>, - N, - Acc) -> - collect_avps(Code, - if 1 == V -> I; 0 == V -> undefined end, - 1 == M, - 1 == P, - Len - 8 - V*4, %% Might be negative, which ensures - ?PAD(Len), %% failure of the Data match below. - Rest, - N, - Acc); - -collect_avps(<<>>, _, Acc) -> - Acc; +collect(<<Code:32, V:1, M:1, P:1, _:5, Len:24, I:V/unit:32, Rest/binary>>) -> + collect(Rest, + Code, + if 1 == V -> I; 0 == V -> undefined end, + Len - 8 - V*4, %% Might be negative, which ensures + ?PAD(Len), %% failure of the match below. + 1 == M, + 1 == P); + +collect(<<>>) -> + []; %% Header is truncated. pack_avp/1 will pad this at encode if sent in %% a Failed-AVP. -collect_avps(Bin, _, Acc) -> - {{5014, #diameter_avp{data = Bin}}, Acc}. +collect(Bin) -> + [#diameter_avp{data = {5014, Bin}}]. -%% collect_avps/9 +%% collect/7 -%% Duplicate the diameter_avp creation in each branch below to avoid -%% modifying the record, which profiling has shown to be a relatively -%% costly part of building the list. - -collect_avps(Code, VendorId, M, P, Len, Pad, Rest, N, Acc) -> - case Rest of - <<Data:Len/binary, _:Pad/binary, T/binary>> -> +collect(Bin, Code, Vid, DataLen, Pad, M, P) -> + case Bin of + <<Data:DataLen/binary, _:Pad/binary, Rest/binary>> -> Avp = #diameter_avp{code = Code, - vendor_id = VendorId, + vendor_id = Vid, is_mandatory = M, need_encryption = P, - data = Data, - index = N}, - collect_avps(T, N+1, [Avp | Acc]); + data = Data}, + [Avp | collect(Rest)]; _ -> %% Length in header points past the end of the message, or - %% doesn't span the header. As stated in the 6733 text - %% above, it's sufficient to return a zero-filled minimal - %% payload if this is a request. Do this (in cases that we - %% know the type) by inducing a decode failure and letting - %% the dictionary's decode (in diameter_gen) deal with it. - %% - %% Note that the extra bit can only occur in the trailing - %% AVP of a message or Grouped AVP, since a faulty AVP - %% Length is otherwise indistinguishable from a correct - %% one here, as we don't know the types of the AVPs being - %% extracted. - Avp = #diameter_avp{code = Code, - vendor_id = VendorId, - is_mandatory = M, - need_encryption = P, - data = {5014, Rest}, - index = N}, - [Avp | Acc] + %% doesn't span the header. Note that an length error can + %% only occur in the trailing AVP of a message or Grouped + %% AVP, since a faulty AVP Length is otherwise + %% indistinguishable from a correct one here, as we don't + %% know the types of the AVPs being extracted. + [#diameter_avp{code = Code, + vendor_id = Vid, + is_mandatory = M, + need_encryption = P, + data = {5014, Bin}}] end. - %% 3588: %% %% DIAMETER_INVALID_AVP_LENGTH 5014 @@ -673,8 +614,8 @@ pack_avp(#diameter_avp{data = {T, {Type, Value}}}, Opts) -> pack_avp(#diameter_avp{data = {T, Data}}, _) -> pack_data(T, Data); -pack_avp(#diameter_avp{data = {Dict, Name, Data}}, Opts) -> - pack_data(Dict:avp_header(Name), Dict:avp(encode, Data, Name, Opts)); +pack_avp(#diameter_avp{data = {Dict, Name, Value}}, Opts) -> + pack_data(Dict:avp_header(Name), Dict:avp(encode, Value, Name, Opts)); %% ... with a truncated header ... pack_avp(#diameter_avp{code = undefined, data = B}, _) diff --git a/lib/diameter/src/base/diameter_config.erl b/lib/diameter/src/base/diameter_config.erl index 34018ae6d3..90a9282349 100644 --- a/lib/diameter/src/base/diameter_config.erl +++ b/lib/diameter/src/base/diameter_config.erl @@ -102,9 +102,6 @@ -record(monitor, {mref = make_ref() :: reference(), service}). %% name -%% The default sequence mask. --define(NOMASK, {0,32}). - %% Time to lay low before restarting a dead service. -define(RESTART_SLEEP, 2000). @@ -560,87 +557,186 @@ add(SvcName, Type, Opts0) -> end. transport_opts(Opts) -> - lists:map(fun topt/1, Opts). + [setopt(transport, T) || T <- Opts]. + +%% setopt/2 -topt(T) -> - case opt(T) of +setopt(K, T) -> + case opt(K, T) of {value, X} -> X; true -> T; false -> - ?THROW({invalid, T}) + ?THROW({invalid, T}); + {error, Reason} -> + ?THROW({invalid, T, Reason}) end. -opt({transport_module, M}) -> +%% opt/2 + +opt(_, {incoming_maxlen, N}) -> + is_integer(N) andalso 0 =< N andalso N < 1 bsl 24; + +opt(service, {K, B}) + when K == string_decode; + K == traffic_counters -> + is_boolean(B); + +opt(service, {K, false}) + when K == share_peers; + K == use_shared_peers; + K == monitor; + K == restrict_connections; + K == strict_arities -> + true; + +opt(service, {K, true}) + when K == share_peers; + K == use_shared_peers; + K == strict_arities -> + true; + +opt(service, {decode_format, T}) + when T == record; + T == list; + T == map; + T == none; + T == record_from_map -> + true; + +opt(service, {strict_arities, T}) + when T == encode; + T == decode -> + true; + +opt(service, {restrict_connections, T}) + when T == node; + T == nodes -> + true; + +opt(service, {K, T}) + when (K == share_peers + orelse K == use_shared_peers + orelse K == restrict_connections), ([] == T + orelse is_atom(hd(T))) -> + true; + +opt(service, {monitor, P}) -> + is_pid(P); + +opt(service, {K, F}) + when K == restrict_connections; + K == share_peers; + K == use_shared_peers -> + try diameter_lib:eval(F) of %% but no guarantee that it won't fail later + Nodes -> + is_list(Nodes) orelse {error, Nodes} + catch + E:R -> + {error, {E, R, ?STACK}} + end; + +opt(service, {sequence, {H,N}}) -> + 0 =< N andalso N =< 32 + andalso is_integer(H) + andalso 0 =< H + andalso 0 == H bsr (32-N); + +opt(service = S, {sequence = K, F}) -> + try diameter_lib:eval(F) of + {_,_} = T -> + KT = {K,T}, + opt(S, KT) andalso {value, KT}; + V -> + {error, V} + catch + E:R -> + {error, {E, R, ?STACK}} + end; + +opt(transport, {transport_module, M}) -> is_atom(M); -opt({transport_config, _, Tmo}) -> +opt(transport, {transport_config, _, Tmo}) -> ?IS_UINT32(Tmo) orelse Tmo == infinity; -opt({applications, As}) -> +opt(transport, {applications, As}) -> is_list(As); -opt({capabilities, Os}) -> - is_list(Os) andalso ok == encode_CER(Os); +opt(transport, {capabilities, Os}) -> + is_list(Os) andalso try ok = encode_CER(Os), true + catch ?FAILURE(No) -> {error, No} + end; -opt({K, Tmo}) +opt(_, {K, Tmo}) when K == capx_timeout; K == dpr_timeout; K == dpa_timeout -> ?IS_UINT32(Tmo); -opt({capx_strictness, B}) -> +opt(_, {capx_strictness, B}) -> + is_boolean(B) andalso {value, {strict_capx, B}}; +opt(_, {K, B}) + when K == strict_capx; + K == strict_mbit -> is_boolean(B); -opt({length_errors, T}) -> +opt(_, {avp_dictionaries, Mods}) -> + is_list(Mods) andalso lists:all(fun erlang:is_atom/1, Mods); + +opt(_, {length_errors, T}) -> lists:member(T, [exit, handle, discard]); -opt({K, Tmo}) - when K == reconnect_timer; %% deprecated - K == connect_timer -> +opt(transport, {reconnect_timer, Tmo}) -> %% deprecated + ?IS_UINT32(Tmo) andalso {value, {connect_timer, Tmo}}; +opt(_, {connect_timer, Tmo}) -> ?IS_UINT32(Tmo); -opt({watchdog_timer, {M,F,A}}) +opt(_, {watchdog_timer, {M,F,A}}) when is_atom(M), is_atom(F), is_list(A) -> true; -opt({watchdog_timer, Tmo}) -> +opt(_, {watchdog_timer, Tmo}) -> ?IS_UINT32(Tmo); -opt({watchdog_config, L}) -> - is_list(L) andalso lists:all(fun wdopt/1, L); +opt(_, {watchdog_config, L}) -> + is_list(L) andalso lists:all(fun wd/1, L); -opt({spawn_opt, {M,F,A}}) +opt(_, {spawn_opt, {M,F,A}}) when is_atom(M), is_atom(F), is_list(A) -> true; -opt({spawn_opt = K, Opts}) -> +opt(_, {spawn_opt = K, Opts}) -> if is_list(Opts) -> {value, {K, spawn_opts(Opts)}}; true -> false end; -opt({pool_size, N}) -> +opt(_, {pool_size, N}) -> is_integer(N) andalso 0 < N; -%% Options that we can't validate. -opt({K, _}) +%% Options we can't validate. +opt(_, {K, _}) + when K == disconnect_cb; + K == capabilities_cb -> + true; +opt(transport, {K, _}) when K == transport_config; - K == capabilities_cb; - K == disconnect_cb; K == private -> true; -%% Anything else, which is ignored by us. This makes options sensitive -%% to spelling mistakes but arbitrary options are passed by some users -%% as a way to identify transports. (That is, can't just do away with -%% it.) -opt(_) -> - true. +%% Anything else, which is ignored in transport config. This makes +%% options sensitive to spelling mistakes, but arbitrary options are +%% passed by some users as a way to identify transports so can't just +%% do away with it. +opt(K, _) -> + K == transport. + +%% wd/1 -wdopt({K,N}) -> +wd({K,N}) -> (K == okay orelse K == suspect) andalso is_integer(N) andalso 0 =< N; -wdopt(_) -> +wd(_) -> false. %% start_transport/2 @@ -705,16 +801,7 @@ make_config(SvcName, Opts) -> ok = encode_CER(CapOpts), - SvcOpts = make_opts((Opts -- AppOpts) -- CapOpts, - [{false, share_peers}, - {false, use_shared_peers}, - {false, monitor}, - {?NOMASK, sequence}, - {nodes, restrict_connections}, - {16#FFFFFF, incoming_maxlen}, - {true, strict_mbit}, - {true, string_decode}, - {[], spawn_opt}]), + SvcOpts = service_opts((Opts -- AppOpts) -- CapOpts), D = proplists:get_value(string_decode, SvcOpts, true), @@ -728,98 +815,22 @@ binary_caps(Caps, true) -> binary_caps(Caps, false) -> diameter_capx:binary_caps(Caps). -%% make_opts/2 - -make_opts(Opts, Defs) -> - Known = [{K, get_opt(K, Opts, D)} || {D,K} <- Defs], - Unknown = Opts -- Known, - - [] == Unknown orelse ?THROW({invalid, hd(Unknown)}), +%% service_opts/1 - [{K, opt(K,V)} || {K,V} <- Known]. - -opt(incoming_maxlen, N) - when 0 =< N, N < 1 bsl 24 -> - N; - -opt(spawn_opt, {M,F,A} = T) - when is_atom(M), is_atom(F), is_list(A) -> - T; - -opt(spawn_opt, L) - when is_list(L) -> - spawn_opts(L); - -opt(K, false = B) - when K == share_peers; - K == use_shared_peers; - K == monitor; - K == restrict_connections; - K == strict_mbit; - K == string_decode -> - B; - -opt(K, true = B) - when K == share_peers; - K == use_shared_peers; - K == strict_mbit; - K == string_decode -> - B; - -opt(restrict_connections, T) - when T == node; - T == nodes -> - T; - -opt(K, T) - when (K == share_peers - orelse K == use_shared_peers - orelse K == restrict_connections), ([] == T - orelse is_atom(hd(T))) -> - T; - -opt(monitor, P) - when is_pid(P) -> - P; - -opt(K, F) - when K == restrict_connections; - K == share_peers; - K == use_shared_peers -> - try diameter_lib:eval(F) of %% but no guarantee that it won't fail later - Nodes when is_list(Nodes) -> - F; - V -> - ?THROW({value, {K,V}}) - catch - E:R -> - ?THROW({value, {K, E, R, ?STACK}}) - end; - -opt(sequence, {_,_} = T) -> - sequence(T); - -opt(sequence = K, F) -> - try diameter_lib:eval(F) of - T -> sequence(T) - catch - E:R -> - ?THROW({value, {K, E, R, ?STACK}}) - end; - -opt(K, _) -> - ?THROW({value, K}). +service_opts(Opts) -> + Res = [setopt(service, T) || T <- Opts], + Keys = sets:to_list(sets:from_list([K || {K,_} <- Res])), %% unique + Dups = lists:foldl(fun(K,A) -> lists:keydelete(K, 1, A) end, Res, Keys), + [] == Dups orelse ?THROW({duplicate, Dups}), + Res. +%% Reject duplicates on a service, but not on a transport. There's no +%% particular reason for the inconsistency, but the historic behaviour +%% ignores all but the first of a transport_opt(), and there's no real +%% reason to change it. spawn_opts(L) -> [T || T <- L, T /= link, T /= monitor]. -sequence({H,N} = T) - when 0 =< N, N =< 32, 0 =< H, 0 == H bsr (32-N) -> - T; - -sequence(_) -> - ?THROW({value, sequence}). - make_caps(Caps, Opts) -> case diameter_capx:make_caps(Caps, Opts) of {ok, T} -> diff --git a/lib/diameter/src/base/diameter_gen.erl b/lib/diameter/src/base/diameter_gen.erl index e832832876..d3b9f704fe 100644 --- a/lib/diameter/src/base/diameter_gen.erl +++ b/lib/diameter/src/base/diameter_gen.erl @@ -26,6 +26,14 @@ -module(diameter_gen). +-compile({inline, [incr/8, + incr/4, + field/1, + setopts/4, + avp_arity/5, + set_failed/2, + set_strict/3]}). + -export([encode_avps/3, decode_avps/3, grouped_avp/4, @@ -37,7 +45,7 @@ -define(THROW(T), throw({?MODULE, T})). -type parent_name() :: atom(). %% parent = Message or AVP --type parent_record() :: tuple(). %% +-type parent_record() :: tuple() | avp_values() | map(). -type avp_name() :: atom(). -type avp_record() :: tuple(). -type avp_values() :: [{avp_name(), term()}]. @@ -46,17 +54,21 @@ -type grouped_avp() :: nonempty_improper_list(#diameter_avp{}, [avp()]). -type avp() :: non_grouped_avp() | grouped_avp(). +%% The arbitrary arity returned from dictionary avp_arity functions. +-define(ANY, {0, '*'}). + %% --------------------------------------------------------------------------- %% # encode_avps/3 %% --------------------------------------------------------------------------- --spec encode_avps(parent_name(), parent_record() | avp_values(), map()) +-spec encode_avps(parent_name(), parent_record(), map()) -> iolist() | no_return(). encode_avps(Name, Vals, #{module := Mod} = Opts) -> + Strict = mget(strict_arities, Opts, encode), try - encode(Name, Vals, Opts, Mod) + encode(Name, Vals, Opts, Strict, Mod) catch throw: {?MODULE, Reason} -> diameter_lib:log({encode, error}, @@ -73,128 +85,340 @@ encode_avps(Name, Vals, #{module := Mod} = Opts) -> erlang:error({encode_failure, Reason, Name, Stack}) end. -%% encode/4 +%% encode/5 -encode(Name, Vals, #{ordered_encode := false} = Opts, Mod) +encode(Name, Vals, Opts, Strict, Mod) when is_list(Vals) -> - lists:map(fun({F,V}) -> encode(Name, F, V, Opts, Mod) end, Vals); + case Opts of + #{ordered_encode := false} -> + lists:map(fun({F,V}) -> encode(Name, F, V, Opts, Strict, Mod) end, + Vals); + _ -> + Rec = Mod:'#set-'(Vals, newrec(Mod, Name)), + encode(Name, Rec, Opts, Strict, Mod) + end; -encode(Name, Vals, Opts, Mod) - when is_list(Vals) -> - encode(Name, Mod:'#set-'(Vals, newrec(Mod, Name)), Opts, Mod); +encode(Name, Map, Opts, Strict, Mod) + when is_map(Map) -> + [enc(F, A, V, Opts, Strict, Mod) || {F,A} <- Mod:avp_arity(Name), + V <- [mget(F, Map, undefined)]]; -encode(Name, Rec, Opts, Mod) -> - [encode(Name, F, V, Opts, Mod) || {F,V} <- Mod:'#get-'(Rec)]. +encode(Name, Rec, Opts, Strict, Mod) -> + [encode(Name, F, V, Opts, Strict, Mod) || {F,V} <- Mod:'#get-'(Rec)]. -%% encode/5 +%% encode/6 + +encode(_, AvpName, Values, Opts, Strict, Mod) + when Strict /= encode -> + enc(AvpName, ?ANY, Values, Opts, Strict, Mod); -encode(Name, AvpName, Values, Opts, Mod) -> - enc(Name, AvpName, Mod:avp_arity(Name, AvpName), Values, Opts, Mod). +encode(Name, AvpName, Values, Opts, Strict, Mod) -> + Arity = Mod:avp_arity(Name, AvpName), + enc(AvpName, Arity, Values, Opts, Strict, Mod). %% enc/6 -enc(_, AvpName, 1, undefined, _, _) -> +enc(AvpName, Arity, Values, Opts, Strict, Mod) + when Strict /= encode, Arity /= ?ANY -> + enc(AvpName, ?ANY, Values, Opts, Strict, Mod); + +enc(AvpName, 1, undefined, _, _, _) -> ?THROW([mandatory_avp_missing, AvpName]); -enc(Name, AvpName, 1, Value, Opts, Mod) -> - enc(Name, AvpName, [Value], Opts, Mod); +enc(AvpName, 1, Value, Opts, _, Mod) -> + H = avp_header(AvpName, Mod), + enc(AvpName, H, Value, Opts, Mod); + +enc(_, {0,_}, [], _, _, _) -> + []; + +enc(_, _, undefined, _, _, _) -> + []; + +%% Be forgiving when a list of values is expected. If the value itself +%% is a list then the user has to wrap it to avoid each member from +%% being interpreted as an individual AVP value. +enc(AvpName, Arity, V, Opts, Strict, Mod) + when not is_list(V) -> + enc(AvpName, Arity, [V], Opts, Strict, Mod); -enc(_, _, {0,_}, [], _, _) -> +enc(AvpName, {Min, Max}, Values, Opts, Strict, Mod) -> + H = avp_header(AvpName, Mod), + enc(AvpName, H, Min, 0, Max, Values, Opts, Strict, Mod). + +%% enc/9 + +enc(AvpName, H, Min, N, Max, Vs, Opts, Strict, Mod) + when Strict /= encode; + Max == '*', Min =< N -> + [enc(AvpName, H, V, Opts, Mod) || V <- Vs]; + +enc(AvpName, _, Min, N, _, [], _, _, _) + when N < Min -> + ?THROW([repeated_avp_insufficient_arity, AvpName, Min, N]); + +enc(_, _, _, _, _, [], _, _, _) -> []; -enc(_, AvpName, _, T, _, _) - when not is_list(T) -> - ?THROW([repeated_avp_as_non_list, AvpName, T]); +enc(AvpName, _, _, N, Max, _, _, _, _) + when Max =< N -> + ?THROW([repeated_avp_excessive_arity, AvpName, Max]); + +enc(AvpName, H, Min, N, Max, [V|Vs], Opts, Strict, Mod) -> + [enc(AvpName, H, V, Opts, Mod) + | enc(AvpName, H, Min, N+1, Max, Vs, Opts, Strict, Mod)]. -enc(_, AvpName, {Min, _}, L, _, _) - when length(L) < Min -> - ?THROW([repeated_avp_insufficient_arity, AvpName, Min, L]); +%% avp_header/2 -enc(_, AvpName, {_, Max}, L, _, _) - when Max < length(L) -> - ?THROW([repeated_avp_excessive_arity, AvpName, Max, L]); +avp_header('AVP', _) -> + false; -enc(Name, AvpName, _, Values, Opts, Mod) -> - enc(Name, AvpName, Values, Opts, Mod). +avp_header(AvpName, Mod) -> + {_,_,_} = Mod:avp_header(AvpName). %% enc/5 -enc(Name, 'AVP', Values, Opts, Mod) -> - [enc_AVP(Name, A, Opts, Mod) || A <- Values]; +enc('AVP', false, Value, Opts, Mod) -> + enc_AVP(Value, Opts, Mod); -enc(_, AvpName, Values, Opts, Mod) -> - enc(AvpName, Values, Opts, Mod). +enc(AvpName, Hdr, Value, Opts, Mod) -> + enc1(AvpName, Hdr, Value, Opts, Mod). -%% enc/4 +%% enc1/5 -enc(AvpName, Values, Opts, Mod) -> - H = Mod:avp_header(AvpName), - [diameter_codec:pack_data(H, Mod:avp(encode, V, AvpName, Opts)) - || V <- Values]. +enc1(AvpName, {_,_,_} = Hdr, Value, Opts, Mod) -> + diameter_codec:pack_data(Hdr, Mod:avp(encode, Value, AvpName, Opts)). -%% enc_AVP/4 +%% enc1/6 + +enc1(AvpName, {_,_,_} = Hdr, Value, Opts, Mod, Dict) -> + diameter_codec:pack_data(Hdr, avp(encode, Value, AvpName, Opts, Mod, Dict)). + +%% enc_AVP/3 %% No value: assume AVP data is already encoded. The normal case will %% be when this is passed back from #diameter_packet.errors as a %% consequence of a failed decode. Any AVP can be encoded this way %% however, which side-steps any arity checks for known AVP's and %% could potentially encode something unfortunate. -enc_AVP(_, #diameter_avp{value = undefined} = A, Opts, _) -> +enc_AVP(#diameter_avp{value = undefined} = A, Opts, _) -> diameter_codec:pack_avp(A, Opts); -%% Missing name for value encode. -enc_AVP(_, #diameter_avp{name = N, value = V}, _, _) - when N == undefined; - N == 'AVP' -> - ?THROW([value_with_nameless_avp, N, V]); +%% Encode a name/value pair using an alternate dictionary if need be ... +enc_AVP(#diameter_avp{name = AvpName, value = Value}, Opts, Mod) -> + enc_AVP(AvpName, Value, Opts, Mod); +enc_AVP({AvpName, Value}, Opts, Mod) -> + enc_AVP(AvpName, Value, Opts, Mod); + +%% ... or with a specified dictionary. +enc_AVP({Dict, AvpName, Value}, Opts, Mod) -> + enc1(AvpName, Dict:avp_header(AvpName), Value, Opts, Mod, Dict). + +%% Don't guard against anything being sent as a generic 'AVP', which +%% allows arity restrictions to be abused. + +%% enc_AVP/4 + +enc_AVP(AvpName, Value, Opts, Mod) -> + try Mod:avp_header(AvpName) of + H -> + enc1(AvpName, H, Value, Opts, Mod) + catch + error: _ -> + Dicts = mget(avp_dictionaries, Opts, []), + enc_AVP(Dicts, AvpName, Value, Opts, Mod) + end. -%% Or not. Ensure that 'AVP' is the appropriate field. Note that if we -%% don't know this AVP at all then the encode will fail. -enc_AVP(Name, #diameter_avp{name = AvpName, value = Data}, Opts, Mod) -> - 0 == Mod:avp_arity(Name, AvpName) - orelse ?THROW([known_avp_as_AVP, Name, AvpName, Data]), - enc(AvpName, [Data], Opts, Mod); +%% enc_AVP/5 -%% The backdoor ... -enc_AVP(_, {AvpName, Value}, Opts, Mod) -> - enc(AvpName, [Value], Opts, Mod); +enc_AVP([Dict | Rest], AvpName, Value, Opts, Mod) -> + try Dict:avp_header(AvpName) of + H -> + enc1(AvpName, H, Value, Opts, Mod, Dict) + catch + error: _ -> + enc_AVP(Rest, AvpName, Value, Opts, Mod) + end; -%% ... and the side door. -enc_AVP(_Name, {_Dict, _AvpName, _Data} = T, Opts, _) -> - diameter_codec:pack_avp(#diameter_avp{data = T}, Opts). +enc_AVP([], AvpName, _, _, _) -> + ?THROW([no_dictionary, AvpName]). %% --------------------------------------------------------------------------- %% # decode_avps/3 %% --------------------------------------------------------------------------- --spec decode_avps(parent_name(), [#diameter_avp{}], map()) - -> {parent_record(), [avp()], Failed} +-spec decode_avps(parent_name(), binary(), map()) + -> {parent_record() | parent_name(), [avp()], Failed} when Failed :: [{5000..5999, #diameter_avp{}}]. -decode_avps(Name, Recs, #{module := Mod} = Opts) -> - {Avps, {Rec, Failed}} - = mapfoldl(fun(T,A) -> decode(Name, Opts, Mod, T, A) end, - {newrec(Mod, Name), []}, - Recs), - {Rec, Avps, Failed ++ missing(Rec, Name, Failed, Opts, Mod)}. -%% Append 5005 errors so that errors are reported in the order +decode_avps(Name, Bin, #{module := Mod, decode_format := Fmt} = Opts) -> + Strict = mget(strict_arities, Opts, decode), + [AM, Avps, Failed | Rec] + = decode(Bin, Name, Mod, Fmt, Strict, Opts, 0, #{}), + %% AM counts the number of top-level AVPs, which missing/5 then + %% uses when appending 5005 errors. + {reformat(Name, Rec, Strict, Mod, Fmt), + Avps, + Failed ++ missing(Name, Strict, Mod, Opts, AM)}. + +%% Append arity errors so that errors are reported in the order %% encountered. Failed-AVP should typically contain the first -%% encountered error accordg to the RFC. +%% error encountered. + +%% decode/8 + +decode(<<Code:32, V:1, M:1, P:1, _:5, Len:24, I:V/unit:32, Rest/binary>>, + Name, + Mod, + Fmt, + Strict, + Opts, + Idx, + AM) -> + decode(Rest, + Code, + if 1 == V -> I; true -> undefined end, + Len - 8 - 4*V, %% possibly negative, causing case match to fail + (4 - (Len rem 4)) rem 4, + 1 == M, + 1 == P, + Name, + Mod, + Fmt, + Strict, + Opts, + Idx, + AM); + +decode(<<>>, Name, Mod, Fmt, Strict, _, _, AM) -> + [AM, [], [] | newrec(Fmt, Mod, Name, Strict)]; + +decode(Bin, Name, Mod, Fmt, Strict, _, Idx, AM) -> + Avp = #diameter_avp{data = Bin, index = Idx}, + [AM, [Avp], [{5014, Avp}] | newrec(Fmt, Mod, Name, Strict)]. + +%% decode/14 + +decode(Bin, Code, Vid, DataLen, Pad, M, P, Name, Mod, Fmt, Strict, Opts0, + Idx, AM0) -> + case Bin of + <<Data:DataLen/binary, _:Pad/binary, T/binary>> -> + {NameT, Field, Arity, {I, AM}} + = incr(Name, Code, Vid, M, Mod, Strict, Opts0, AM0), + + Opts = setopts(NameT, Name, M, Opts0), + %% Not AvpName or else a failed Failed-AVP + %% decode is packed into 'AVP'. + + Avp = #diameter_avp{code = Code, + vendor_id = Vid, + is_mandatory = M, + need_encryption = P, + data = Data, + name = name(NameT), + type = type(NameT), + index = Idx}, + + Dec = dec(Data, Name, NameT, Mod, Fmt, Opts, Avp), + Acc = decode(T, Name, Mod, Fmt, Strict, Opts, Idx+1, AM),%% recurse + acc(Acc, Dec, I, Field, Arity, Strict, Mod, Opts); + _ -> + {NameT, _Field, _Arity, {_, AM}} + = incr(Name, Code, Vid, M, Mod, Strict, Opts0, AM0), + + Avp = #diameter_avp{code = Code, + vendor_id = Vid, + is_mandatory = M, + need_encryption = P, + data = Bin, + name = name(NameT), + type = type(NameT), + index = Idx}, + + [AM, [Avp], [{5014, Avp}] | newrec(Fmt, Mod, Name, Strict)] + end. + +%% incr/8 -%% mapfoldl/3 +incr(Name, Code, Vid, M, Mod, Strict, Opts, AM0) -> + NameT = Mod:avp_name(Code, Vid), %% {AvpName, Type} | 'AVP' + Field = field(NameT), %% AvpName | 'AVP' + Arity = avp_arity(Name, Field, Mod, Opts, M), + if 0 == Arity, 'AVP' /= Field -> + A = pack_arity(Name, Field, Opts, Mod, M), + {NameT, 'AVP', A, incr('AVP', A, Strict, AM0)}; + true -> + {NameT, Field, Arity, incr(Field, Arity, Strict, AM0)} + end. + +%% Data is a truncated header if command_code = undefined, otherwise +%% payload bytes. The former is padded to the length of a header if +%% the AVP reaches an outgoing encode. %% -%% Like lists:mapfoldl/3, but don't reverse the list. +%% RFC 6733 says that an AVP returned with 5014 can contain a minimal +%% payload for the AVP's type, but don't always know the type. -mapfoldl(F, Acc, List) -> - mapfoldl(F, Acc, List, []). +setopts('AVP', _, _, Opts) -> + Opts; -mapfoldl(F, Acc0, [T|Rest], List) -> - {B, Acc} = F(T, Acc0), - mapfoldl(F, Acc, Rest, [B|List]); -mapfoldl(_, Acc, [], List) -> - {List, Acc}. +setopts({_, Type}, Name, M, Opts) -> + set_failed(Name, set_strict(Type, M, Opts)). -%% 3588: +%% incr/4 + +incr(_, A, SA, AM) + when A == ?ANY; + A == 0; + SA /= decode -> + {undefined, AM}; + +incr(AvpName, _, _, AM) -> + case AM of + #{AvpName := N} -> + {N, AM#{AvpName => N+1}}; + _ -> + {0, AM#{AvpName => 1}} + end. + +%% mget/3 +%% +%% Measurably faster than maps:get/3. + +mget(Key, Map, Def) -> + case Map of + #{Key := V} -> + V; + _ -> + Def + end. + +%% name/1 + +name({Name, _}) -> + Name; +name(_) -> + undefined. + +%% type/1 + +type({_, Type}) -> + Type; +type(_) -> + undefined. + +%% missing/5 + +missing(Name, decode, Mod, Opts, AM) -> + [{5005, empty_avp(N, Opts, Mod)} || {N,A} <- Mod:avp_arity(Name), + N /= 'AVP', + Mn <- [min_arity(A)], + 0 < Mn, + mget(N, AM, 0) < Mn]; + +missing(_, _, _, _, _) -> + []. + +%% 3588/6733: %% %% DIAMETER_MISSING_AVP 5005 %% The request did not contain an AVP that is required by the Command @@ -204,57 +428,20 @@ mapfoldl(_, Acc, [], List) -> %% Vendor-Id if applicable. The value field of the missing AVP %% should be of correct minimum length and contain zeros. -missing(Rec, Name, Failed, Opts, Mod) -> - Avps = lists:foldl(fun({_, #diameter_avp{code = C, vendor_id = V}}, A) -> - maps:put({C,V}, true, A) - end, - maps:new(), - Failed), - missing(Mod:avp_arity(Name), tl(tuple_to_list(Rec)), Avps, Opts, Mod, []). - -missing([{Name, Arity} | As], [Value | Vs], Avps, Opts, Mod, Acc) -> - missing(As, - Vs, - Avps, - Opts, - Mod, - case - [H || missing_arity(Arity, Value), - {C,_,V} = H <- [Mod:avp_header(Name)], - not maps:is_key({C,V}, Avps)] - of - [H] -> - [{5005, empty_avp(Name, H, Opts, Mod)} | Acc]; - [] -> - Acc - end); - -missing([], [], _, _, _, Acc) -> - Acc. - -%% Maximum arities have already been checked in building the record. - -missing_arity(1, V) -> - V == undefined; -missing_arity({0, _}, _) -> - false; -missing_arity({1, _}, L) -> - [] == L; -missing_arity({Min, _}, L) -> - not has_prefix(Min, L). - -%% Compare a non-negative integer and the length of a list without -%% computing the length. -has_prefix(0, _) -> - true; -has_prefix(_, []) -> - false; -has_prefix(N, [_|L]) -> - has_prefix(N-1, L). +%% min_arity/1 + +min_arity(1) -> + 1; +min_arity({Mn,_}) -> + Mn. -%% empty_avp/4 +%% empty_avp/3 -empty_avp(Name, {Code, Flags, VId}, Opts, Mod) -> +empty_avp('AVP', _, _) -> + #diameter_avp{data = <<0:64>>}; + +empty_avp(Name, Opts, Mod) -> + {Code, Flags, VId} = Mod:avp_header(Name), {Name, Type} = Mod:avp_name(Code, VId), #diameter_avp{name = Name, code = Code, @@ -273,21 +460,19 @@ empty_avp(Name, {Code, Flags, VId}, Opts, Mod) -> %% specific errors that can be described by this AVP are described in %% the following section. -%% decode/5 +%% field/1 -decode(Name, - Opts, - Mod, - #diameter_avp{code = Code, vendor_id = Vid} - = Avp, - Acc) -> - decode(Name, Opts, Mod, Mod:avp_name(Code, Vid), Avp, Acc). +field({AvpName, _}) -> + AvpName; +field(_) -> + 'AVP'. -%% decode/6 +%% dec/7 -%% AVP not in dictionary. -decode(Name, Opts, Mod, 'AVP', Avp, Acc) -> - decode_AVP(Name, Avp, Opts, Mod, Acc); +%% AVP not in dictionary: try an alternate. + +dec(Data, Name, 'AVP', Mod, Fmt, Opts, Avp) -> + dec_AVP(dicts(Mod, Opts), Data, Name, Mod, Fmt, Opts, Avp); %% 6733, 4.4: %% @@ -336,130 +521,149 @@ decode(Name, Opts, Mod, 'AVP', Avp, Acc) -> %% defined the RFC's "unrecognized", which is slightly stronger than %% "not defined".) -decode(Name, Opts0, Mod, {AvpName, Type}, Avp, Acc) -> - #diameter_avp{data = Data, is_mandatory = M} - = Avp, +dec(Data, Name, {AvpName, Type}, Mod, Fmt, Opts, Avp) -> + #{app_dictionary := AppMod, failed_avp := Failed} + = Opts, - %% Whether or not to ignore an M-bit on an encapsulated AVP, or on - %% all AVPs with the service_opt() strict_mbit. - Opts1 = set_strict(Type, M, Opts0), + %% Reset the dictionary for best-effort decode of Failed-AVP. + Dict = if Failed -> AppMod; + true -> Mod + end, - %% Whether or not we're decoding within Failed-AVP and should - %% ignore decode errors. - #{dictionary := AppMod, failed_avp := Failed} - = Opts - = set_failed(Name, Opts1), %% Not AvpName or else a failed Failed-AVP - %% decode is packed into 'AVP'. + dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, Avp). - %% Reset the dictionary for best-effort decode of Failed-AVP. - DecMod = if Failed -> - AppMod; - true -> - Mod - end, - - %% On decode, a Grouped AVP is represented as a #diameter_avp{} - %% list with AVP as head and component AVPs as tail. On encode, - %% data can be a list of component AVPs. - - try avp_decode(Data, AvpName, Opts, DecMod, Mod) of - {Rec, As} when Type == 'Grouped' -> - A = Avp#diameter_avp{name = AvpName, - value = Rec, - type = Type}, - {[A|As], pack_avp(Name, A, Opts, Mod, Acc)}; +%% dicts/2 - V when Type /= 'Grouped' -> - A = Avp#diameter_avp{name = AvpName, - value = V, - type = Type}, - {A, pack_avp(Name, A, Opts, Mod, Acc)} - catch - throw: {?MODULE, {grouped, Error, ComponentAvps}} -> - decode_error(Name, - Error, - ComponentAvps, - Opts, - Mod, - Avp#diameter_avp{name = AvpName, - data = trim(Avp#diameter_avp.data), - type = Type}, - Acc); +dicts(Mod, #{app_dictionary := Mod, avp_dictionaries := Dicts}) -> + Dicts; + +dicts(_, #{app_dictionary := Dict, avp_dictionaries := Dicts}) -> + [Dict | Dicts]; +dicts(Mod, #{app_dictionary := Mod}) -> + []; + +dicts(_, #{app_dictionary := Dict}) -> + [Dict]. + +%% dec/10 + +dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, Avp) -> + try avp(decode, Data, AvpName, Opts, Mod, Dict) of + V -> + set(Type, Fmt, Avp, V) + catch + throw: {?MODULE, T} -> + decode_error(Failed, Fmt, T, Avp); error: Reason -> - decode_error(Name, - Reason, - Opts, - Mod, - Avp#diameter_avp{name = AvpName, - data = trim(Avp#diameter_avp.data), - type = Type}, - Acc) + decode_error(Failed, Reason, Name, Mod, Opts, Avp) end. -%% avp_decode/5 +%% dec_AVP/7 -avp_decode(Data, AvpName, Opts, Mod, Mod) -> - Mod:avp(decode, Data, AvpName, Opts); +dec_AVP([], _, _, _, _, _, Avp) -> + Avp; -avp_decode(Data, AvpName, Opts, Mod, _) -> - Mod:avp(decode, Data, AvpName, Opts, Mod). +dec_AVP(Dicts, Data, Name, Mod, Fmt, Opts, #diameter_avp{code = Code, + vendor_id = Vid} + = Avp) -> + dec_AVP(Dicts, Data, Name, Mod, Fmt, Opts, Code, Vid, Avp). -%% trim/1 +%% dec_AVP/9 %% -%% Remove any extra bit that was added in diameter_codec to induce a -%% 5014 error. +%% Try to decode an AVP in the first alternate dictionary that defines +%% it. + +dec_AVP([Dict | Rest], Data, Name, Mod, Fmt, Opts, Code, Vid, Avp) -> + case Dict:avp_name(Code, Vid) of + {AvpName, Type} -> + A = Avp#diameter_avp{name = AvpName, + type = Type}, + #{failed_avp := Failed} = Opts, + dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, A); + _ -> + dec_AVP(Rest, Data, Name, Mod, Fmt, Opts, Code, Vid, Avp) + end; -trim(#diameter_avp{data = Data} = Avp) -> - Avp#diameter_avp{data = trim(Data)}; +dec_AVP([], _, _, _, _, _, _, _, Avp) -> + Avp. -trim({5014, Bin}) -> - Bin; +%% set/4 +%% +%% A Grouped AVP is represented as a #diameter_avp{} list with AVP +%% as head and component AVPs as tail. -trim(Avps) - when is_list(Avps) -> - lists:map(fun trim/1, Avps); +set('Grouped', Fmt, Avp, V) -> + {Rec, As} = V, + [set(Fmt, Avp, Rec) | As]; -trim(Avp) -> - Avp. +set(_, _, Avp, V) -> + Avp#diameter_avp{value = V}. -%% decode_error/7 +%% decode_error/4 +%% +%% Error when decoding a grouped AVP. -decode_error(Name, [_ | Rec], _, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> - decode_AVP(Name, Avp#diameter_avp{value = Rec}, Opts, Mod, Acc); +%% Ignoring errors in Failed-AVP. +decode_error(true, Fmt, {Rec, ComponentAvps, _Errors}, Avp) -> + [set(Fmt, Avp, Rec) | ComponentAvps]; -decode_error(Name, _, _, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> - decode_AVP(Name, Avp, Opts, Mod, Acc); +%% Or not. A faulty component is encoded by itself in Failed-AVP, as +%% suggested by 7.5 of RFC 6733 (quoted below), so that errors are +%% reported unambigiously. +decode_error(false, _, {_, ComponentAvps, [{RC,A} | _]}, Avp) -> + {RC, [Avp | ComponentAvps], Avp#diameter_avp{data = [A]}}. -decode_error(_, [Error | _], ComponentAvps, _, _, Avp, Acc) -> - decode_error(Error, Avp, Acc, ComponentAvps); +%% set/3 -decode_error(_, Error, ComponentAvps, _, _, Avp, Acc) -> - decode_error(Error, Avp, Acc, ComponentAvps). +set(none, Avp, _Name) -> + Avp; +set(_, Avp, Rec) -> + Avp#diameter_avp{value = Rec}. -%% decode_error/5 +%% decode_error/6 +%% +%% Error when decoding a non-grouped AVP. -decode_error(Name, _Reason, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> - decode_AVP(Name, Avp, Opts, Mod, Acc); +decode_error(true, _, _, _, _, Avp) -> + Avp; -decode_error(Name, Reason, Opts, Mod, Avp, {Rec, Failed}) -> +decode_error(false, Reason, Name, Mod, Opts, Avp) -> Stack = diameter_lib:get_stacktrace(), diameter_lib:log(decode_error, ?MODULE, ?LINE, {Reason, Name, Avp#diameter_avp.name, Mod, Stack}), - {Avp, {Rec, [rc(Reason, Avp, Opts, Mod) | Failed]}}. + case Reason of + {'DIAMETER', 5014 = RC, _} -> + %% Length error communicated from diameter_types or a + %% @custom_types/@codecs module. + AvpName = Avp#diameter_avp.name, + {RC, Avp#diameter_avp{data = Mod:empty_value(AvpName, Opts)}}; + _ -> + {5004, Avp} + end. -%% decode_error/4 +%% 3588/6733: +%% +%% DIAMETER_INVALID_AVP_VALUE 5004 +%% The request contained an AVP with an invalid value in its data +%% portion. A Diameter message indicating this error MUST include +%% the offending AVPs within a Failed-AVP AVP. -decode_error({RC, ErrorData}, Avp, {Rec, Failed}, ComponentAvps) -> - E = Avp#diameter_avp{data = [ErrorData]}, - {[Avp | trim(ComponentAvps)], {Rec, [{RC, E} | Failed]}}. +%% avp/6 -%% set_strict/3 +avp(T, Data, AvpName, Opts, Mod, Mod) -> + Mod:avp(T, Data, AvpName, Opts); + +avp(T, Data, AvpName, Opts, _, Mod) -> + Mod:avp(T, Data, AvpName, Opts#{module := Mod}). +%% set_strict/3 +%% %% Set false as soon as we see a Grouped AVP that doesn't set the %% M-bit, to ignore the M-bit on an encapsulated AVP. + set_strict('Grouped', false = M, #{strict_mbit := true} = Opts) -> Opts#{strict_mbit := M}; set_strict(_, _, Opts) -> @@ -476,102 +680,84 @@ set_failed('Failed-AVP', #{failed_avp := false} = Opts) -> set_failed(_, Opts) -> Opts. -%% decode_AVP/5 -%% -%% Don't know this AVP: see if it can be packed in an 'AVP' field -%% undecoded. Note that the type field is 'undefined' in this case. - -decode_AVP(Name, Avp, Opts, Mod, Acc) -> - {trim(Avp), pack_AVP(Name, Avp, Opts, Mod, Acc)}. - -%% rc/2 - -%% diameter_types will raise an error of this form to communicate -%% DIAMETER_INVALID_AVP_LENGTH (5014). A module specified to a -%% @custom_types tag in a dictionary file can also raise an error of -%% this form. -rc({'DIAMETER', 5014 = RC, _}, #diameter_avp{name = AvpName} = Avp, Opts, Mod) -> - {RC, Avp#diameter_avp{data = Mod:empty_value(AvpName, Opts)}}; - -%% 3588: -%% -%% DIAMETER_INVALID_AVP_VALUE 5004 -%% The request contained an AVP with an invalid value in its data -%% portion. A Diameter message indicating this error MUST include -%% the offending AVPs within a Failed-AVP AVP. -rc(_, Avp, _, _) -> - {5004, Avp}. - -%% pack_avp/5 - -pack_avp(Name, #diameter_avp{name = AvpName} = Avp, Opts, Mod, Acc) -> - pack_avp(Name, Mod:avp_arity(Name, AvpName), Avp, Opts, Mod, Acc). - -%% pack_avp/6 - -pack_avp(Name, 0, Avp, Opts, Mod, Acc) -> - pack_AVP(Name, Avp, Opts, Mod, Acc); - -pack_avp(_, Arity, #diameter_avp{name = AvpName} = Avp, _Opts, Mod, Acc) -> - pack(Arity, AvpName, Avp, Mod, Acc). - -%% pack_AVP/5 +%% acc/8 + +acc([AM | Acc], As, I, Field, Arity, Strict, Mod, Opts) -> + [AM | acc1(Acc, As, I, Field, Arity, Strict, Mod, Opts)]. + +%% acc1/8 + +%% Faulty AVP, not grouped. +acc1(Acc, {_RC, Avp} = E, _, _, _, _, _, _) -> + [Avps, Failed | Rec] = Acc, + [[Avp | Avps], [E | Failed] | Rec]; + +%% Faulty component in grouped AVP. +acc1(Acc, {RC, As, Avp}, _, _, _, _, _, _) -> + [Avps, Failed | Rec] = Acc, + [[As | Avps], [{RC, Avp} | Failed] | Rec]; + +%% Grouped AVP ... +acc1([Avps | Acc], [Avp|_] = As, I, Field, Arity, Strict, Mod, Opts) -> + [[As|Avps] | acc2(Acc, Avp, I, Field, Arity, Strict, Mod, Opts)]; + +%% ... or not. +acc1([Avps | Acc], Avp, I, Field, Arity, Strict, Mod, Opts) -> + [[Avp|Avps] | acc2(Acc, Avp, I, Field, Arity, Strict, Mod, Opts)]. + +%% The component list of a Grouped AVP is discarded when packing into +%% the record (or equivalent): the values in an 'AVP' field are +%% diameter_avp records, not a list of records in the Grouped case, +%% and the decode into the value field is best-effort. The reason is +%% history more than logic: it would probably have made more sense to +%% retain the same structure as in diameter_packet.avps, but an 'AVP' +%% list has always been flat. + +%% acc2/8 + +%% No errors, but nowhere to pack. +acc2(Acc, Avp, _, 'AVP', 0, _, _, _) -> + [Failed | Rec] = Acc, + [[{rc(Avp), Avp} | Failed] | Rec]; + +%% Relaxed arities. +acc2(Acc, Avp, _, Field, Arity, Strict, Mod, _) + when Strict /= decode -> + pack(Arity, Field, Avp, Mod, Acc); + +%% No maximum arity. +acc2(Acc, Avp, _, Field, {_,'*'} = Arity, _, Mod, _) -> + pack(Arity, Field, Avp, Mod, Acc); + +%% Or check. +acc2(Acc, Avp, I, Field, Arity, _, Mod, _) -> + Mx = max_arity(Arity), + if Mx =< I -> + [Failed | Rec] = Acc, + [[{5009, Avp} | Failed] | Rec]; + true -> + pack(Arity, Field, Avp, Mod, Acc) + end. -%% Length failure was induced because of a header/payload length -%% mismatch. The AVP Length is reset to match the received data if -%% this AVP is encoded in an answer message, since the length is -%% computed. -%% -%% Data is a truncated header if command_code = undefined, otherwise -%% payload bytes. The former is padded to the length of a header if -%% the AVP reaches an outgoing encode in diameter_codec. +%% 3588/6733: %% -%% RFC 6733 says that an AVP returned with 5014 can contain a minimal -%% payload for the AVP's type, but in this case we don't know the -%% type. - -pack_AVP(_, #diameter_avp{data = {5014 = RC, Data}} = Avp, _, _, Acc) -> - {Rec, Failed} = Acc, - {Rec, [{RC, Avp#diameter_avp{data = Data}} | Failed]}; - -pack_AVP(Name, Avp, Opts, Mod, Acc) -> - pack_arity(Name, pack_arity(Name, Opts, Mod, Avp), Avp, Mod, Acc). - -%% pack_arity/5 - -pack_arity(_, 0, #diameter_avp{is_mandatory = M} = Avp, _, Acc) -> - {Rec, Failed} = Acc, - {Rec, [{if M -> 5001; true -> 5008 end, Avp} | Failed]}; - -pack_arity(_, Arity, Avp, Mod, Acc) -> - pack(Arity, 'AVP', Avp, Mod, Acc). +%% DIAMETER_AVP_OCCURS_TOO_MANY_TIMES 5009 +%% A message was received that included an AVP that appeared more +%% often than permitted in the message definition. The Failed-AVP +%% AVP MUST be included and contain a copy of the first instance of +%% the offending AVP that exceeded the maximum number of occurrences -%% Give Failed-AVP special treatment since (1) it'll contain any -%% unrecognized mandatory AVP's and (2) the RFC 3588 grammar failed to -%% allow for Failed-AVP in an answer-message. +%% max_arity/1 -pack_arity(Name, - #{strict_mbit := Strict, - failed_avp := Failed}, - Mod, - #diameter_avp{is_mandatory = M, - name = AvpName}) -> +max_arity(1) -> + 1; +max_arity({_,Mx}) -> + Mx. - %% Not testing just Name /= 'Failed-AVP' means we're changing the - %% packing of AVPs nested within Failed-AVP, but the point of - %% ignoring errors within Failed-AVP is to decode as much as - %% possible, and failing because a mandatory AVP couldn't be - %% packed into a dedicated field defeats that point. +%% rc/1 - if Failed == true; - Name == 'Failed-AVP'; - Name == 'answer-message', AvpName == 'Failed-AVP'; - not M; - not Strict -> - Mod:avp_arity(Name, 'AVP'); - true -> - 0 - end. +rc(#diameter_avp{is_mandatory = M}) -> + if M -> 5001; true -> 5008 end. %% 3588: %% @@ -586,75 +772,99 @@ pack_arity(Name, %% Failed-AVP AVP MUST be included and contain a copy of the %% offending AVP. +%% pack_arity/5 + +%% Give Failed-AVP special treatment since (1) it'll contain any +%% unrecognized mandatory AVP's and (2) the RFC 3588 grammar failed to +%% allow for Failed-AVP in an answer-message. + +pack_arity(Name, AvpName, _, Mod, M) + when Name == 'Failed-AVP'; + Name == 'answer-message', AvpName == 'Failed-AVP'; + not M -> + Mod:avp_arity(Name, 'AVP'); +%% Not testing just Name /= 'Failed-AVP' means we're changing the +%% packing of AVPs nested within Failed-AVP, but the point of +%% ignoring errors within Failed-AVP is to decode as much as +%% possible, and failing because a mandatory AVP couldn't be +%% packed into a dedicated field defeats that point. + +pack_arity(Name, _, #{strict_mbit := Strict, failed_avp := Failed}, Mod, _) + when not Strict; + Failed -> + Mod:avp_arity(Name, 'AVP'); + +pack_arity(_, _, _, _, _) -> + 0. + +%% avp_arity/5 + +avp_arity(Name, 'AVP' = AvpName, Mod, Opts, M) -> + pack_arity(Name, AvpName, Opts, Mod, M); + +avp_arity(Name, AvpName, Mod, _, _) -> + Mod:avp_arity(Name, AvpName). + %% pack/5 -pack(Arity, FieldName, Avp, Mod, {Rec, _} = Acc) -> - pack(Mod:'#get-'(FieldName, Rec), Arity, FieldName, Avp, Mod, Acc). +pack(Arity, F, Avp, Mod, [Failed | Rec]) -> + [Failed | set(Arity, F, value(F, Avp), Mod, Rec)]. -%% pack/6 +%% set/5 -pack(undefined, 1, 'AVP' = F, Avp, Mod, {Rec, Failed}) -> %% unlikely - {Mod:'#set-'({F, Avp}, Rec), Failed}; +set(_, _, _, _, Name) + when is_atom(Name) -> + Name; -pack(undefined, 1, F, #diameter_avp{value = V}, Mod, {Rec, Failed}) -> - {Mod:'#set-'({F, V}, Rec), Failed}; +set(1, F, Value, _, Map) + when is_map(Map) -> + Map#{F => Value}; -%% 3588: -%% -%% DIAMETER_AVP_OCCURS_TOO_MANY_TIMES 5009 -%% A message was received that included an AVP that appeared more -%% often than permitted in the message definition. The Failed-AVP -%% AVP MUST be included and contain a copy of the first instance of -%% the offending AVP that exceeded the maximum number of occurrences -%% +set(_, F, V, _, Map) + when is_map(Map) -> + maps:update_with(F, fun(Vs) -> [V|Vs] end, [V], Map); -pack(_, 1, _, Avp, _, {Rec, Failed}) -> - {Rec, [{5009, Avp} | Failed]}; - -pack(L, {_, Max}, F, Avp, Mod, {Rec, Failed}) -> - case '*' /= Max andalso has_prefix(Max+1, L) of - true -> - {Rec, [{5009, Avp} | Failed]}; - false when F == 'AVP' -> - {Mod:'#set-'({F, [Avp | L]}, Rec), Failed}; - false -> - {Mod:'#set-'({F, [Avp#diameter_avp.value | L]}, Rec), Failed} - end. +set(1, F, Value, Mod, Rec) -> + Mod:'#set-'({F, Value}, Rec); + +set(_, F, V, Mod, Rec) -> + Vs = Mod:'#get-'(F, Rec), + Mod:'#set-'({F, [V|Vs]}, Rec). + +%% value/2 + +value('AVP', Avp) -> + Avp; + +value(_, #diameter_avp{value = V}) -> + V. %% --------------------------------------------------------------------------- %% # grouped_avp/3 %% --------------------------------------------------------------------------- --spec grouped_avp(decode, avp_name(), binary() | {5014, binary()}, term()) +%% Note that Grouped is the only AVP type that doesn't just return a +%% decoded value, also returning the list of component diameter_avp +%% records. + +-spec grouped_avp(decode, avp_name(), binary(), term()) -> {avp_record(), [avp()]}; (encode, avp_name(), avp_record() | avp_values(), term()) -> iolist() | no_return(). -%% Length error induced by diameter_codec:collect_avps/1: the AVP -%% length in the header was too short (insufficient for the extracted -%% header) or too long (past the end of the message). An empty payload -%% is sufficient according to the RFC text for 5014. -grouped_avp(decode, _Name, {5014 = RC, _Bin}, _) -> - ?THROW({grouped, {RC, []}, []}); - -grouped_avp(decode, Name, Data, Opts) -> - grouped_decode(Name, diameter_codec:collect_avps(Data), Opts); +%% An error in decoding a component AVP throws the first faulty +%% component, which a catch wraps in the Grouped AVP in question. A +%% partially decoded record is only used when ignoring errors in +%% Failed-AVP. +grouped_avp(decode, Name, Bin, Opts) -> + {Rec, Avps, Es} = T = decode_avps(Name, Bin, Opts), + [] == Es orelse ?THROW(T), + {Rec, Avps}; grouped_avp(encode, Name, Data, Opts) -> encode_avps(Name, Data, Opts). -%% grouped_decode/2 -%% -%% Note that Grouped is the only AVP type that doesn't just return a -%% decoded value, also returning the list of component diameter_avp -%% records. - -%% Length error in trailing component AVP. -grouped_decode(_Name, {Error, Acc}, _) -> - {5014, Avp} = Error, - ?THROW({grouped, Error, [Avp | Acc]}); - %% 7.5. Failed-AVP AVP %% In the case where the offending AVP is embedded within a Grouped AVP, @@ -665,15 +875,6 @@ grouped_decode(_Name, {Error, Acc}, _) -> %% to the single offending AVP. This enables the recipient to detect %% the location of the offending AVP when embedded in a group. -%% An error in decoding a component AVP throws the first faulty -%% component, which the catch in d/3 wraps in the Grouped AVP in -%% question. A partially decoded record is only used when ignoring -%% errors in Failed-AVP. -grouped_decode(Name, ComponentAvps, Opts) -> - {Rec, Avps, Es} = decode_avps(Name, ComponentAvps, Opts), - [] == Es orelse ?THROW({grouped, [{_,_} = hd(Es) | Rec], Avps}), - {Rec, Avps}. - %% --------------------------------------------------------------------------- %% # empty_group/2 %% --------------------------------------------------------------------------- @@ -705,5 +906,45 @@ empty(Name, #{module := Mod} = Opts) -> %% ------------------------------------------------------------------------------ +%% newrec/4 + +newrec(none, _, Name, _) -> + Name; + +newrec(record, Mod, Name, T) + when T /= decode -> + RecName = Mod:name2rec(Name), + Sz = Mod:'#info-'(RecName, size), + erlang:make_tuple(Sz, [], [{1, RecName}]); + +newrec(record, Mod, Name, _) -> + newrec(Mod, Name); + +newrec(_, _, _, _) -> + #{}. + +%% newrec/2 + newrec(Mod, Name) -> Mod:'#new-'(Mod:name2rec(Name)). + +%% reformat/5 + +reformat(Name, Map, _Strict, Mod, list) -> + [{F,V} || {F,_} <- Mod:avp_arity(Name), #{F := V} <- [Map]]; + +reformat(Name, Map, Strict, Mod, record_from_map) -> + RecName = Mod:name2rec(Name), + list_to_tuple([RecName | [mget(F, Map, def(A, Strict)) + || {F,A} <- Mod:avp_arity(Name)]]); + +reformat(_, Rec, _, _, _) -> + Rec. + +%% def/2 + +def(1, decode) -> + undefined; + +def(_, _) -> + []. diff --git a/lib/diameter/src/base/diameter_lib.erl b/lib/diameter/src/base/diameter_lib.erl index 8792e97621..1c1ea42cb5 100644 --- a/lib/diameter/src/base/diameter_lib.erl +++ b/lib/diameter/src/base/diameter_lib.erl @@ -283,7 +283,7 @@ ip(T) %% Or not: convert from '.'/':'-separated decimal/hex. ip(Addr) -> - {ok, A} = inet_parse:address(Addr), %% documented in inet(3) + {ok, A} = inet:parse_address(Addr), A. %% --------------------------------------------------------------------------- diff --git a/lib/diameter/src/base/diameter_peer.erl b/lib/diameter/src/base/diameter_peer.erl index 2759f17e64..4cb5a57a54 100644 --- a/lib/diameter/src/base/diameter_peer.erl +++ b/lib/diameter/src/base/diameter_peer.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2015. All Rights Reserved. +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -202,10 +202,10 @@ match1(Addr, Match) -> match(Addr, {ok, A}, _) -> Addr == A; match(Addr, {error, _}, RE) -> - match == re:run(inet_parse:ntoa(Addr), RE, [{capture, none}]). + match == re:run(inet:ntoa(Addr), RE, [{capture, none}, caseless]). addr([_|_] = A) -> - inet_parse:address(A); + inet:parse_address(A); addr(A) -> {ok, A}. diff --git a/lib/diameter/src/base/diameter_peer_fsm.erl b/lib/diameter/src/base/diameter_peer_fsm.erl index 1b0dc417e5..d99f11a697 100644 --- a/lib/diameter/src/base/diameter_peer_fsm.erl +++ b/lib/diameter/src/base/diameter_peer_fsm.erl @@ -128,7 +128,8 @@ %% outgoing DPR; boolean says whether or not %% the request was sent explicitly with %% diameter:call/4. - codec :: #{string_decode := boolean(), + codec :: #{decode_format := diameter:decode_format(), + string_decode := boolean(), strict_mbit := boolean(), rfc := 3588 | 6733, ordered_encode := false}, @@ -237,7 +238,7 @@ i({Ack, WPid, {M, Ref} = T, Opts, {SvcOpts, Nodes, Dict0, Svc}}) -> proplists:get_value(dpa_timeout, Opts, ?DPA_TIMEOUT)}), Tmo = proplists:get_value(capx_timeout, Opts, ?CAPX_TIMEOUT), - Strictness = proplists:get_value(capx_strictness, Opts, true), + Strict = proplists:get_value(strict_capx, Opts, true), LengthErr = proplists:get_value(length_errors, Opts, exit), {TPid, Addrs} = start_transport(T, Rest, Svc), @@ -251,9 +252,10 @@ i({Ack, WPid, {M, Ref} = T, Opts, {SvcOpts, Nodes, Dict0, Svc}}) -> mode = M, service = svc(Svc, Addrs), length_errors = LengthErr, - strict = Strictness, + strict = Strict, incoming_maxlen = Maxlen, - codec = maps:with([string_decode, + codec = maps:with([decode_format, + string_decode, strict_mbit, rfc, ordered_encode], @@ -542,11 +544,11 @@ put_route(Pid) -> MRef = monitor(process, Pid), put(Pid, MRef). -%% get_route/2 +%% get_route/3 -%% incoming answer -get_route(_, #diameter_packet{header = #diameter_header{is_request = false}} - = Pkt) -> +%% Incoming answer. +get_route(_, _, #diameter_packet{header = #diameter_header{is_request = false}} + = Pkt) -> Seqs = diameter_codec:sequence_numbers(Pkt), case erase(Seqs) of {Pid, Ref, MRef} -> @@ -557,8 +559,14 @@ get_route(_, #diameter_packet{header = #diameter_header{is_request = false}} false end; -%% incoming request -get_route(Ack, _) -> +%% Requests answered here ... +get_route(_, N, _) + when N == 'CER'; + N == 'DPR' -> + false; + +%% ... or not. +get_route(Ack, _, _) -> Ack. %% erase_route/1 @@ -649,10 +657,6 @@ encode(Rec, Opts, Dict) -> %% incoming/2 -incoming({recv = T, Name, Pkt}, #state{parent = Pid, ack = Ack} = S) -> - Pid ! {T, self(), get_route(Ack, Pkt), Name, Pkt}, - rcv(Name, Pkt, S); - incoming(#diameter_header{is_request = R}, #state{transport = TPid, ack = Ack}) -> R andalso Ack andalso send(TPid, false), @@ -670,98 +674,97 @@ incoming(T, _) -> %% recv/2 -recv(#diameter_packet{header = #diameter_header{} = Hdr} - = Pkt, - #state{dictionary = Dict0} - = S) -> - recv1(diameter_codec:msg_name(Dict0, Hdr), Pkt, S); - -recv(#diameter_packet{header = undefined, - bin = Bin} - = Pkt, - S) -> - recv(diameter_codec:decode_header(Bin), Pkt, S); +recv(#diameter_packet{bin = Bin} = Pkt, S) -> + recv(Bin, Pkt, S); recv(Bin, S) -> - recv(#diameter_packet{bin = Bin}, S). + recv(Bin, Bin, S). + +%% recv/3 + +recv(Bin, Msg, S) -> + recv(diameter_codec:decode_header(Bin), Bin, Msg, S). -%% recv1/3 +%% recv/4 -recv1(_, - #diameter_packet{header = H, bin = Bin}, - #state{incoming_maxlen = M}) +recv(false, Bin, _, #state{length_errors = E}) -> + invalid(E, truncated_header, Bin), + Bin; + +recv(#diameter_header{length = Len} = H, Bin, Msg, #state{length_errors = E, + incoming_maxlen = M, + dictionary = Dict0} + = S) + when E == handle; + 0 == Len rem 4, bit_size(Bin) == 8*Len, size(Bin) =< M -> + recv1(diameter_codec:msg_name(Dict0, H), H, Msg, S); + +recv(H, Bin, _, #state{incoming_maxlen = M}) when M < size(Bin) -> invalid(false, incoming_maxlen_exceeded, {size(Bin), H}), H; +recv(H, Bin, _, #state{length_errors = E}) -> + T = {size(Bin), bit_size(Bin) rem 8, H}, + invalid(E, message_length_mismatch, T), + H. + +%% recv1/4 + %% Ignore anything but an expected CER/CEA if so configured. This is %% non-standard behaviour. -recv1(Name, #diameter_packet{header = H}, #state{state = {'Wait-CEA', _, _}, - strict = false}) +recv1(Name, H, _, #state{state = {'Wait-CEA', _, _}, + strict = false}) when Name /= 'CEA' -> H; -recv1(Name, #diameter_packet{header = H}, #state{state = recv_CER, - strict = false}) +recv1(Name, H, _, #state{state = recv_CER, + strict = false}) when Name /= 'CER' -> H; %% Incoming request after outgoing DPR: discard. Don't discard DPR, so %% both ends don't do so when sending simultaneously. -recv1(Name, - #diameter_packet{header = #diameter_header{is_request = true} = H}, - #state{dpr = {_,_,_}}) +recv1(Name, #diameter_header{is_request = true} = H, _, #state{dpr = {_,_,_}}) when Name /= 'DPR' -> invalid(false, recv_after_outgoing_dpr, H), H; %% Incoming request after incoming DPR: discard. -recv1(_, - #diameter_packet{header = #diameter_header{is_request = true} = H}, - #state{dpr = true}) -> +recv1(_, #diameter_header{is_request = true} = H, _, #state{dpr = true}) -> invalid(false, recv_after_incoming_dpr, H), H; %% DPA with identifier mismatch, or in response to a DPR initiated by %% the service. -recv1('DPA' = N, - #diameter_packet{header = #diameter_header{hop_by_hop_id = Hid, - end_to_end_id = Eid}} - = Pkt, - #state{dpr = {X,H,E}} +recv1('DPA' = Name, + #diameter_header{hop_by_hop_id = Hid, end_to_end_id = Eid} + = H, + Msg, + #state{dpr = {X,HI,EI}} = S) - when H /= Hid; - E /= Eid; + when HI /= Hid; + EI /= Eid; not X -> - rcv(N, Pkt, S); + Pkt = pkt(H, Msg), + handle(Name, Pkt, S); -%% Any other message with a header and no length errors: send to the -%% parent. -recv1(Name, Pkt, #state{}) -> - {recv, Name, Pkt}. +%% Any other message with a header and no length errors. +recv1(Name, H, Msg, #state{parent = Pid, ack = Ack} = S) -> + Pkt = pkt(H, Msg), + Pid ! {recv, self(), get_route(Ack, Name, Pkt), Name, Pkt}, + handle(Name, Pkt, S). -%% recv/3 +%% pkt/2 -recv(#diameter_header{length = Len} - = H, - #diameter_packet{bin = Bin} - = Pkt, - #state{length_errors = E} - = S) - when E == handle; - 0 == Len rem 4, bit_size(Bin) == 8*Len -> - recv(Pkt#diameter_packet{header = H}, S); +pkt(H, Bin) + when is_binary(Bin) -> + #diameter_packet{header = H, + bin = Bin}; -recv(#diameter_header{} - = H, - #diameter_packet{bin = Bin}, - #state{length_errors = E}) -> - T = {size(Bin), bit_size(Bin) rem 8, H}, - invalid(E, message_length_mismatch, T), - Bin; +pkt(H, Pkt) -> + Pkt#diameter_packet{header = H}. -recv(false, #diameter_packet{bin = Bin}, #state{length_errors = E}) -> - invalid(E, truncated_header, Bin), - Bin. +%% invalid/3 %% Note that counters here only count discarded messages. invalid(E, Reason, T) -> @@ -770,39 +773,39 @@ invalid(E, Reason, T) -> ?LOG(Reason, T), ok. -%% rcv/3 +%% handle/3 %% Incoming CEA. -rcv('CEA' = N, - #diameter_packet{header = #diameter_header{end_to_end_id = Eid, - hop_by_hop_id = Hid}} - = Pkt, - #state{state = {'Wait-CEA', Hid, Eid}} - = S) -> +handle('CEA' = N, + #diameter_packet{header = #diameter_header{end_to_end_id = Eid, + hop_by_hop_id = Hid}} + = Pkt, + #state{state = {'Wait-CEA', Hid, Eid}} + = S) -> ?LOG(recv, N), handle_CEA(Pkt, S); %% Incoming CER -rcv('CER' = N, Pkt, #state{state = recv_CER} = S) -> +handle('CER' = N, Pkt, #state{state = recv_CER} = S) -> handle_request(N, Pkt, S); %% Anything but CER/CEA in a non-Open state is an error, as is %% CER/CEA in anything but recv_CER/Wait-CEA. -rcv(Name, _, #state{state = PS}) +handle(Name, _, #state{state = PS}) when PS /= 'Open'; Name == 'CER'; Name == 'CEA' -> {stop, {Name, PS}}; -rcv('DPR' = N, Pkt, S) -> +handle('DPR' = N, Pkt, S) -> handle_request(N, Pkt, S); %% DPA in response to DPR, with the expected identifiers. -rcv('DPA' = N, - #diameter_packet{header = #diameter_header{end_to_end_id = Eid, - hop_by_hop_id = Hid} - = H} - = Pkt, +handle('DPA' = N, + #diameter_packet{header = #diameter_header{end_to_end_id = Eid, + hop_by_hop_id = Hid} + = H} + = Pkt, #state{dictionary = Dict0, transport = TPid, dpr = {X, Hid, Eid}, @@ -813,7 +816,8 @@ rcv('DPA' = N, %% service: explicit DPR is counted in the same way %% as other explicitly sent requests. incr(recv, H, Dict0), - incr_rc(recv, diameter_codec:decode(Dict0, Opts, Pkt), Dict0) + {_, RecPkt} = decode(Dict0, Opts, Pkt), + incr_rc(recv, RecPkt, Dict0) end, diameter_peer:close(TPid), {stop, N}; @@ -821,13 +825,13 @@ rcv('DPA' = N, %% Ignore an unsolicited DPA in particular. Note that dpa_timeout %% deals with the case in which the peer sends the wrong identifiers %% in DPA. -rcv('DPA' = N, #diameter_packet{header = H}, _) -> +handle('DPA' = N, #diameter_packet{header = H}, _) -> ?LOG(ignored, N), %% Note that these aren't counted in the normal recv counter. diameter_stats:incr({diameter_codec:msg_id(H), recv, ignored}), ok; -rcv(_, _, _) -> +handle(_, _, _) -> ok. %% incr/3 @@ -917,21 +921,30 @@ handle_request(Name, = S) -> ?LOG(recv, Name), incr(recv, H, Dict0), - send_answer(Name, diameter_codec:decode(Dict0, Opts, Pkt), S). + send_answer(Name, decode(Dict0, Opts, Pkt), S). + +%% decode/3 +%% +%% Decode the message as record for diameter_capx, and in the +%% configured format for events. + +decode(Dict0, Opts, Pkt) -> + {diameter_codec:decode(Dict0, Opts, Pkt), + diameter_codec:decode(Dict0, Opts#{decode_format := record}, Pkt)}. %% send_answer/3 -send_answer(Type, ReqPkt, #state{transport = TPid, - dictionary = Dict, - codec = Opts} - = S) -> - incr_error(recv, ReqPkt, Dict), +send_answer(Type, {DecPkt, RecPkt}, #state{transport = TPid, + dictionary = Dict, + codec = Opts} + = S) -> + incr_error(recv, RecPkt, Dict), #diameter_packet{header = H, transport_data = TD} - = ReqPkt, + = RecPkt, - {Msg, PostF} = build_answer(Type, ReqPkt, S), + {Msg, PostF} = build_answer(Type, DecPkt, RecPkt, S), %% An answer message clears the R and T flags and retains the P %% flag. The E flag is set at encode. @@ -959,15 +972,15 @@ eval([F|A], S) -> eval(T, _) -> close(T). -%% build_answer/3 +%% build_answer/4 build_answer('CER', + DecPkt, #diameter_packet{msg = CER, header = #diameter_header{version = ?DIAMETER_VERSION, is_error = false}, - errors = []} - = Pkt, + errors = []}, #state{dictionary = Dict0} = S) -> {SupportedApps, RCaps, CEA} = recv_CER(CER, S), @@ -985,25 +998,25 @@ build_answer('CER', orelse ?THROW(4003), %% DIAMETER_ELECTION_LOST caps_cb(Caps) of - N -> {cea(CEA, N, Dict0), [fun open/5, Pkt, + N -> {cea(CEA, N, Dict0), [fun open/5, DecPkt, SupportedApps, Caps, {accept, inband_security(IS)}]} catch ?FAILURE(Reason) -> - rejected(Reason, {'CER', Reason, Caps, Pkt}, S) + rejected(Reason, {'CER', Reason, Caps, DecPkt}, S) end; %% The error checks below are similar to those in diameter_traffic for %% other messages. Should factor out the commonality. build_answer(Type, + DecPkt, #diameter_packet{header = H, - errors = Es} - = Pkt, + errors = Es}, S) -> {RC, FailedAVP} = result_code(Type, H, Es), - {answer(Type, RC, FailedAVP, S), post(Type, RC, Pkt, S)}. + {answer(Type, RC, FailedAVP, S), post(Type, RC, DecPkt, S)}. inband_security([]) -> ?NO_INBAND_SECURITY; @@ -1175,12 +1188,10 @@ handle_CEA(#diameter_packet{header = H} = S) -> incr(recv, H, Dict0), - #diameter_packet{} - = DPkt - = diameter_codec:decode(Dict0, Opts, Pkt), + {DecPkt, RecPkt} = decode(Dict0, Opts, Pkt), - RC = result_code(incr_rc(recv, DPkt, Dict0)), - {SApps, IS, RCaps} = recv_CEA(DPkt, S), + RC = result_code(incr_rc(recv, RecPkt, Dict0)), + {SApps, IS, RCaps} = recv_CEA(RecPkt, S), #diameter_caps{origin_host = {OH, DH}} = Caps @@ -1203,9 +1214,9 @@ handle_CEA(#diameter_packet{header = H} orelse ?THROW(election_lost), caps_cb(Caps) of - _ -> open(DPkt, SApps, Caps, {connect, hd([_] = IS)}, S) + _ -> open(DecPkt, SApps, Caps, {connect, hd([_] = IS)}, S) catch - ?FAILURE(Reason) -> close({'CEA', Reason, Caps, DPkt}) + ?FAILURE(Reason) -> close({'CEA', Reason, Caps, DecPkt}) end. %% Check more than the result code since the peer could send success %% regardless. If not 2001 then a peer_up callback could do anything diff --git a/lib/diameter/src/base/diameter_reg.erl b/lib/diameter/src/base/diameter_reg.erl index 97e74657bd..bd5db54a5c 100644 --- a/lib/diameter/src/base/diameter_reg.erl +++ b/lib/diameter/src/base/diameter_reg.erl @@ -19,10 +19,11 @@ %% %% -%% The module implements a simple term -> pid registry. +%% A simple term -> pid registry. %% -module(diameter_reg). + -behaviour(gen_server). -export([add/1, @@ -57,18 +58,18 @@ -type key() :: term(). -type from() :: {pid(), term()}. +-type rcvr() :: [pid() | term()] %% subscribe + | from(). %% wait -type pattern() :: term(). -record(state, {id = diameter_lib:now(), - receivers = dict:new() - :: dict:dict(pattern(), [[pid() | term()]%% subscribe - | from()]), %% wait + notify = #{} :: #{pattern() => [rcvr()]}, monitors = sets:new() :: sets:set(pid())}). %% The ?TABLE bag contains the Key -> Pid mapping, as {Key, Pid} %% tuples. Each pid is stored in the monitors set to ensure only one %% monitor for each pid: more are harmless, but unnecessary. A pattern -%% is added to receivers a result of calls to wait/1 or subscribe/2: +%% is added to notify a result of calls to wait/1 or subscribe/2: %% changes to ?TABLE causes processes to be notified as required. %% =========================================================================== @@ -156,7 +157,7 @@ wait(Pat) -> %% # subscribe(Pat, T) %% %% Like match/1, but additionally receive messages of the form -%% {T, add|remove, {term(), pid()} when associations are added +%% {T, add|remove, {term(), pid()}} when associations are added %% or removed. %% =========================================================================== @@ -186,15 +187,12 @@ uptime() -> -> [{pid(), [key()]}]. pids() -> - to_list(fun swap/1). - -to_list(Fun) -> - ets:foldl(fun(T,D) -> append(Fun(T), D) end, orddict:new(), ?TABLE). + append(ets:select(?TABLE, [{{'$1','$2'}, [], [{{'$2', '$1'}}]}])). -append({K,V}, Dict) -> - orddict:append(K, V, Dict). - -id(T) -> T. +append(Pairs) -> + dict:to_list(lists:foldl(fun({K,V}, D) -> dict:append(K, V, D) end, + dict:new(), + Pairs)). %% terms/0 @@ -202,9 +200,7 @@ id(T) -> T. -> [{key(), [pid()]}]. terms() -> - to_list(fun id/1). - -swap({X,Y}) -> {Y,X}. + append(ets:tab2list(?TABLE)). %% subs/0 @@ -212,31 +208,19 @@ swap({X,Y}) -> {Y,X}. -> [{pattern(), [{pid(), term()}]}]. subs() -> - #state{receivers = RD} = state(), - dict:fold(fun sub/3, orddict:new(), RD). - -sub(Pat, Ps, Dict) -> - lists:foldl(fun([P|T], D) -> orddict:append(Pat, {P,T}, D); - (_, D) -> D - end, - Dict, - Ps). + #state{notify = Dict} = state(), + [{K, Ts} || {K,Ps} <- maps:to_list(Dict), + Ts <- [[{P,T} || [P|T] <- Ps]]]. %% waits/0 -spec waits() - -> [{pattern(), [{from(), term()}]}]. + -> [{pattern(), [from()]}]. waits() -> - #state{receivers = RD} = state(), - dict:fold(fun wait/3, orddict:new(), RD). - -wait(Pat, Ps, Dict) -> - lists:foldl(fun({_,_} = F, D) -> orddict:append(Pat, F, D); - (_, D) -> D - end, - Dict, - Ps). + #state{notify = Dict} = state(), + [{K, Ts} || {K,Ps} <- maps:to_list(Dict), + Ts <- [[T || {_,_} = T <- Ps]]]. %% ---------------------------------------------------------- %% # init/1 @@ -250,33 +234,28 @@ init(_) -> %% # handle_call/3 %% ---------------------------------------------------------- -handle_call({add, Uniq, Key}, {Pid, _}, S0) -> +handle_call({add, Uniq, Key}, {Pid, _}, S) -> Rec = {Key, Pid}, - S1 = flush(Uniq, Rec, S0), + NS = flush(Uniq, Rec, S), %% before insert {Res, New} = insert(Uniq, Rec), - {Recvs, S} = add(New, Rec, S1), - notify(Recvs, Rec), - {reply, Res, S}; + {reply, Res, notify(add, New andalso Rec, NS)}; handle_call({remove, Key}, {Pid, _}, S) -> Rec = {Key, Pid}, - Recvs = delete([Rec], S), ets:delete_object(?TABLE, Rec), - notify(Recvs, remove), - {reply, true, S}; + {reply, true, notify(remove, Rec, S)}; -handle_call({wait, Pat}, {Pid, _} = From, #state{receivers = RD} = S) -> +handle_call({wait, Pat}, {Pid, _} = From, S) -> NS = add_monitor(Pid, S), case match(Pat) of - [_|_] = L -> - {reply, L, NS}; + [_|_] = Recs -> + {reply, Recs, NS}; [] -> - {noreply, NS#state{receivers = dict:append(Pat, From, RD)}} + {noreply, queue(Pat, From, NS)} end; -handle_call({subscribe, Pat, T}, {Pid, _}, #state{receivers = RD} = S) -> - NS = add_monitor(Pid, S), - {reply, match(Pat), NS#state{receivers = dict:append(Pat, [Pid | T], RD)}}; +handle_call({subscribe, Pat, T}, {Pid, _}, S) -> + {reply, match(Pat), queue(Pat, [Pid | T], add_monitor(Pid, S))}; handle_call(state, _, S) -> {reply, S, S}; @@ -332,106 +311,60 @@ insert(true, Rec) -> B = ets:insert_new(?TABLE, Rec), %% entry inserted? {B, B}. -%% add/3 - +%% add_monitor/2 +%% %% Only add a single monitor for any given process, since there's no %% use to more. -add(true, {_Key, Pid} = Rec, S) -> - NS = add_monitor(Pid, S), - {Recvs, RD} = add(Rec, NS), - {Recvs, S#state{receivers = RD}}; - -add(false = No, _, S) -> - {No, S}. - -%% add/2 - -%% Notify processes whose patterns match the inserted key. -add({_Key, Pid} = Rec, #state{receivers = RD}) -> - dict:fold(fun(Pt, Ps, A) -> - add(lists:member(Rec, match(Pt, Pid)), Pt, Ps, Rec, A) - end, - {sets:new(), RD}, - RD). - -%% add/5 - -add(true, Pat, Recvs, {_,_} = Rec, {Set, Dict}) -> - {lists:foldl(fun sets:add_element/2, Set, Recvs), - remove(fun erlang:is_list/1, Pat, Recvs, Dict)}; -add(false, _, _, _, Acc) -> - Acc. +add_monitor(Pid, #state{monitors = Ps} = S) -> + case sets:is_element(Pid, Ps) of + false -> + monitor(process, Pid), + S#state{monitors = sets:add_element(Pid, Ps)}; + true -> + S + end. -%% add_monitor/2 - -add_monitor(Pid, #state{monitors = MS} = S) -> - add_monitor(sets:is_element(Pid, MS), Pid, S). - -%% add_monitor/3 - -add_monitor(false, Pid, #state{monitors = MS} = S) -> - monitor(process, Pid), - S#state{monitors = sets:add_element(Pid, MS)}; - -add_monitor(true, _, S) -> - S. - -%% delete/2 - -delete(Recs, #state{receivers = RD}) -> - lists:foldl(fun(R,S) -> delete(R, RD, S) end, sets:new(), Recs). +%% notify/3 -%% delete/3 +notify(_, false, S) -> + S; -delete({_Key, Pid} = Rec, RD, Set) -> - dict:fold(fun(Pt, Ps, S) -> - delete(lists:member(Rec, match(Pt, Pid)), Rec, Ps, S) - end, - Set, - RD). +notify(Op, {_,_} = Rec, #state{notify = Dict} = S) -> + S#state{notify = maps:fold(fun(P,Rs,D) -> notify(Op, Rec, P, Rs, D) end, + Dict, + Dict)}. -%% delete/4 +%% notify/5 -%% Entry matches a pattern ... -delete(true, Rec, Recvs, Set) -> - lists:foldl(fun(R,S) -> sets:add_element({R, Rec}, S) end, - Set, - Recvs); - -%% ... or not. -delete(false, _, _, Set) -> - Set. - -%% notify/2 - -notify(false = No, _) -> - No; - -notify(Recvs, remove = Op) -> - sets:fold(fun({P,R}, N) -> send(P, R, Op), N+1 end, 0, Recvs); - -notify(Recvs, {_,_} = Rec) -> - sets:fold(fun(P,N) -> send(P, Rec, add), N+1 end, 0, Recvs). +notify(Op, {_, Pid} = Rec, Pat, Rcvrs, Dict) -> + case lists:member(Rec, match(Pat, Pid)) of + true -> + reset(Pat, Dict, [P || P <- Rcvrs, send(P, Op, Rec)]); + false -> + Dict + end. %% send/3 -%% No processes waiting on remove, by construction: they've either -%% received notification at add or aren't waiting. -send([Pid | T], Rec, Op) -> - Pid ! {T, Op, Rec}; +send([Pid | T], Op, Rec) -> + Pid ! {T, Op, Rec}, + true; -send({_,_} = From, Rec, add) -> - gen_server:reply(From, [Rec]). +%% No processes wait on remove: they receive notification immediately +%% or at add, by construction. +send({_,_} = From, add, Rec) -> + gen_server:reply(From, [Rec]), + false. %% down/2 -down(Pid, #state{monitors = MS} = S) -> - NS = flush(Pid, S), - Recvs = delete(match('_', Pid), NS), +down(Pid, #state{monitors = Ps} = S) -> + Recs = match('_', Pid), ets:match_delete(?TABLE, {'_', Pid}), - notify(Recvs, remove), - NS#state{monitors = sets:del_element(Pid, MS)}. + lists:foldl(fun(R,NS) -> notify(remove, R, NS) end, + flush(Pid, S#state{monitors = sets:del_element(Pid, Ps)}), + Recs). %% flush/3 @@ -452,16 +385,15 @@ flush(false, _, S) -> %% flush/2 %% Process has died and should no longer receive messages/replies. -flush(Pid, #state{receivers = RD} = S) - when is_pid(Pid) -> - S#state{receivers = dict:fold(fun(Pt,Ps,D) -> flush(Pid, Pt, Ps, D) end, - RD, - RD)}. +flush(Pid, #state{notify = Dict} = S) -> + S#state{notify = maps:fold(fun(P,Rs,D) -> flush(Pid, P, Rs, D) end, + Dict, + Dict)}. %% flush/4 -flush(Pid, Pat, Recvs, Dict) -> - remove(fun(T) -> Pid /= head(T) end, Pat, Recvs, Dict). +flush(Pid, Pat, Rcvrs, Dict) -> + reset(Pat, Dict, [T || T <- Rcvrs, Pid /= head(T)]). %% head/1 @@ -471,15 +403,18 @@ head([P|_]) -> head({P,_}) -> P. -%% remove/4 +%% reset/3 + +reset(Key, Map, []) -> + maps:remove(Key, Map); + +reset(Key, Map, List) -> + maps:put(Key, List, Map). + +%% queue/3 -remove(Pred, Key, Values, Dict) -> - case lists:filter(Pred, Values) of - [] -> - dict:erase(Key, Dict); - Rest -> - dict:store(Key, Rest, Dict) - end. +queue(Pat, Rcvr, #state{notify = Dict} = S) -> + S#state{notify = maps:put(Pat, [Rcvr | maps:get(Pat, Dict, [])], Dict)}. %% call/1 diff --git a/lib/diameter/src/base/diameter_service.erl b/lib/diameter/src/base/diameter_service.erl index a976a8b998..31dd92f878 100644 --- a/lib/diameter/src/base/diameter_service.erl +++ b/lib/diameter/src/base/diameter_service.erl @@ -112,8 +112,24 @@ use_shared_peers := diameter:remotes(),%% use from restrict_connections := diameter:restriction(), incoming_maxlen := diameter:message_length(), + strict_arities => diameter:strict_arities(), strict_mbit := boolean(), + decode_format := diameter:decode_format(), + avp_dictionaries => nonempty_list(module()), + traffic_counters := boolean(), string_decode := boolean(), + capabilities_cb => diameter:evaluable(), + pool_size => pos_integer(), + capx_timeout => diameter:'Unsigned32'(), + strict_capx => boolean(), + disconnect_cb => diameter:evaluable(), + dpr_timeout => diameter:'Unsigned32'(), + dpa_timeout => diameter:'Unsigned32'(), + length_errors => exit | handle | discard, + connect_timer => diameter:'Unsigned32'(), + watchdog_timer => diameter:'Unsigned32'() + | {module(), atom(), list()}, + watchdog_config => [{okay|suspect, non_neg_integer()}], spawn_opt := list() | {module(), atom(), list()}}}). %% Record representing an RFC 3539 watchdog process implemented by @@ -514,6 +530,13 @@ transition({tc_timeout, T}, S) -> tc_timeout(T, S), ok; +transition({nodeup, Node, _}, S) -> + nodeup(Node, S), + ok; + +transition({nodedown, _Node, _}, _) -> + ok; + transition(Req, S) -> unexpected(handle_info, [Req], S), ok. @@ -679,12 +702,15 @@ i(SvcName) -> cfg_acc({SvcName, #diameter_service{applications = Apps} = Rec, Opts}, {false, Acc}) -> lists:foreach(fun init_mod/1, Apps), + #{monitor := M} + = SvcOpts + = service_opts(Opts), S = #state{service_name = SvcName, service = Rec#diameter_service{pid = self()}, local = init_peers(), remote = init_peers(), - monitor = mref(get_value(monitor, Opts)), - options = service_options(lists:keydelete(monitor, 1, Opts))}, + monitor = mref(M), + options = maps:remove(monitor, SvcOpts)}, {S, Acc}; cfg_acc({_Ref, Type, _Opts} = T, {S, Acc}) @@ -699,8 +725,29 @@ init_peers() -> %% Alias, %% TPid} -service_options(Opts) -> - maps:from_list(Opts). +service_opts(Opts) -> + remove([{strict_arities, true}, + {avp_dictionaries, []}], + maps:merge(maps:from_list([{monitor, false} | def_opts()]), + maps:from_list(Opts))). + +remove(List, Map) -> + maps:filter(fun(K,V) -> not lists:member({K,V}, List) end, + Map). + +def_opts() -> %% defaults on the service map + [{share_peers, false}, + {use_shared_peers, false}, + {sequence, {0,32}}, + {restrict_connections, nodes}, + {incoming_maxlen, 16#FFFFFF}, + {strict_arities, true}, + {strict_mbit, true}, + {decode_format, record}, + {avp_dictionaries, []}, + {traffic_counters, true}, + {string_decode, true}, + {spawn_opt, []}]. mref(false = No) -> No; @@ -709,6 +756,8 @@ mref(P) -> init_shared(#state{options = #{use_shared_peers := T}, service_name = Svc}) -> + T == false orelse net_kernel:monitor_nodes(true, [{node_type, visible}, + nodedown_reason]), notify(T, Svc, {service, self()}). init_mod(#diameter_app{alias = Alias, @@ -718,16 +767,17 @@ init_mod(#diameter_app{alias = Alias, start_fsm({Ref, Type, Opts}, S) -> start(Ref, {Type, Opts}, S). -get_value(Key, Vs) -> - {_, V} = lists:keyfind(Key, 1, Vs), - V. - notify(Share, SvcName, T) -> Nodes = remotes(Share), [] /= Nodes andalso diameter_peer:notify(Nodes, SvcName, T). %% Test for the empty list for upgrade reasons: there's no %% diameter_peer:notify/3 in old code. +nodeup(Node, #state{options = #{share_peers := SP}, + service_name = SvcName}) -> + lists:member(Node, remotes(SP)) + andalso diameter_peer:notify([Node], SvcName, {service, self()}). + remotes(false) -> []; @@ -806,7 +856,7 @@ start(Ref, Type, Opts, State) -> start(Ref, Type, Opts, N, #state{watchdogT = WatchdogT, local = {PeerT, _, _}, options = #{string_decode := SD} - = SvcOpts0, + = SvcOpts, service_name = SvcName, service = Svc0}) when Type == connect; @@ -815,12 +865,12 @@ start(Ref, Type, Opts, N, #state{watchdogT = WatchdogT, = Svc1 = merge_service(Opts, Svc0), Svc = binary_caps(Svc1, SD), - SvcOpts = merge_options(Opts, SvcOpts0), - RecvData = diameter_traffic:make_recvdata([SvcName, PeerT, Apps, SvcOpts]), - T = {Opts, SvcOpts, RecvData, Svc}, + {SOpts, TOpts} = merge_opts(SvcOpts, Opts), + RecvData = diameter_traffic:make_recvdata([SvcName, PeerT, Apps, SOpts]), + T = {TOpts, SOpts, RecvData, Svc}, Rec = #watchdog{type = Type, ref = Ref, - options = Opts}, + options = TOpts}, diameter_lib:fold_n(fun(_,A) -> [wd(Type, Ref, T, WatchdogT, Rec) | A] @@ -828,10 +878,14 @@ start(Ref, Type, Opts, N, #state{watchdogT = WatchdogT, [], N). -merge_options(Opts, SvcOpts) -> - Keys = maps:keys(SvcOpts), - Map = maps:from_list([KV || {K,_} = KV <- Opts, lists:member(K, Keys)]), - maps:merge(SvcOpts, Map). +merge_opts(SvcOpts, Opts) -> + Keys = [K || {K,_} <- def_opts()], + SO = [T || {K,_} = T <- Opts, lists:member(K, Keys)], + TO = Opts -- SO, + {maps:merge(maps:with(Keys, SvcOpts), maps:from_list(SO)), + TO ++ [T || {K,_} = T <- maps:to_list(SvcOpts), + not lists:member(K, Keys), + not lists:keymember(K, 1, Opts)]}. binary_caps(Svc, true) -> Svc; @@ -1400,9 +1454,15 @@ is_remote(Pid, T) -> %% # remote_peer_up/4 %% --------------------------------------------------------------------------- -remote_peer_up(TPid, Aliases, Caps, #state{options = #{use_shared_peers := T}} +remote_peer_up(TPid, Aliases, Caps, #state{options = #{use_shared_peers := T}, + remote = {PeerT, _, _}} = S) -> - is_remote(TPid, T) andalso rpu(TPid, Aliases, Caps, S). + is_remote(TPid, T) + andalso not ets:member(PeerT, TPid) + andalso rpu(TPid, Aliases, Caps, S). + +%% Notification can be duplicate since remote nodes push and the local +%% node pulls. rpu(TPid, Aliases, Caps, #state{service = Svc, remote = RT}) -> #diameter_service{applications = Apps} = Svc, @@ -1412,6 +1472,7 @@ rpu(TPid, Aliases, Caps, #state{service = Svc, remote = RT}) -> rpu(_, [] = No, _, _) -> No; + rpu(TPid, Aliases, Caps, {PeerT, _, _} = RT) -> monitor(process, TPid), ets:insert(PeerT, #peer{pid = TPid, diff --git a/lib/diameter/src/base/diameter_traffic.erl b/lib/diameter/src/base/diameter_traffic.erl index 85378babea..d2856ae530 100644 --- a/lib/diameter/src/base/diameter_traffic.erl +++ b/lib/diameter/src/base/diameter_traffic.erl @@ -70,13 +70,17 @@ timeout = 5000 :: 0..16#FFFFFFFF, %% for outgoing requests detach = false :: boolean()}). -%% Term passed back to receive_message/6 with every incoming message. +%% Term passed back to receive_message/5 with every incoming message. -record(recvdata, {peerT :: ets:tid(), service_name :: diameter:service_name(), apps :: [#diameter_app{}], sequence :: diameter:sequence(), - codec :: #{string_decode := boolean(), + counters :: boolean(), + codec :: #{decode_format := diameter:decode_format(), + avp_dictionaries => nonempty_list(module()), + string_decode := boolean(), + strict_arities => diameter:strict_arities(), strict_mbit := boolean(), incoming_maxlen := diameter:message_length()}}). %% Note that incoming_maxlen is currently handled in diameter_peer_fsm, @@ -89,6 +93,7 @@ caller :: pid() | undefined, %% calling process handler :: pid(), %% request process peer :: undefined | {pid(), #diameter_caps{}}, + caps :: undefined, %% no longer used packet :: #diameter_packet{} | undefined}). %% of request %% --------------------------------------------------------------------------- @@ -96,13 +101,17 @@ %% --------------------------------------------------------------------------- make_recvdata([SvcName, PeerT, Apps, SvcOpts | _]) -> - #{sequence := {_,_} = Mask, spawn_opt := Opts} + #{sequence := {_,_} = Mask, spawn_opt := Opts, traffic_counters := B} = SvcOpts, {Opts, #recvdata{service_name = SvcName, peerT = PeerT, apps = Apps, sequence = Mask, - codec = maps:with([string_decode, + counters = B, + codec = maps:with([decode_format, + avp_dictionaries, + string_decode, + strict_arities, strict_mbit, ordered_encode, incoming_maxlen], @@ -182,7 +191,7 @@ incr_error(Dir, Id, TPid) -> %% --------------------------------------------------------------------------- -spec incr_rc(send|recv, Pkt, TPid, DictT) - -> {Counter, non_neg_integer()} + -> Counter | Reason when Pkt :: #diameter_packet{}, TPid :: pid(), @@ -193,18 +202,26 @@ incr_error(Dir, Id, TPid) -> | {'Experimental-Result', integer(), integer()}, Reason :: atom(). -incr_rc(Dir, Pkt, TPid, {_, AppDict, _} = DictT) -> - try - incr_result(Dir, Pkt, TPid, DictT) +incr_rc(Dir, Pkt, TPid, {MsgDict, AppDict, Dict0}) -> + incr_rc(Dir, Pkt, TPid, MsgDict, AppDict, Dict0); + +incr_rc(Dir, Pkt, TPid, Dict0) -> + incr_rc(Dir, Pkt, TPid, Dict0, Dict0, Dict0). + +%% incr_rc/6 + +incr_rc(Dir, Pkt, TPid, MsgDict, AppDict, Dict0) -> + try get_result(Dir, MsgDict, Dict0, Pkt) of + false -> + unknown; + Avp -> + incr_result(Dir, Avp, Pkt, TPid, AppDict) catch exit: {E,_} when E == no_result_code; E == invalid_error_bit -> incr(TPid, {msg_id(Pkt#diameter_packet.header, AppDict), Dir, E}), E - end; - -incr_rc(Dir, Pkt, TPid, Dict0) -> - incr_rc(Dir, Pkt, TPid, {Dict0, Dict0, Dict0}). + end. %% --------------------------------------------------------------------------- %% receive_message/5 @@ -216,13 +233,13 @@ incr_rc(Dir, Pkt, TPid, Dict0) -> -> pid() %% request handler | boolean() %% answer, known request or not | discard %% request discarded by MFA - when Route :: {Handler, RequestRef, Seqs} + when Route :: {Handler, RequestRef, TPid} | Ack, RecvData :: {[SpawnOpt], #recvdata{}}, SpawnOpt :: term(), Handler :: pid(), RequestRef :: reference(), - Seqs :: {0..16#FFFFFFFF, 0..16#FFFFFFFF}, + TPid :: pid(), Ack :: boolean(). receive_message(TPid, Route, Pkt, Dict0, RecvData) -> @@ -303,14 +320,15 @@ recv_request(Ack, = Pkt, Dict0, #recvdata{peerT = PeerT, - apps = Apps} + apps = Apps, + counters = Count} = RecvData) -> Ack andalso (TPid ! {handler, self()}), case diameter_service:find_incoming_app(PeerT, TPid, Id, Apps) of {#diameter_app{id = Aid, dictionary = AppDict} = App, Caps} -> - incr(recv, Pkt, TPid, AppDict), + Count andalso incr(recv, Pkt, TPid, AppDict), DecPkt = decode(Aid, AppDict, RecvData, Pkt), - incr_error(recv, DecPkt, TPid, AppDict), + Count andalso incr_error(recv, DecPkt, TPid, AppDict), send_A(recv_R(App, TPid, Dict0, Caps, RecvData, DecPkt), TPid, App, @@ -323,9 +341,7 @@ recv_request(Ack, %% A request was sent for an application that is not %% supported. RC = 3007, - Es = Pkt#diameter_packet.errors, - DecPkt = Pkt#diameter_packet{avps = collect_avps(Pkt), - errors = [RC | Es]}, + DecPkt = diameter_codec:collect_avps(Pkt), send_answer(answer_message(RC, Dict0, Caps, DecPkt), TPid, Dict0, @@ -338,17 +354,11 @@ recv_request(Ack, No end. +%% decode/4 + decode(Id, Dict, #recvdata{codec = Opts}, Pkt) -> errors(Id, diameter_codec:decode(Id, Dict, Opts, Pkt)). -collect_avps(Pkt) -> - case diameter_codec:collect_avps(Pkt) of - {_Error, Avps} -> - Avps; - Avps -> - Avps - end. - %% send_A/7 send_A([T | Fs], TPid, App, Dict0, RecvData, DecPkt, Caps) -> @@ -541,6 +551,7 @@ send_A({call, Opts}, TPid, App, Dict0, RecvData, Pkt, Caps, Fs) -> MsgDict, AppDict, Dict0, + RecvData#recvdata.counters, Fs); RC -> send_answer(answer_message(RC, Dict0, Caps, Pkt), @@ -584,14 +595,22 @@ send_answer(Ans, TPid, MsgDict, AppDict, Dict0, RecvData, DecPkt, Fs) -> TPid, RecvData#recvdata.codec, make_answer_packet(Ans, DecPkt, MsgDict, Dict0)), - send_answer(Pkt, TPid, MsgDict, AppDict, Dict0, Fs). + send_answer(Pkt, + TPid, + MsgDict, + AppDict, + Dict0, + RecvData#recvdata.counters, + Fs). -%% send_answer/6 +%% send_answer/7 -send_answer(Pkt, TPid, MsgDict, AppDict, Dict0, [EvalPktFs | EvalFs]) -> +send_answer(Pkt, TPid, MsgDict, AppDict, Dict0, Count, [EvalPktFs | EvalFs]) -> eval_packet(Pkt, EvalPktFs), - incr(send, Pkt, TPid, AppDict), - incr_rc(send, Pkt, TPid, {MsgDict, AppDict, Dict0}), %% count outgoing + Count andalso begin + incr(send, Pkt, TPid, AppDict), + incr_rc(send, Pkt, TPid, MsgDict, AppDict, Dict0) + end, send(TPid, z(Pkt), _Route = self()), lists:foreach(fun diameter_lib:eval/1, EvalFs). @@ -619,7 +638,7 @@ is_answer_message(#diameter_packet{msg = Msg}, Dict0) -> is_answer_message([#diameter_header{is_request = R, is_error = E} | _], _) -> E andalso not R; -%% Message sent as a tagged avp/value list. +%% Message sent as a map or tagged avp/value list. is_answer_message([Name | _], _) -> Name == 'answer-message'; @@ -665,7 +684,7 @@ resend(false, Route = #diameter_avp{data = {Dict0, 'Route-Record', OH}}, Seq = diameter_session:sequence(Mask), Hdr = Hdr0#diameter_header{hop_by_hop_id = Seq}, - Msg = [Hdr, Route | Avps], %% reordered at encode + Msg = [Hdr | Avps ++ [Route]], case send_request(SvcName, App, Msg, Opts) of #diameter_packet{} = Ans -> Ans; @@ -867,7 +886,10 @@ reset(Msg, [RC | Avps], Dict) -> %% set/3 -%% Reply as name and tuple list ... +%% Reply as name/values list ... +set([Name|As], Avps, _) + when is_map(As) -> + [Name | maps:merge(As, maps:from_list(Avps))]; set([_|_] = Ans, Avps, _) -> Ans ++ Avps; %% Values nearer tail take precedence. @@ -900,33 +922,44 @@ failed_avp(_, [] = No, _) -> failed_avp(Msg, [_|_] = Avps, Dict) -> [failed(Msg, [{'AVP', Avps}], Dict)]. -%% Reply as name and tuple list ... -failed([MsgName | Values], FailedAvp, Dict) -> - RecName = Dict:msg2rec(MsgName), +%% failed/3 + +failed(Msg, FailedAvp, Dict) -> + RecName = msg2rec(Msg, Dict), try - Dict:'#info-'(RecName, {index, 'Failed-AVP'}), + Dict:'#info-'(RecName, {index, 'Failed-AVP'}), %% assert existence {'Failed-AVP', [FailedAvp]} catch error: _ -> - Avps = proplists:get_value('AVP', Values, []), + Avps = values(Msg, 'AVP', Dict), A = #diameter_avp{name = 'Failed-AVP', value = FailedAvp}, {'AVP', [A|Avps]} + end. + +%% msg2rec/2 + +%% Message as name/values list ... +msg2rec([MsgName | _], Dict) -> + Dict:msg2rec(MsgName); + +%% ... or record. +msg2rec(Rec, _) -> + element(1, Rec). + +%% values/2 + +%% Message as name/values list ... +values([_ | Avps], F, _) -> + if is_map(Avps) -> + maps:get(F, Avps, []); + is_list(Avps) -> + proplists:get_value(F, Avps, []) end; %% ... or record. -failed(Rec, FailedAvp, Dict) -> - try - RecName = element(1, Rec), - Dict:'#info-'(RecName, {index, 'Failed-AVP'}), - {'Failed-AVP', [FailedAvp]} - catch - error: _ -> - Avps = Dict:'#get-'('AVP', Rec), - A = #diameter_avp{name = 'Failed-AVP', - value = FailedAvp}, - {'AVP', [A|Avps]} - end. +values(Rec, F, Dict) -> + Dict:'#get-'(F, Rec). %% 3. Diameter Header %% @@ -1003,15 +1036,15 @@ answer_message(RC, origin_realm = {OR,_}}, #diameter_packet{avps = Avps, errors = Es}) -> - {Code, _, Vid} = Dict0:avp_header('Session-Id'), ['answer-message', {'Origin-Host', OH}, {'Origin-Realm', OR}, - {'Result-Code', RC}] - ++ session_id(Code, Vid, Avps) - ++ failed_avp(RC, Es). + {'Result-Code', RC} + | session_id(Dict0, Avps) + ++ failed_avp(RC, Es) + ++ proxy_info(Dict0, Avps)]. -session_id(Code, Vid, Avps) - when is_list(Avps) -> +session_id(Dict0, Avps) -> + {Code, _, Vid} = Dict0:avp_header('Session-Id'), try #diameter_avp{data = Bin} = find_avp(Code, Vid, Avps), [{'Session-Id', [Bin]}] @@ -1029,6 +1062,14 @@ failed_avp(RC, [_ | Es]) -> failed_avp(_, [] = No) -> No. +proxy_info(Dict0, Avps) -> + {Code, _, Vid} = Dict0:avp_header('Proxy-Info'), + [{'AVP', [A#diameter_avp{value = undefined} + || [#diameter_avp{code = C, vendor_id = I} = A | _] + <- Avps, + C == Code, + I == Vid]}]. + %% find_avp/3 %% Grouped ... @@ -1102,48 +1143,31 @@ find_avp(Code, VId, [_ | Avps]) -> %% Message sent as a header/avps list. incr_result(send = Dir, - #diameter_packet{msg = [#diameter_header{} = H | _]} - = Pkt, + Avp, + #diameter_packet{msg = [#diameter_header{} = H | _]}, TPid, - DictT) -> - incr_res(Dir, Pkt#diameter_packet{header = H}, TPid, DictT); - -%% Outgoing message as binary: don't count. (Sending binaries is only -%% partially supported.) -incr_result(send, #diameter_packet{header = undefined = No}, _, _) -> - No; + AppDict) -> + incr_result(Dir, Avp, H, [], TPid, AppDict); %% Incoming or outgoing. Outgoing with encode errors never gets here %% since encode fails. -incr_result(Dir, Pkt, TPid, DictT) -> - incr_res(Dir, Pkt, TPid, DictT). - -incr_res(Dir, - #diameter_packet{header = #diameter_header{is_error = E} - = Hdr, - errors = Es} - = Pkt, - TPid, - DictT) -> - {MsgDict, AppDict, Dict0} = DictT, +incr_result(Dir, Avp, Pkt, TPid, AppDict) -> + #diameter_packet{header = H, errors = Es} + = Pkt, + incr_result(Dir, Avp, H, Es, TPid, AppDict). + +%% incr_result/6 +incr_result(Dir, Avp, Hdr, Es, TPid, AppDict) -> Id = msg_id(Hdr, AppDict), %% Could be {relay, 0}, in which case the R-bit is redundant since %% only answers are being counted. Let it be however, so that the %% same tuple is in both send/recv and result code counters. %% Count incoming decode errors. - recv /= Dir orelse [] == Es orelse incr_error(Dir, Id, TPid, AppDict), - - %% Exit on a missing result code. - T = rc_counter(MsgDict, Dir, Pkt), - T == false andalso ?LOGX(no_result_code, {MsgDict, Dir, Hdr}), - {Ctr, RC, Avp} = T, - - %% Or on an inappropriate value. - is_result(RC, E, Dict0) - orelse ?LOGX(invalid_error_bit, {MsgDict, Dir, Hdr, Avp}), + send == Dir orelse [] == Es orelse incr_error(Dir, Id, TPid, AppDict), + Ctr = rcc(Avp), incr(TPid, {Id, Dir, Ctr}), Ctr. @@ -1188,7 +1212,50 @@ is_result(RC, true, _) -> incr(TPid, Counter) -> diameter_stats:incr(Counter, TPid, 1). -%% rc_counter/3 +%% rcc/1 + +rcc(#diameter_avp{name = 'Result-Code' = Name, value = V}) -> + {Name, head(V)}; + +rcc(#diameter_avp{name = 'Experimental-Result', value = V}) -> + head(V). + +%% head/1 + +head([V|_]) -> + V; +head(V) -> + V. + +%% rcv/1 + +rcv(#diameter_avp{name = N, value = V}) -> + rcv(N, head(V)). + +%% rcv/2 + +rcv('Experimental-Result', {_,_,N}) -> + N; + +rcv('Result-Code', N) -> + N. + +%% get_result/4 + +%% Message sent as binary: no checks or counting. +get_result(_, _, _, #diameter_packet{header = undefined}) -> + false; + +get_result(Dir, MsgDict, Dict0, Pkt) -> + Avp = get_result(MsgDict, msg(Dir, Pkt)), + Hdr = Pkt#diameter_packet.header, + %% Exit on a missing result code or inappropriate value. + Avp == false + andalso ?LOGX(no_result_code, {MsgDict, Dir, Hdr}), + E = Hdr#diameter_header.is_error, + is_result(rcv(Avp), E, Dict0) + orelse ?LOGX(invalid_error_bit, {MsgDict, Dir, Hdr, Avp}), + Avp. %% RFC 3588, 7.6: %% @@ -1196,46 +1263,29 @@ incr(TPid, Counter) -> %% applications MUST include either one Result-Code AVP or one %% Experimental-Result AVP. -rc_counter(Dict, Dir, #diameter_packet{header = H, - avps = As, - msg = Msg}) +%% msg/2 + +msg(Dir, #diameter_packet{header = H, + avps = As, + msg = Msg}) when Dir == recv; %% decoded incoming Msg == undefined -> %% relayed outgoing - rc_counter(Dict, [H|As]); - -rc_counter(Dict, _, #diameter_packet{msg = Msg}) -> - rc_counter(Dict, Msg). - -rc_counter(Dict, Msg) -> - rcc(get_result(Dict, Msg)). - -rcc(#diameter_avp{name = 'Result-Code' = Name, value = N} = A) - when is_integer(N) -> - {{Name, N}, N, A}; - -rcc(#diameter_avp{name = 'Result-Code' = Name, value = [N|_]} = A) - when is_integer(N) -> - {{Name, N}, N, A}; + [H|As]; -rcc(#diameter_avp{name = 'Experimental-Result', value = {_,_,N} = T} = A) - when is_integer(N) -> - {T, N, A}; - -rcc(#diameter_avp{name = 'Experimental-Result', value = [{_,_,N} = T|_]} = A) - when is_integer(N) -> - {T, N, A}; - -rcc(_) -> - false. +msg(_, #diameter_packet{msg = Msg}) -> + Msg. %% get_result/2 get_result(Dict, Msg) -> try [throw(A) || N <- ['Result-Code', 'Experimental-Result'], - #diameter_avp{} = A <- [get_avp(Dict, N, Msg)]] + #diameter_avp{} = A <- [get_avp(Dict, N, Msg)], + is_integer(catch rcv(A))], + false catch - #diameter_avp{} = A -> A + #diameter_avp{} = A -> + A end. x(T) -> @@ -1359,7 +1409,7 @@ make_opts([T | _], _, _, _, _, _) -> send_request({{TPid, _Caps} = TC, App} = Transport, - #{sequence := Mask} + #{sequence := Mask, traffic_counters := Count} = SvcOpts, Msg0, CallOpts, @@ -1375,9 +1425,15 @@ send_request({{TPid, _Caps} = TC, App} SvcOpts, ReqPkt), eval_packet(EncPkt, Fs), - T = send_R(ReqPkt, EncPkt, Transport, CallOpts, Caller, SvcName), + T = send_R(ReqPkt, + EncPkt, + Transport, + CallOpts, + Caller, + Count, + SvcName), Ans = recv_answer(SvcName, App, CallOpts, T), - handle_answer(SvcName, SvcOpts, App, Ans); + handle_answer(SvcName, Count, SvcOpts, App, Ans); {discard, Reason} -> {error, Reason}; discard -> @@ -1520,6 +1576,7 @@ send_R(ReqPkt, {{TPid, _Caps} = TC, #diameter_app{dictionary = AppDict}}, #options{timeout = Timeout}, {Pid, Ref}, + Count, SvcName) -> Req = #request{ref = Ref, caller = Pid, @@ -1527,7 +1584,7 @@ send_R(ReqPkt, peer = TC, packet = ReqPkt}, - incr(send, EncPkt, TPid, AppDict), + Count andalso incr(send, EncPkt, TPid, AppDict), {TRef, MRef} = zend_requezt(TPid, EncPkt, Req, SvcName, Timeout), Pid ! Ref, %% tell caller a send has been attempted {TRef, MRef, Req}. @@ -1559,15 +1616,16 @@ failover(SvcName, App, Req, CallOpts) -> CallOpts, SvcName). -%% handle_answer/4 +%% handle_answer/5 -handle_answer(SvcName, _, App, {error, Req, Reason}) -> +handle_answer(SvcName, _, _, App, {error, Req, Reason}) -> #request{packet = Pkt, peer = {_TPid, _Caps} = TC} = Req, cb(App, handle_error, [Reason, msg(Pkt), SvcName, TC]); handle_answer(SvcName, + Count, SvcOpts, #diameter_app{id = Id, dictionary = AppDict, @@ -1581,43 +1639,50 @@ handle_answer(SvcName, #request{peer = {TPid, _}} = Req, - incr(recv, DecPkt, TPid, AppDict), - - AnsPkt = try - incr_result(recv, DecPkt, TPid, {MsgDict, AppDict, Dict0}) - of - _ -> DecPkt - catch - exit: {no_result_code, _} -> - %% RFC 6733 requires one of Result-Code or - %% Experimental-Result, but the decode will have - %% detected a missing AVP. If both are optional in - %% the dictionary then this isn't a decode error: - %% just continue on. - DecPkt; - exit: {invalid_error_bit, {_, _, _, Avp}} -> - #diameter_packet{errors = Es} - = DecPkt, - E = {5004, Avp}, - DecPkt#diameter_packet{errors = [E|Es]} - end, - - handle_answer(AnsPkt, SvcName, App, AE, Req). + answer(answer(DecPkt, TPid, MsgDict, AppDict, Dict0, Count), + SvcName, + App, + AE, + Req). + +%% answer/6 + +answer(DecPkt, TPid, MsgDict, AppDict, Dict0, Count) -> + Count andalso incr(recv, DecPkt, TPid, AppDict), + try get_result(recv, MsgDict, Dict0, DecPkt) of + Avp -> + Count andalso false /= Avp + andalso incr_result(recv, Avp, DecPkt, TPid, AppDict), + DecPkt + catch + exit: {no_result_code, _} -> + %% RFC 6733 requires one of Result-Code or + %% Experimental-Result, but the decode will have + %% detected a missing AVP. If both are optional in + %% the dictionary then this isn't a decode error: + %% just continue on. + DecPkt; + exit: {invalid_error_bit, {_, _, _, Avp}} -> + #diameter_packet{errors = Es} + = DecPkt, + E = {5004, Avp}, + DecPkt#diameter_packet{errors = [E|Es]} + end. -%% handle_answer/5 +%% answer/5 -handle_answer(#diameter_packet{errors = Es} - = Pkt, - SvcName, - App, - AE, - #request{peer = {_TPid, _Caps} = TC, - packet = P}) +answer(#diameter_packet{errors = Es} + = Pkt, + SvcName, + App, + AE, + #request{peer = {_TPid, _Caps} = TC, + packet = P}) when callback == AE; [] == Es -> cb(App, handle_answer, [Pkt, msg(P), SvcName, TC]); -handle_answer(#diameter_packet{header = H}, SvcName, _, AE, _) -> +answer(#diameter_packet{header = H}, SvcName, _, AE, _) -> handle_error(H, SvcName, AE). %% handle_error/3 @@ -1830,10 +1895,8 @@ get_destination(Dict, Msg) -> [str(get_avp_value(Dict, D, Msg)) || D <- ['Destination-Realm', 'Destination-Host']]. -%% This is not entirely correct. The avp could have an arity 1, in -%% which case an empty list is a DiameterIdentity of length 0 rather -%% than the list of no values we treat it as by mapping to undefined. -%% This behaviour is documented. +%% A DiameterIdentity has length at least one, so an empty list is not +%% a Realm/Host. str([]) -> undefined; str(T) -> @@ -1841,16 +1904,12 @@ str(T) -> %% get_avp/3 %% -%% Find an AVP in a message of one of three forms: -%% -%% - a message record (as generated from a .dia spec) or -%% - a list of an atom message name followed by 2-tuple, avp name/value pairs. -%% - a list of a #diameter_header{} followed by #diameter_avp{} records, -%% -%% In the first two forms a dictionary module is used at encode to -%% identify the type of the AVP and its arity in the message in -%% question. The third form allows messages to be sent as is, without -%% a dictionary, which is needed in the case of relay agents, for one. +%% Find an AVP in a message in one of the decoded formats, or as a +%% header/avps list. There are only four AVPs that are extracted here: +%% Result-Code and Experimental-Result in order when constructing +%% counter keys, and Destination-Host/Realm when selecting a next-hop +%% peer. Experimental-Result is the only of type Grouped, and is given +%% special treatment in order to return the value as a record. %% Messages will be header/avps list as a relay and the only AVP's we %% look for are in the common dictionary. This is required since the @@ -1859,37 +1918,58 @@ str(T) -> get_avp(?RELAY, Name, Msg) -> get_avp(?BASE, Name, Msg); -%% Message as a header/avps list. +%% Message as header/avps list. get_avp(Dict, Name, [#diameter_header{} | Avps]) -> try - {Code, _, VId} = Dict:avp_header(Name), - find_avp(Code, VId, Avps) - of - A -> - (avp_decode(Dict, Name, ungroup(A)))#diameter_avp{name = Name} + {Code, _, Vid} = Dict:avp_header(Name), + A = find_avp(Code, Vid, Avps), + avp_decode(Dict, Name, ungroup(A)) catch error: _ -> undefined end; -%% Outgoing message as a name/values list. +%% Message as name/values list ... get_avp(_, Name, [_MsgName | Avps]) -> - case lists:keyfind(Name, 1, Avps) of + case find(Name, Avps) of {_, V} -> - #diameter_avp{name = Name, value = V}; + #diameter_avp{name = Name, value = value(Name, V)}; _ -> undefined end; -%% Message is typically a record but not necessarily. +%% ... or record. get_avp(Dict, Name, Rec) -> - try - #diameter_avp{name = Name, value = Dict:'#get-'(Name, Rec)} + try Dict:'#get-'(Name, Rec) of + V -> + #diameter_avp{name = Name, value = value(Name, V)} catch error:_ -> undefined end. +value('Experimental-Result' = N, #{'Vendor-Id' := Vid, + 'Experimental-Result-Code' := RC}) -> + {N, Vid, RC}; +value('Experimental-Result' = N, [{'Experimental-Result-Code', RC}, + {'Vendor-Id', Vid}]) -> + {N, Vid, RC}; +value('Experimental-Result' = N, [{'Vendor-Id', Vid}, + {'Experimental-Result-Code', RC}]) -> + {N, Vid, RC}; +value(_, V) -> + V. + +%% find/2 + +find(Key, Map) + when is_map(Map) -> + maps:find(Key, Map); + +find(Key, List) + when is_list(List) -> + lists:keyfind(Key, 1, List). + %% get_avp_value/3 get_avp_value(Dict, Name, Msg) -> @@ -1909,18 +1989,25 @@ ungroup(Avp) -> %% avp_decode/3 +%% Ensure Experimental-Result is decoded as record, since this format +%% is used for counter keys. +avp_decode(Dict, 'Experimental-Result' = N, #diameter_avp{data = Bin} + = Avp) + when is_binary(Bin) -> + {V,_} = Dict:avp(decode, Bin, N, decode_opts(Dict)), + Avp#diameter_avp{name = N, value = V}; + avp_decode(Dict, Name, #diameter_avp{value = undefined, data = Bin} - = Avp) -> - try Dict:avp(decode, Bin, Name, decode_opts(Dict)) of - V -> - Avp#diameter_avp{value = V} - catch - error:_ -> - Avp - end; -avp_decode(_, _, #diameter_avp{} = Avp) -> - Avp. + = Avp) + when is_binary(Bin) -> + V = Dict:avp(decode, Bin, Name, decode_opts(Dict)), + Avp#diameter_avp{name = Name, value = V}; + +avp_decode(_, Name, #diameter_avp{} = Avp) -> + Avp#diameter_avp{name = Name}. + +%% cb/3 cb(#diameter_app{module = [_|_] = M}, F, A) -> eval(M, F, A). @@ -1933,7 +2020,9 @@ choose(false, _, X) -> X. %% Decode options sufficient for AVP extraction. decode_opts(Dict) -> - #{string_decode => false, + #{decode_format => record, + string_decode => false, strict_mbit => false, failed_avp => false, - dictionary => Dict}. + module => Dict, + app_dictionary => Dict}. diff --git a/lib/diameter/src/base/diameter_watchdog.erl b/lib/diameter/src/base/diameter_watchdog.erl index a63425d92a..43623334a9 100644 --- a/lib/diameter/src/base/diameter_watchdog.erl +++ b/lib/diameter/src/base/diameter_watchdog.erl @@ -72,12 +72,11 @@ restrict := boolean(), suspect := non_neg_integer(), %% OKAY -> SUSPECT okay := non_neg_integer()}, %% REOPEN -> OKAY - codec :: #{string_decode := false, + codec :: #{decode_format := none, + string_decode := false, strict_mbit := boolean(), - failed_avp := false, rfc := 3588 | 6733, - ordered_encode := false, - incoming_maxlen := diameter:message_length()}, + ordered_encode := false}, shutdown = false :: boolean()}). %% --------------------------------------------------------------------------- @@ -135,13 +134,6 @@ i({Ack, T, Pid, {Opts, putr(restart, {T, Opts, Svc, SvcOpts}), %% save seeing it in trace putr(dwr, dwr(Caps)), %% Nodes = restrict_nodes(Restrict), - CodecKeys = [string_decode, - strict_mbit, - incoming_maxlen, - spawn_opt, - rfc, - ordered_encode], - #watchdog{parent = Pid, transport = start(T, Opts, SvcOpts, Nodes, Dict0, Svc), tw = proplists:get_value(watchdog_timer, @@ -149,14 +141,23 @@ i({Ack, T, Pid, {Opts, ?DEFAULT_TW_INIT), receive_data = RecvData, dictionary = Dict0, - config = - maps:without(CodecKeys, - config(SvcOpts#{restrict => restrict(Nodes), - suspect => 1, - okay => 3}, - Opts)), - codec = maps:with(CodecKeys, SvcOpts#{string_decode := false, - ordered_encode => false})}. + config = maps:with([sequence, + restrict_connections, + restrict, + suspect, + okay], + config(SvcOpts#{restrict => restrict(Nodes), + suspect => 1, + okay => 3}, + Opts)), + codec = maps:with([decode_format, + strict_mbit, + string_decode, + rfc, + ordered_encode], + SvcOpts#{decode_format := none, + string_decode := false, + ordered_encode => false})}. wait(Ref, Pid) -> receive diff --git a/lib/diameter/src/compiler/diameter_codegen.erl b/lib/diameter/src/compiler/diameter_codegen.erl index f56e4a5249..4e6fe32d69 100644 --- a/lib/diameter/src/compiler/diameter_codegen.erl +++ b/lib/diameter/src/compiler/diameter_codegen.erl @@ -21,15 +21,14 @@ -module(diameter_codegen). %% -%% This module generates erl/hrl files for encode/decode modules -%% from the orddict parsed from a dictionary file (.dia) by -%% diameter_dict_util. The generated code is simple (one-liners), -%% the generated functions being called by code included iin the -%% generated modules from diameter_gen.hrl. The orddict itself is -%% returned by dict/0 in the generated module and diameter_dict_util -%% calls this function when importing dictionaries as a consequence -%% of @inherits sections. That is, @inherits introduces a dependency -%% on the beam file of another dictionary. +%% This module generates erl/hrl files for encode/decode modules from +%% the orddict parsed from a dictionary file by diameter_dict_util. +%% The generated code is simple (one-liners), and is called from +%% diameter_gen. The orddict itself is returned by dict/0 in the +%% generated module and diameter_dict_util calls this function when +%% importing dictionaries as a consequence of @inherits sections. That +%% is, @inherits introduces a dependency on the beam file of another +%% dictionary. %% -export([from_dict/4, diff --git a/lib/diameter/src/compiler/diameter_dict_util.erl b/lib/diameter/src/compiler/diameter_dict_util.erl index f9f2b02e94..7b53e51cb6 100644 --- a/lib/diameter/src/compiler/diameter_dict_util.erl +++ b/lib/diameter/src/compiler/diameter_dict_util.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2016. All Rights Reserved. +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -923,7 +923,7 @@ xa([D|_] = Ds, [[Qual, D, {_, Line, AvpName}] | Avps], Dict, Key, Name) -> store_new({Key, {Name, AvpName}}, [Line, Qual, D], Dict, - [Name, Line], + [AvpName, Line], avp_already_referenced), Key, Name); diff --git a/lib/diameter/src/compiler/diameter_exprecs.erl b/lib/diameter/src/compiler/diameter_exprecs.erl index 9a0cb6baf2..143dede037 100644 --- a/lib/diameter/src/compiler/diameter_exprecs.erl +++ b/lib/diameter/src/compiler/diameter_exprecs.erl @@ -110,9 +110,9 @@ %% parse_transform/2 parse_transform(Forms, _Options) -> - Rs = [R || {attribute, _, record, R} <- Forms], - Es = lists:append([E || {attribute, _, export_records, E} <- Forms]), {H,T} = lists:splitwith(fun is_head/1, Forms), + Rs = [R || {attribute, _, record, R} <- H], + Es = lists:append([E || {attribute, _, export_records, E} <- H]), H ++ [a_export(Es) | f_accessors(Es, Rs)] ++ T. is_head(T) -> diff --git a/lib/diameter/src/diameter.appup.src b/lib/diameter/src/diameter.appup.src index 07d0389bfd..7566cf25c3 100644 --- a/lib/diameter/src/diameter.appup.src +++ b/lib/diameter/src/diameter.appup.src @@ -52,7 +52,8 @@ {"1.11.2", [{restart_application, diameter}]}, %% 18.3 {"1.12", [{restart_application, diameter}]}, %% 19.0 {"1.12.1", [{restart_application, diameter}]}, %% 19.1 - {"1.12.2", [{restart_application, diameter}]} %% 19.3 + {"1.12.2", [{restart_application, diameter}]}, %% 19.3 + {"2.0", [{restart_application, diameter}]} %% 20.0 ], [ {"0.9", [{restart_application, diameter}]}, @@ -86,6 +87,7 @@ {"1.11.2", [{restart_application, diameter}]}, {"1.12", [{restart_application, diameter}]}, {"1.12.1", [{restart_application, diameter}]}, - {"1.12.2", [{restart_application, diameter}]} + {"1.12.2", [{restart_application, diameter}]}, + {"2.0", [{restart_application, diameter}]} ] }. diff --git a/lib/diameter/src/dict/doic_rfc7683.dia b/lib/diameter/src/dict/doic_rfc7683.dia new file mode 100644 index 0000000000..2b7804115e --- /dev/null +++ b/lib/diameter/src/dict/doic_rfc7683.dia @@ -0,0 +1,50 @@ +;; +;; %CopyrightBegin% +;; +;; Copyright Ericsson AB 2017. All Rights Reserved. +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. +;; +;; %CopyrightEnd% +;; + +@name diameter_gen_doic_rfc7683 +@prefix diameter_doic + +@avp_types + + OC-Supported-Features 621 Grouped - + OC-Feature-Vector 622 Unsigned64 - + OC-OLR 623 Grouped - + OC-Sequence-Number 624 Unsigned64 - + OC-Validity-Duration 625 Unsigned32 - + OC-Report-Type 626 Enumerated - + OC-Reduction-Percentage 627 Unsigned32 - + +@enum OC-Report-Type + + HOST_REPORT 0 + REALM_REPORT 1 + +@grouped + + OC-Supported-Features ::= < AVP Header: 621 > + [ OC-Feature-Vector ] + * [ AVP ] + + OC-OLR ::= < AVP Header: 623 > + < OC-Sequence-Number > + < OC-Report-Type > + [ OC-Reduction-Percentage ] + [ OC-Validity-Duration ] + * [ AVP ] diff --git a/lib/diameter/src/modules.mk b/lib/diameter/src/modules.mk index bb3b234d20..bb86de016a 100644 --- a/lib/diameter/src/modules.mk +++ b/lib/diameter/src/modules.mk @@ -24,6 +24,7 @@ DICTS = \ base_rfc6733 \ base_accounting \ acct_rfc6733 \ + doic_rfc7683 \ relay # The yecc grammar for the dictionary parser. diff --git a/lib/diameter/src/transport/diameter_sctp.erl b/lib/diameter/src/transport/diameter_sctp.erl index 6a9f1f940b..64b34da690 100644 --- a/lib/diameter/src/transport/diameter_sctp.erl +++ b/lib/diameter/src/transport/diameter_sctp.erl @@ -79,7 +79,7 @@ -type option() :: {sender, boolean()} | sender | {packet, boolean() | raw} - | {message_cb, false | diameter:evaluable()}. + | {message_cb, false | diameter:eval()}. -type uint() :: non_neg_integer(). @@ -102,9 +102,12 @@ streams :: {uint(), uint()} %% {InStream, OutStream} counts | undefined, os = 0 :: uint(), %% next output stream + rotate = 1 :: boolean() | 0 | 1, %% rotate os? + unordered = false :: boolean() %% always send unordered? + | pos_integer(),% or if =< N outbound streams? packet = true :: boolean() %% legacy transport_data? | raw, - message_cb = false :: false | diameter:evaluable(), + message_cb = false :: false | diameter:eval(), send = false :: pid() | boolean()}). %% sending process %% Monitor process state. @@ -112,7 +115,7 @@ {transport :: pid(), ack = false :: boolean(), socket :: gen_sctp:sctp_socket(), - assoc_id :: gen_sctp:assoc_id()}). %% next output stream + assoc_id :: gen_sctp:assoc_id()}). %% Listener process state. -record(listener, @@ -120,7 +123,7 @@ socket :: gen_sctp:sctp_socket(), service :: pid(), %% service process pending = {0, queue:new()}, - opts :: [[match()] | boolean() | diameter:evaluable()]}). + opts :: [[match()] | boolean() | diameter:eval()]}). %% Field pending implements two queues: the first of transport-to-be %% processes to which an association has been assigned but for which %% diameter hasn't yet spawned a transport process, a short-lived @@ -156,12 +159,7 @@ start(T, Svc, Opts) = Svc, diameter_sctp_sup:start(), %% start supervisors on demand Addrs = Caps#diameter_caps.host_ip_address, - s(T, Addrs, Pid, lists:map(fun ip/1, Opts)). - -ip({ifaddr, A}) -> - {ip, A}; -ip(T) -> - T. + s(T, Addrs, Pid, Opts). %% A listener spawns transports either as a consequence of this call %% when there is not yet an association to assign it, or at comm_up on @@ -246,8 +244,11 @@ i(#monitor{transport = TPid} = S) -> i({listen, Ref, {Opts, SvcPid, Addrs}}) -> monitor(process, SvcPid), [_] = diameter_config:subscribe(Ref, transport), %% assert existence - {Split, Rest} - = proplists:split(Opts, [accept, packet, sender, message_cb]), + {Split, Rest} = proplists:split(Opts, [accept, + packet, + sender, + message_cb, + unordered]), OwnOpts = lists:append(Split), {LAs, Sock} = AS = open(Addrs, Rest, ?DEFAULT_PORT), ok = gen_sctp:listen(Sock, true), @@ -259,12 +260,16 @@ i({listen, Ref, {Opts, SvcPid, Addrs}}) -> opts = [[[M] || {accept, M} <- OwnOpts], proplists:get_value(packet, OwnOpts, true) | [proplists:get_value(K, OwnOpts, false) - || K <- [sender, message_cb]]]}; + || K <- [sender, message_cb, unordered]]]}; %% A connecting transport. i({connect, Pid, Opts, Addrs, Ref}) -> - {[Ps | Split], Rest} - = proplists:split(Opts, [rport, raddr, packet, sender, message_cb]), + {[Ps | Split], Rest} = proplists:split(Opts, [rport, + raddr, + packet, + sender, + message_cb, + unordered]), OwnOpts = lists:append(Split), CB = proplists:get_value(message_cb, OwnOpts, false), false == CB orelse (Pid ! {diameter, ack}), @@ -278,6 +283,7 @@ i({connect, Pid, Opts, Addrs, Ref}) -> mode = {connect, connect(Sock, RAs, RP, [])}, socket = Sock, message_cb = CB, + unordered = proplists:get_value(ordered, OwnOpts, false), packet = proplists:get_value(packet, OwnOpts, true), send = proplists:get_value(sender, OwnOpts, false)}; @@ -315,12 +321,13 @@ i({K, Ref}, #transport{mode = {accept, _}} = S) -> S#transport{parent = Pid}; {K, T, Opts} when K == peeloff -> %% association {sctp, Sock, _RA, _RP, _Data} = T, - [Matches, Packet, Sender, CB] = Opts, + [Matches, Packet, Sender, CB, Unordered] = Opts, ok = accept_peer(Sock, Matches), demonitor(Ref, [flush]), false == CB orelse (S#transport.parent ! {diameter, ack}), t(T, S#transport{socket = Sock, message_cb = CB, + unordered = Unordered, packet = Packet, send = Sender}); accept_timeout = T -> @@ -354,23 +361,35 @@ l([], Ref, T) -> %% open/3 open(Addrs, Opts, PortNr) -> - {LAs, Os} = addrs(Addrs, Opts), - {LAs, case gen_sctp:open(gen_opts(portnr(Os, PortNr))) of - {ok, Sock} -> - Sock; - {error, Reason} -> - x({open, Reason}) - end}. + case gen_sctp:open(gen_opts(portnr(addrs(Addrs, Opts), PortNr))) of + {ok, Sock} -> + {addrs(Sock), Sock}; + {error, Reason} -> + x({open, Reason}) + end. addrs(Addrs, Opts) -> - case proplists:split(Opts, [ip]) of - {[[]], _} -> - {Addrs, Opts ++ [{ip, A} || A <- Addrs]}; - {[As], Os} -> - LAs = [diameter_lib:ipaddr(A) || {ip, A} <- As], - {LAs, Os ++ [{ip, A} || A <- LAs]} + case lists:mapfoldl(fun ipaddr/2, false, Opts) of + {Os, true} -> + Os; + {_, false} -> + Opts ++ [{ip, A} || A <- Addrs] end. +ipaddr({K,A}, _) + when K == ifaddr; + K == ip -> + {{ip, ipaddr(A)}, true}; +ipaddr(T, B) -> + {T, B}. + +ipaddr(A) + when A == loopback; + A == any -> + A; +ipaddr(A) -> + diameter_lib:ipaddr(A). + portnr(Opts, PortNr) -> case proplists:get_value(port, Opts) of undefined -> @@ -379,6 +398,14 @@ portnr(Opts, PortNr) -> Opts end. +addrs(Sock) -> + case inet:socknames(Sock) of + {ok, As} -> + [A || {A,_} <- As]; + {error, Reason} -> + x({socknames, Reason}) + end. + %% x/1 x(Reason) -> @@ -565,7 +592,7 @@ transition(Msg, S) %% Deferred actions from a message_cb. transition({actions, Dir, Acts}, S) -> - actions(Acts, Dir, S); + setopts(ok, actions(Acts, Dir, S)); %% Request to close the transport connection. transition({diameter, {close, Pid}}, #transport{parent = Pid}) -> @@ -677,11 +704,16 @@ send(#diameter_packet{transport_data = {outstream, SId}} = S) -> send(SId rem OS, Msg, S); -%% ... or not: rotate through all streams. -send(Msg, #transport{streams = {_, OS}, +%% ... or not: rotate when sending on multiple streams ... +send(Msg, #transport{rotate = true, + streams = {_, OS}, os = N} = S) -> - send(N, Msg, S#transport{os = (N + 1) rem OS}). + send(N, Msg, S#transport{os = (N + 1) rem OS}); + +%% ... or send on the only stream available. +send(Msg, S) -> + send(0, Msg, S). %% send/3 @@ -749,7 +781,7 @@ recv({[#sctp_sndrcvinfo{assoc_id = Id}], _Bin} %% Inbound Diameter message. recv({[#sctp_sndrcvinfo{}], Bin} = Msg, S) when is_binary(Bin) -> - message(recv, Msg, S); + message(recv, Msg, recv(S)); recv({_, #sctp_shutdown_event{}}, _) -> stop; @@ -769,6 +801,41 @@ recv({_, #sctp_paddr_change{}}, _) -> recv({_, #sctp_pdapi_event{}}, _) -> ok. +%% recv/1 +%% +%% Start sending unordered after the second reception, so that an +%% outgoing CER/CEA will arrive at the peer before another request. + +recv(#transport{rotate = B} = S) + when is_boolean(B) -> + S; + +recv(#transport{rotate = 0, + streams = {_,OS}, + socket = Sock, + unordered = B} + = S) -> + ok = unordered(Sock, OS, B), + S#transport{rotate = 1 < OS}; + +recv(#transport{rotate = N} = S) -> + S#transport{rotate = N-1}. + +%% unordered/3 + +unordered(Sock, OS, B) + when B; + is_integer(B), OS =< B -> + inet:setopts(Sock, [{sctp_default_send_param, + #sctp_sndrcvinfo{flags = [unordered]}}]); + +unordered(_, OS, B) + when not B; + is_integer(B), B < OS -> + ok. + +%% publish/4 + publish(T, Ref, Id, Sock) -> true = diameter_reg:add_new({?MODULE, T, {Ref, {Id, Sock}}}), putr(?INFO_KEY, {gen_sctp, Sock}). %% for info/1 diff --git a/lib/diameter/src/transport/diameter_tcp.erl b/lib/diameter/src/transport/diameter_tcp.erl index a2f393d5d4..a8639baa11 100644 --- a/lib/diameter/src/transport/diameter_tcp.erl +++ b/lib/diameter/src/transport/diameter_tcp.erl @@ -87,8 +87,7 @@ module :: module() | undefined}). -type length() :: 0..16#FFFFFF. %% message length from Diameter header --type size() :: non_neg_integer(). %% accumulated binary size --type frag() :: {length(), size(), binary(), list(binary())} +-type frag() :: maybe_improper_list(length(), binary()) | binary(). -type connect_option() :: {raddr, inet:ip_address()} @@ -111,7 +110,7 @@ -type option() :: {port, non_neg_integer()} | {sender, boolean()} | sender - | {message_cb, false | diameter:evaluable()} + | {message_cb, false | diameter:eval()} | {fragment_timer, 0..16#FFFFFFFF}. %% Accepting/connecting transport process state. @@ -126,7 +125,7 @@ timeout :: infinity | 0..16#FFFFFFFF, %% fragment timeout tref = false :: false | reference(), %% fragment timer reference flush = false :: boolean(), %% flush fragment at timeout? - message_cb :: false | diameter:evaluable(), + message_cb :: false | diameter:eval(), send :: pid() | false}). %% sending process %% The usual transport using gen_tcp can be replaced by anything @@ -143,8 +142,7 @@ -> {ok, pid(), [inet:ip_address()]} when Ref :: diameter:transport_ref(); ({connect, Ref}, #diameter_service{}, [connect_option()]) - -> {ok, pid(), [inet:ip_address()]} - | {ok, pid()} + -> {ok, pid()} when Ref :: diameter:transport_ref(). start({T, Ref}, Svc, Opts) -> @@ -259,22 +257,14 @@ i(#monitor{parent = Pid, transport = TPid} = S) -> i({listen, Ref, {Mod, Opts, Addrs}}) -> [_] = diameter_config:subscribe(Ref, transport), %% assert existence - {[LA, LP], Rest} = proplists:split(Opts, [ip, port]), - LAddrOpt = get_addr(LA, Addrs), - LPort = get_port(LP), - {ok, LSock} = Mod:listen(LPort, gen_opts(LAddrOpt, Rest)), - LAddr = laddr(LAddrOpt, Mod, LSock), + {[LP], Rest} = proplists:split(Opts, [port]), + {ok, LSock} = Mod:listen(get_port(LP), gen_opts(Addrs, Rest)), + {ok, {LAddr, _}} = sockname(Mod, LSock), true = diameter_reg:add_new({?MODULE, listener, {Ref, {LAddr, LSock}}}), proc_lib:init_ack({ok, self(), {LAddr, LSock}}), #listener{socket = LSock, module = Mod}. -laddr([], Mod, Sock) -> - {ok, {Addr, _Port}} = sockname(Mod, Sock), - Addr; -laddr([{ip, Addr}], _, _) -> - Addr. - ssl_opts([]) -> false; ssl_opts([{ssl_options, true}]) -> @@ -309,24 +299,16 @@ init(accept = T, Ref, Mod, Pid, Opts, Addrs, SvcPid) -> Sock; init(connect = T, Ref, Mod, Pid, Opts, Addrs, _SvcPid) -> - {[LA, RA, RP], Rest} = proplists:split(Opts, [ip, raddr, rport]), - LAddrOpt = get_addr(LA, Addrs), + {[RA, RP], Rest} = proplists:split(Opts, [raddr, rport]), RAddr = get_addr(RA), RPort = get_port(RP), - proc_lib:init_ack(init_rc(LAddrOpt)), - Sock = ok(connect(Mod, RAddr, RPort, gen_opts(LAddrOpt, Rest))), + proc_lib:init_ack({ok, self()}), + Sock = ok(connect(Mod, RAddr, RPort, gen_opts(Addrs, Rest))), publish(Mod, T, Ref, Sock), - up(Pid, {RAddr, RPort}, LAddrOpt, Mod, Sock), + up(Pid, {RAddr, RPort}, Mod, Sock), Sock. -init_rc([{ip, Addr}]) -> - {ok, self(), [Addr]}; -init_rc([]) -> - {ok, self()}. - -up(Pid, Remote, [{ip, _Addr}], _, _) -> - diameter_peer:up(Pid, Remote); -up(Pid, Remote, [], Mod, Sock) -> +up(Pid, Remote, Mod, Sock) -> {Addr, _Port} = ok(sockname(Mod, Sock)), diameter_peer:up(Pid, Remote, [Addr]). @@ -383,25 +365,41 @@ l([{{?MODULE, listener, {_, AS}}, LPid}], _, _) -> l([], Ref, T) -> diameter_tcp_sup:start_child({listen, Ref, T}). -%% get_addr/1 +%% addrs/2 +%% +%% Take the first address from the service if several are specified +%% and not address is configured. + +addrs(Addrs, Opts) -> + case lists:mapfoldr(fun ipaddr/2, [], Opts) of + {Os, [_]} -> + Os; + {_, []} -> + Opts ++ [{ip, A} || [A|_] <- [Addrs]]; + {_, As} -> + ?ERROR({invalid_addrs, As, Addrs}) + end. -get_addr(As) -> - diameter_lib:ipaddr(addr(As, [])). +ipaddr({K,A}, As) + when K == ifaddr; + K == ip -> + {{ip, ipaddr(A)}, [A | As]}; +ipaddr(T, B) -> + {T, B}. -%% get_addr/2 +ipaddr(A) + when A == loopback; + A == any -> + A; +ipaddr(A) -> + diameter_lib:ipaddr(A). -get_addr([], []) -> - []; -get_addr(As, Def) -> - [{ip, diameter_lib:ipaddr(addr(As, Def))}]. +%% get_addr/1 -%% Take the first address from the service if several are unspecified. -addr([], [Addr | _]) -> - Addr; -addr([{_, Addr}], _) -> - Addr; -addr(As, Addrs) -> - ?ERROR({invalid_addrs, As, Addrs}). +get_addr([{_, Addr}]) -> + diameter_lib:ipaddr(Addr); +get_addr(Addrs) -> + ?ERROR({invalid_addrs, Addrs}). %% get_port/1 @@ -414,10 +412,15 @@ get_port(Ps) -> %% gen_opts/2 -gen_opts(LAddrOpt, Opts) -> +gen_opts(Addrs, Opts) -> + gen_opts(addrs(Addrs, Opts)). + +%% gen_opts/1 + +gen_opts(Opts) -> {L,_} = proplists:split(Opts, [binary, packet, active]), [[],[],[]] == L orelse ?ERROR({reserved_options, Opts}), - [binary, {packet, 0}, {active, false}] ++ LAddrOpt ++ Opts. + [binary, {packet, 0}, {active, false} | Opts]. %% --------------------------------------------------------------------------- %% # ports/1 @@ -599,11 +602,12 @@ t(T,S) -> %% Incoming packets. transition({P, Sock, Bin}, #transport{socket = Sock, - ssl = B} + ssl = B, + frag = Frag} = S) when P == ssl, true == B; P == tcp -> - recv(Bin, S#transport{active = false}); + recv(acc(Frag, Bin), S); %% Capabilties exchange has decided on whether or not to run over TLS. transition({diameter, {tls, Ref, Type, B}}, #transport{parent = Pid} @@ -640,7 +644,7 @@ transition(Msg, S) %% Deferred actions from a message_cb. transition({actions, Dir, Acts}, S) -> - actions(Acts, Dir, S); + setopts(actions(Acts, Dir, S)); %% Request to close the transport connection. transition({diameter, {close, Pid}}, #transport{parent = Pid, @@ -720,86 +724,77 @@ tls(accept, Sock, Opts) -> %% using Nagle. %% Receive packets until a full message is received, -recv(Bin, #transport{frag = Head} = S) -> - case rcv(Head, Bin) of - {Msg, B} -> %% have a complete message ... - message(recv, Msg, S#transport{frag = B}); - Frag -> %% read more on the socket - start_fragment_timer(setopts(S#transport{frag = Frag, - flush = false})) - end. -%% rcv/2 +recv({Msg, Rest}, S) -> %% have a complete message ... + recv(acc(Rest), message(recv, Msg, S)); + +recv(Frag, #transport{recv = B, + socket = Sock, + module = M} + = S) -> %% or not + B andalso setopts(M, Sock), + start_fragment_timer(S#transport{frag = Frag, + flush = false, + active = B}). -%% No previous fragment. -rcv(<<>>, Bin) -> - rcv(Bin); +%% acc/2 -%% Not even the first four bytes of the header. -rcv(Head, Bin) - when is_binary(Head) -> - rcv(<<Head/binary, Bin/binary>>); +%% Know how many bytes to extract. +acc([Len | Acc], Bin) -> + acc1(Len, <<Acc/binary, Bin/binary>>); -%% Or enough to know how many bytes to extract. -rcv({Len, N, Head, Acc}, Bin) -> - rcv(Len, N + size(Bin), Head, [Bin | Acc]). +%% Or not. +acc(Head, Bin) -> + acc(<<Head/binary, Bin/binary>>). -%% rcv/4 +%% acc1/3 %% Extract a message for which we have all bytes. -rcv(Len, N, Head, Acc) - when Len =< N -> - recv1(Len, bin(Head, Acc)); +acc1(Len, Bin) + when Len =< byte_size(Bin) -> + split_binary(Bin, Len); %% Wait for more packets. -rcv(Len, N, Head, Acc) -> - {Len, N, Head, Acc}. - -%% rcv/1 - -%% Nothing left. -rcv(<<>> = Bin) -> - Bin; - -%% The Message Length isn't even sufficient for a header. Chances are -%% things will go south from here but if we're lucky then the bytes we -%% have extend to an intended message boundary and we can recover by -%% simply receiving them. Make it so. -rcv(<<_:1/binary, Len:24, _/binary>> = Bin) - when Len < 20 -> - {Bin, <<>>}; - -%% Enough bytes to extract a message. -rcv(<<_:1/binary, Len:24, _/binary>> = Bin) - when Len =< size(Bin) -> - recv1(Len, Bin); - -%% Or not: wait for more packets. -rcv(<<_:1/binary, Len:24, _/binary>> = Head) -> - {Len, size(Head), Head, []}; +acc1(Len, Bin) -> + [Len | Bin]. + +%% acc/1 + +%% Don't match on Bin since this results in it being copied at the +%% next append according to the Efficiency Guide. This is also the +%% reason that the Len is extracted and maintained when accumulating +%% messages. The simplest implementation is just to accumulate a +%% binary and match <<_, Len:24, _/binary>> each time the length is +%% required, but the performance of this decays quadratically with the +%% message length, since the binary is then copied with each append of +%% additional bytes from gen_tcp. + +acc(Bin) + when 3 < byte_size(Bin) -> + {Head, _} = split_binary(Bin, 4), + [_,A,B,C] = binary_to_list(Head), + Len = (A bsl 16) bor (B bsl 8) bor C, + if Len < 20 -> + %% Message length isn't sufficient for a Diameter Header. + %% Chances are things will go south from here but if we're + %% lucky then the bytes we have extend to an intended + %% message boundary and we can recover by simply receiving + %% them. Make it so. + {Bin, <<>>}; + true -> + acc1(Len, Bin) + end; %% Not even 4 bytes yet. -rcv(Head) -> - Head. - -%% recv1/2 - -recv1(Len, Bin) -> - <<Msg:Len/binary, Rest/binary>> = Bin, - {Msg, Rest}. - -%% bin/2 - -bin(Head, Acc) -> - list_to_binary([Head | lists:reverse(Acc)]). +acc(Bin) -> + Bin. %% bin/1 -bin({_, _, Head, Acc}) -> - bin(Head, Acc); +bin([_ | Bin]) -> + Bin; -bin(Bin) - when is_binary(Bin) -> +bin(Bin) -> Bin. %% flush/1 @@ -911,14 +906,20 @@ setopts(#transport{socket = Sock, module = M} = S) when B, not A -> - case setopts(M, Sock, [{active, once}]) of - ok -> S#transport{active = true}; - X -> x({setopts, Sock, M, X}) %% possibly on peer disconnect - end; + setopts(M, Sock), + S#transport{active = true}; setopts(S) -> S. +%% setopts/2 + +setopts(M, Sock) -> + case setopts(M, Sock, [{active, once}]) of + ok -> ok; + X -> x({setopts, Sock, M, X}) %% possibly on peer disconnect + end. + %% portnr/2 portnr(gen_tcp, Sock) -> @@ -988,7 +989,7 @@ message(ack, _, #transport{message_cb = false} = S) -> S; message(Dir, Msg, #transport{message_cb = CB} = S) -> - recv(<<>>, actions(cb(CB, Dir, Msg), Dir, S)). + setopts(actions(cb(CB, Dir, Msg), Dir, S)). %% actions/3 diff --git a/lib/diameter/test/diameter_codec_SUITE.erl b/lib/diameter/test/diameter_codec_SUITE.erl index 9f08f49f9f..17112794e4 100644 --- a/lib/diameter/test/diameter_codec_SUITE.erl +++ b/lib/diameter/test/diameter_codec_SUITE.erl @@ -291,7 +291,8 @@ recode(Msg, Dict) -> recode(#diameter_packet{msg = Msg}, Dict). opts(Mod) -> - #{dictionary => Mod, + #{app_dictionary => Mod, + decode_format => record, string_decode => false, strict_mbit => true, rfc => 6733, diff --git a/lib/diameter/test/diameter_codec_SUITE_data/diameter_test_unknown.erl b/lib/diameter/test/diameter_codec_SUITE_data/diameter_test_unknown.erl index 700910878c..c6bba75f09 100644 --- a/lib/diameter/test/diameter_codec_SUITE_data/diameter_test_unknown.erl +++ b/lib/diameter/test/diameter_codec_SUITE_data/diameter_test_unknown.erl @@ -77,7 +77,8 @@ dec('BR', #diameter_packet ok. opts(Mod) -> - #{dictionary => Mod, + #{app_dictionary => Mod, + decode_format => record, string_decode => true, strict_mbit => true, rfc => 6733, diff --git a/lib/diameter/test/diameter_codec_test.erl b/lib/diameter/test/diameter_codec_test.erl index b548f85cb8..70e910ffa6 100644 --- a/lib/diameter/test/diameter_codec_test.erl +++ b/lib/diameter/test/diameter_codec_test.erl @@ -44,7 +44,8 @@ base() -> [] = run([[fun base/1, T] || T <- [zero, decode]]). gen(Mod) -> - Fs = [{Mod, F, []} || F <- [name, id, vendor_id, vendor_name]], + Fs = [{Mod, F, []} || Mod /= diameter_gen_doic_rfc7683, + F <- [name, id, vendor_id, vendor_name]], [] = run(Fs ++ [[fun gen/2, Mod, T] || T <- [messages, command_codes, avp_types, @@ -216,10 +217,11 @@ avp(Mod, encode = X, V, Name, _) -> opts(Mod) -> (opts())#{module => Mod, - dictionary => Mod}. + app_dictionary => Mod}. opts() -> - #{string_decode => true, + #{decode_format => record, + string_decode => true, strict_mbit => true, rfc => 6733, failed_avp => false}. diff --git a/lib/diameter/test/diameter_event_SUITE.erl b/lib/diameter/test/diameter_event_SUITE.erl index 57d3427037..a291dde6be 100644 --- a/lib/diameter/test/diameter_event_SUITE.erl +++ b/lib/diameter/test/diameter_event_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2013-2016. All Rights Reserved. +%% Copyright Ericsson AB 2013-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -63,7 +63,8 @@ {'Host-IP-Address', [?ADDR]}, {'Vendor-Id', 12345}, {'Product-Name', "OTP/diameter"}, - {'Acct-Application-Id', [D:id() || D <- Dicts]} + {'Acct-Application-Id', [D:id() || D <- Dicts]}, + {decode_format, map} | [{application, [{dictionary, D}, {module, #diameter_callback{}}]} || D <- Dicts]]). @@ -111,7 +112,8 @@ up(Config) -> {Svc, Ref} = connect(Config, [{connect_timer, 5000}, {watchdog_timer, 15000}]), start = event(Svc), - {up, Ref, {TPid, Caps}, Cfg, #diameter_packet{}} = event(Svc), + {up, Ref, {TPid, Caps}, Cfg, #diameter_packet{msg = M}} = event(Svc), + ['CEA' | #{}] = M, %% assert {watchdog, Ref, _, {initial, okay}, _} = event(Svc), %% Kill the transport process and see that the connection is %% reestablished after a watchdog timeout, not after connect_timer @@ -131,8 +133,9 @@ down(Config) -> {connect_timer, 5000}, {watchdog_timer, 20000}]), start = event(Svc), - {closed, Ref, {'CEA', ?NO_COMMON_APP, _, #diameter_packet{}}, _} + {closed, Ref, {'CEA', ?NO_COMMON_APP, _, #diameter_packet{msg = M}}, _} = event(Svc), + ['CEA' | #{}] = M, %% assert {reconnect, Ref, _} = event(Svc, 4000, 10000). %% Connect with matching capabilities but have the server delay its diff --git a/lib/diameter/test/diameter_examples_SUITE.erl b/lib/diameter/test/diameter_examples_SUITE.erl index eb99f10fe6..ee44ed8dc9 100644 --- a/lib/diameter/test/diameter_examples_SUITE.erl +++ b/lib/diameter/test/diameter_examples_SUITE.erl @@ -344,7 +344,7 @@ top(Dir, LibDir) -> start({server, Prot}) -> ok = diameter:start(), ok = server:start(), - {ok, Ref} = server:listen(Prot), + {ok, Ref} = server:listen({Prot, any, 3868}), [_] = ?util:lport(Prot, Ref), ok; @@ -352,7 +352,7 @@ start({client = Svc, Prot}) -> ok = diameter:start(), true = diameter:subscribe(Svc), ok = client:start(), - {ok, Ref} = client:connect(Prot), + {ok, Ref} = client:connect({Prot, loopback, loopback, 3868}), receive #diameter_event{info = {up, Ref, _, _, _}} -> ok end; start(Config) -> diff --git a/lib/diameter/test/diameter_traffic_SUITE.erl b/lib/diameter/test/diameter_traffic_SUITE.erl index 84b41f14b7..c224f9a27e 100644 --- a/lib/diameter/test/diameter_traffic_SUITE.erl +++ b/lib/diameter/test/diameter_traffic_SUITE.erl @@ -20,6 +20,7 @@ %% %% Tests of traffic between two Diameter nodes, one client, one server. +%% The traffic isn't meant to be sensible, just to exercise code. %% -module(diameter_traffic_SUITE). @@ -27,15 +28,18 @@ -export([suite/0, all/0, groups/0, + init_per_suite/0, init_per_suite/1, end_per_suite/1, + init_per_group/1, init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2]). %% testcases --export([start/1, +-export([rfc4005/1, + start/1, start_services/1, add_transports/1, result_codes/1, @@ -46,6 +50,7 @@ send_protocol_error/1, send_experimental_result/1, send_arbitrary/1, + send_proxy_info/1, send_unknown/1, send_unknown_short/1, send_unknown_mandatory/1, @@ -63,6 +68,7 @@ send_invalid_reject/1, send_unexpected_mandatory_decode/1, send_unexpected_mandatory/1, + send_too_many/1, send_long/1, send_maxlen/1, send_nopeer/1, @@ -98,18 +104,20 @@ stop/1]). %% diameter callbacks --export([peer_up/3, - peer_down/3, - pick_peer/6, pick_peer/7, - prepare_request/5, prepare_request/6, - prepare_retransmit/5, - handle_answer/6, handle_answer/7, - handle_error/6, - handle_request/3]). +-export([peer_up/4, + peer_down/4, + pick_peer/7, pick_peer/8, + prepare_request/6, prepare_request/7, + prepare_retransmit/6, + handle_answer/7, handle_answer/8, + handle_error/7, + handle_request/4]). %% diameter_{tcp,sctp} callbacks -export([message/3]). +-include_lib("kernel/include/inet_sctp.hrl"). + -include("diameter.hrl"). -include("diameter_gen_base_rfc3588.hrl"). -include("diameter_gen_base_accounting.hrl"). @@ -119,13 +127,22 @@ %% =========================================================================== +%% Fraction of shuffle/parallel groups to randomly skip. +-define(SKIP, 0.25). + +%% Positive number of testcases from which to select (randomly) from +%% tc(), the list of testcases to run, or [] to run all. The random +%% selection is to limit the time it takes for the suite to run. +-define(LIMIT, #{tcp => 42, sctp => 5}). + -define(util, diameter_util). -define(A, list_to_atom). -define(L, atom_to_list). +-define(B, iolist_to_binary). %% Don't use is_record/2 since dictionary hrl's aren't included. -%% (Since they define conflicting reqcords with the same names.) +%% (Since they define conflicting records with the same names.) -define(is_record(Rec, Name), (Name == element(1, Rec))). -define(ADDR, {127,0,0,1}). @@ -138,14 +155,14 @@ %% Sequence mask for End-to-End and Hop-by-Hop identifiers. -define(CLIENT_MASK, {1,26}). %% 1 in top 6 bits -%% How to construct messages, as record or list. --define(ENCODINGS, [list, record]). +%% How to construct outgoing messages. +-define(ENCODINGS, [list, record, map]). -%% How to send answers, in a diameter_packet or not. --define(CONTAINERS, [pkt, msg]). +%% How to decode incoming messages. +-define(DECODINGS, [record, none, map, list, record_from_map]). -%% Which common dictionary to use in the clients. --define(RFCS, [rfc3588, rfc6733]). +%% Which dictionary to use in the clients. +-define(RFCS, [rfc3588, rfc6733, rfc4005]). %% Whether to decode stringish Diameter types to strings, or leave %% them as binary. @@ -163,13 +180,12 @@ -record(group, {transport, strings, + encoding, client_service, - client_encoding, - client_dict0, + client_dict, client_sender, server_service, - server_encoding, - server_container, + server_decoding, server_sender, server_throttle}). @@ -182,34 +198,37 @@ %% A common match when receiving answers in a client. -define(answer_message(SessionId, ResultCode), - ['answer-message', - {'Session-Id', SessionId}, - {'Origin-Host', _}, - {'Origin-Realm', _}, - {'Result-Code', ResultCode} - | _]). + ['answer-message' | #{'Session-Id' := SessionId, + 'Origin-Host' := _, + 'Origin-Realm' := _, + 'Result-Code' := ResultCode}]). -define(answer_message(ResultCode), - ?answer_message(_, ResultCode)). + ['answer-message' | #{'Origin-Host' := _, + 'Origin-Realm' := _, + 'Result-Code' := ResultCode}]). %% Config for diameter:start_service/2. --define(SERVICE(Name, Decode), +-define(SERVICE(Name, Grp), [{'Origin-Host', Name ++ "." ++ ?REALM}, {'Origin-Realm', ?REALM}, {'Host-IP-Address', [?ADDR]}, {'Vendor-Id', 12345}, {'Product-Name', "OTP/diameter"}, - {'Auth-Application-Id', [?DIAMETER_APP_ID_COMMON]}, - {'Acct-Application-Id', [?DIAMETER_APP_ID_ACCOUNTING]}, + {'Auth-Application-Id', [0]}, %% common messages + {'Acct-Application-Id', [3]}, %% base accounting {restrict_connections, false}, - {string_decode, Decode}, + {string_decode, Grp#group.strings}, + {avp_dictionaries, [diameter_gen_doic_rfc7683]}, {incoming_maxlen, 1 bsl 21} | [{application, [{dictionary, D}, - {module, ?MODULE}, + {module, [?MODULE, Grp]}, {answer_errors, callback}]} || D <- [diameter_gen_base_rfc3588, diameter_gen_base_accounting, diameter_gen_base_rfc6733, - diameter_gen_acct_rfc6733]]]). + diameter_gen_acct_rfc6733, + nas4005], + D /= nas4005 orelse have_nas()]]). -define(SUCCESS, ?'DIAMETER_BASE_RESULT-CODE_SUCCESS'). @@ -227,6 +246,8 @@ ?'DIAMETER_BASE_RESULT-CODE_AVP_UNSUPPORTED'). -define(UNSUPPORTED_VERSION, ?'DIAMETER_BASE_RESULT-CODE_UNSUPPORTED_VERSION'). +-define(TOO_MANY, + ?'DIAMETER_BASE_RESULT-CODE_AVP_OCCURS_TOO_MANY_TIMES'). -define(REALM_NOT_SERVED, ?'DIAMETER_BASE_RESULT-CODE_REALM_NOT_SERVED'). -define(UNABLE_TO_DELIVER, @@ -254,64 +275,75 @@ suite() -> [{timetrap, {seconds, 10}}]. all() -> - [start, result_codes, {group, traffic}, empty, stop]. + [rfc4005, start, result_codes, {group, traffic}, empty, stop]. +%% Redefine this to run one or more groups for debugging purposes. +-define(GROUPS, []). +%-define(GROUPS, [[tcp,rfc6733,record,map,false,false,false,false]]). + +%% Issues with gen_sctp sporadically cause huge numbers of failed +%% testcases when running testcases in parallel. groups() -> - [{P, [P], Ts} || Ts <- [tc(tc())], P <- [shuffle, parallel]] + Names = names(), + [{P, [P], Ts} || Ts <- [tc()], P <- [shuffle, parallel]] ++ - [{?util:name([T,R,D,A,C,S,SS,ST,CS]), - [], - [{group, if S -> shuffle; not S -> parallel end}]} - || T <- ?TRANSPORTS, - R <- ?ENCODINGS, - D <- ?RFCS, - A <- ?ENCODINGS, - C <- ?CONTAINERS, - S <- ?STRING_DECODES, - SS <- ?SENDERS, - ST <- ?CALLBACKS, - CS <- ?SENDERS] + [{?util:name(N), [], [{group, if T == sctp; S -> shuffle; + true -> parallel end}]} + || [T,_,_,_,S|_] = N <- Names] ++ - [{T, [], groups([[T,R,D,A,C,S,SS,ST,CS] - || R <- ?ENCODINGS, - D <- ?RFCS, - A <- ?ENCODINGS, - C <- ?CONTAINERS, - S <- ?STRING_DECODES, - SS <- ?SENDERS, - ST <- ?CALLBACKS, - CS <- ?SENDERS, - SS orelse CS])} %% avoid deadlock + [{T, [], [{group, ?util:name(N)} || N <- names(Names, ?GROUPS), + T == hd(N)]} || T <- ?TRANSPORTS] ++ [{traffic, [], [{group, T} || T <- ?TRANSPORTS]}]. -%groups(_) -> %% debug -% Name = [sctp,record,rfc6733,record,pkt,false,false,false,false], -% [{group, ?util:name(Name)}]; -groups(Names) -> - [{group, ?util:name(L)} || L <- Names]. +names() -> + [[T,R,E,D,S,ST,SS,CS] || T <- ?TRANSPORTS, + R <- ?RFCS, + E <- ?ENCODINGS, + D <- ?DECODINGS, + S <- ?STRING_DECODES, + ST <- ?CALLBACKS, + SS <- ?SENDERS, + CS <- ?SENDERS]. + +names(Names, []) -> + [N || N <- Names, + [CS,SS|_] <- [lists:reverse(N)], + SS orelse CS]; %% avoid deadlock -%tc([N|_]) -> %% debug -% [N]; -tc(L) -> - L. +names(_, Names) -> + Names. %% -------------------- +init_per_suite() -> + [{timetrap, {seconds, 60}}]. + init_per_suite(Config) -> - [{sctp, ?util:have_sctp()} | Config]. + [{rfc4005, compile_and_load()}, {sctp, ?util:have_sctp()} | Config]. end_per_suite(_Config) -> + code:delete(nas4005), + code:purge(nas4005), ok. %% -------------------- +init_per_group(_) -> + [{timetrap, {seconds, 30}}]. + init_per_group(Name, Config) when Name == shuffle; Name == parallel -> - start_services(Config), - add_transports(Config); + case rand:uniform() < ?SKIP of + true -> + {skip, random}; + false -> + start_services(Config), + add_transports(Config), + replace({sleep, Name == parallel}, Config) + end; init_per_group(sctp = Name, Config) -> {_, Sctp} = lists:keyfind(Name, 1, Config), @@ -322,24 +354,22 @@ init_per_group(sctp = Name, Config) -> end; init_per_group(Name, Config) -> + Nas = proplists:get_value(rfc4005, Config, false), case ?util:name(Name) of - [T,R,D,A,C,S,SS,ST,CS] -> + [_,R,_,_,_,_,_,_] when R == rfc4005, true /= Nas -> + {skip, rfc4005}; + [T,R,E,D,S,ST,SS,CS] -> G = #group{transport = T, strings = S, + encoding = E, client_service = [$C|?util:unique_string()], - client_encoding = R, - client_dict0 = dict0(D), + client_dict = appdict(R), client_sender = CS, server_service = [$S|?util:unique_string()], - server_encoding = A, - server_container = C, + server_decoding = D, server_sender = SS, server_throttle = ST}, - %% Limit the number of testcase, since the number of - %% groups is large. - All = ?util:scramble(tc()), - TCs = lists:sublist(All, rand:uniform(32)), - [{group, G}, {runlist, TCs} | Config]; + replace([{group, G}, {runlist, select(T)}], Config); _ -> Config end. @@ -353,8 +383,26 @@ end_per_group(Name, Config) end_per_group(_, _) -> ok. +select(T) -> + try maps:get(T, ?LIMIT) of + N -> + lists:sublist(?util:scramble(tc()), max(5, rand:uniform(N))) + catch + error:_ -> ?LIMIT + end. + %% -------------------- +%% Work around common_test accumulating Config improperly, causing +%% testcases to get Config from groups and suites they're not in. +init_per_testcase(N, Config) + when N == rfc4005; + N == start; + N == result_codes; + N == empty; + N == stop -> + Config; + %% Skip testcases that can reasonably fail under SCTP. init_per_testcase(Name, Config) -> TCs = proplists:get_value(runlist, Config, []), @@ -368,12 +416,26 @@ init_per_testcase(Name, Config) -> _ when not Run -> {skip, random}; _ -> + proplists:get_value(sleep, Config, false) + andalso timer:sleep(rand:uniform(200)), [{testcase, Name} | Config] end. end_per_testcase(_, _) -> ok. +%% replace/2 +%% +%% Work around common_test running init functions inappropriately, and +%% this accumulating more config than expected. + +replace(Pairs, Config) + when is_list(Pairs) -> + lists:foldl(fun replace/2, Config, Pairs); + +replace({Key, _} = T, Config) -> + [T | lists:keydelete(Key, 1, Config)]. + %% -------------------- %% Testcases to run when services are started and connections @@ -386,6 +448,7 @@ tc() -> send_protocol_error, send_experimental_result, send_arbitrary, + send_proxy_info, send_unknown, send_unknown_short, send_unknown_mandatory, @@ -403,6 +466,7 @@ tc() -> send_invalid_reject, send_unexpected_mandatory_decode, send_unexpected_mandatory, + send_too_many, send_long, send_maxlen, send_nopeer, @@ -440,16 +504,26 @@ start(_Config) -> ok = diameter:start(). start_services(Config) -> - #group{strings = S, - client_service = CN, - server_service = SN} + #group{client_service = CN, + server_service = SN, + server_decoding = SD} + = Grp = group(Config), - ok = diameter:start_service(SN, ?SERVICE(SN, S)), - ok = diameter:start_service(CN, [{sequence, ?CLIENT_MASK} - | ?SERVICE(CN, S)]). + ok = diameter:start_service(SN, [{traffic_counters, bool()}, + {decode_format, SD} + | ?SERVICE(SN, Grp)]), + ok = diameter:start_service(CN, [{traffic_counters, bool()}, + {sequence, ?CLIENT_MASK}, + {decode_format, map}, + {strict_arities, decode} + | ?SERVICE(CN, Grp)]). + +bool() -> + 0.5 =< rand:uniform(). add_transports(Config) -> #group{transport = T, + encoding = E, client_service = CN, client_sender = CS, server_service = SN, @@ -459,30 +533,59 @@ add_transports(Config) -> LRef = ?util:listen(SN, [T, {sender, SS}, - {message_cb, ST andalso {?MODULE, message, [4]}} - | [{packet, hd(?util:scramble([false, raw]))} - || T == sctp andalso CS]], + {message_cb, ST andalso {?MODULE, message, [0]}}] + ++ [{packet, hd(?util:scramble([false, raw]))} + || T == sctp andalso CS] + ++ [{unordered, unordered()} || T == sctp], [{capabilities_cb, fun capx/2}, - {pool_size, 8}, - {applications, apps(rfc3588)}] + {pool_size, 8} + | server_apps()] ++ [{spawn_opt, {erlang, spawn, []}} || CS]), Cs = [?util:connect(CN, - [T, {sender, CS}], + [T, {sender, CS} | client_opts(T)], LRef, - [{id, Id}, - {capabilities, [{'Origin-State-Id', origin(Id)}]}, - {applications, apps(D)}]) - || A <- ?ENCODINGS, - C <- ?CONTAINERS, - D <- ?RFCS, - Id <- [{A,C}]], - %% The server uses the client's Origin-State-Id to decide how to - %% answer. + [{id, Id} + | client_apps(R, [{'Origin-State-Id', origin(Id)}])]) + || D <- ?DECODINGS, %% for multiple candidate peers + R <- ?RFCS, + R /= rfc4005 orelse have_nas(), + Id <- [{D,E}]], ?util:write_priv(Config, "transport", [LRef | Cs]). -apps(D0) -> - D = dict0(D0), - [acct(D), D]. +unordered() -> + element(rand:uniform(4), {true, false, 1, 2}). + +client_opts(tcp) -> + []; +client_opts(sctp) -> + [{unordered, unordered()} + | [{sctp_initmsg, #sctp_initmsg{num_ostreams = N, + max_instreams = 5}} + || N <- [rand:uniform(8)], + N =< 6]]. + +server_apps() -> + B = have_nas(), + [{applications, [diameter_gen_base_rfc3588, + diameter_gen_base_accounting] + ++ [nas4005 || B]}, + {capabilities, [{'Auth-Application-Id', [0] ++ [1 || B]}, %% common, NAS + {'Acct-Application-Id', [3]}]}]. %% accounting + +client_apps(D, Caps) -> + if D == rfc4005 -> + [{applications, [nas4005]}, + {capabilities, [{'Auth-Application-Id', [1]}, %% NAS + {'Acct-Application-Id', []} + | Caps]}]; + true -> + D0 = dict0(D), + [{applications, [acct(D0), D0]}, + {capabilities, Caps}] + end. + +have_nas() -> + false /= code:is_loaded(nas4005). remove_transports(Config) -> #group{client_service = CN, @@ -515,9 +618,16 @@ capx(_, #diameter_caps{origin_host = {OH,DH}}) -> %% =========================================================================== +%% Fail only this testcase if the RFC 4005 dictionary hasn't been +%% successfully compiled and loaded. +rfc4005(Config) -> + true = proplists:get_value(rfc4005, Config). + %% Ensure that result codes have the expected values. result_codes(_Config) -> - {2001, 3001, 3002, 3003, 3004, 3007, 3008, 3009, 5001, 5011, 5014} + {2001, + 3001, 3002, 3003, 3004, 3007, 3008, 3009, + 5001, 5009, 5011, 5014} = {?SUCCESS, ?COMMAND_UNSUPPORTED, ?UNABLE_TO_DELIVER, @@ -527,6 +637,7 @@ result_codes(_Config) -> ?INVALID_HDR_BITS, ?INVALID_AVP_BITS, ?AVP_UNSUPPORTED, + ?TOO_MANY, ?UNSUPPORTED_VERSION, ?INVALID_AVP_LENGTH}. @@ -534,8 +645,8 @@ result_codes(_Config) -> send_ok(Config) -> Req = ['ACR', {'Accounting-Record-Type', ?EVENT_RECORD}, {'Accounting-Record-Number', 1}], - - ['ACA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['ACA' | #{'Result-Code' := ?SUCCESS, + 'Session-Id' := _}] = call(Config, Req). %% Send an accounting ACR that the server answers badly to. @@ -551,7 +662,8 @@ send_eval(Config) -> Req = ['ACR', {'Accounting-Record-Type', ?EVENT_RECORD}, {'Accounting-Record-Number', 3}], - ['ACA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['ACA' | #{'Result-Code' := ?SUCCESS, + 'Session-Id' := _}] = call(Config, Req). %% Send an accounting ACR that the server tries to answer with an @@ -564,20 +676,87 @@ send_bad_answer(Config) -> = call(Config, Req). %% Send an ACR that the server callback answers explicitly with a -%% protocol error. +%% protocol error and some AVPs to check the decoding of. send_protocol_error(Config) -> Req = ['ACR', {'Accounting-Record-Type', ?EVENT_RECORD}, {'Accounting-Record-Number', 4}], - ?answer_message(?TOO_BUSY) - = call(Config, Req). + ['answer-message' | #{'Result-Code' := ?TOO_BUSY, + 'AVP' := [OLR | _]} = Avps] + = call(Config, Req), + + #diameter_avp{name = 'OC-OLR', + value = #{'OC-Sequence-Number' := 1, + 'OC-Report-Type' := 0, %% HOST_REPORT + 'OC-Reduction-Percentage' := [25], + 'OC-Validity-Duration' := [60], + 'AVP' := [OSF]}} + = OLR, + #diameter_avp{name = 'OC-Supported-Features', + value = #{} = Fs} + = OSF, + 0 = maps:size(Fs), + + #group{client_dict = D} = group(Config), + + if D == nas4005 -> + error = maps:find('Failed-AVP', Avps), + #{'AVP' := [_,Failed]} + = Avps, + #diameter_avp{name = 'Failed-AVP', + value = #{'AVP' := [NP,FR,AP]}} + = Failed, + #diameter_avp{name = 'NAS-Port', + value = 44} + = NP, + #diameter_avp{name = 'Firmware-Revision', + value = 12} + = FR, + #diameter_avp{name = 'Auth-Grace-Period', + value = 13} + = AP; + + D == diameter_gen_base_rfc3588; + D == diameter_gen_basr_accounting -> + error = maps:find('Failed-AVP', Avps), + #{'AVP' := [_,Failed]} + = Avps, + + #diameter_avp{name = 'Failed-AVP', + value = #{'AVP' := [NP,FR,AP]}} + = Failed, + #diameter_avp{name = undefined, + value = undefined} + = NP, + #diameter_avp{name = 'Firmware-Revision', + value = 12} + = FR, + #diameter_avp{name = 'Auth-Grace-Period', + value = 13} + = AP; + + D == diameter_gen_base_rfc6733; + D == diameter_gen_acct_rfc6733 -> + #{'Failed-AVP' := [#{'AVP' := [NP,FR,AP]}], + 'AVP' := [_]} + = Avps, + #diameter_avp{name = undefined, + value = undefined} + = NP, + #diameter_avp{name = 'Firmware-Revision', + value = 12} + = FR, + #diameter_avp{name = 'Auth-Grace-Period', + value = 13} + = AP + end. %% Send a 3xxx Experimental-Result in an answer not setting the E-bit %% and missing a Result-Code. send_experimental_result(Config) -> Req = ['ACR', {'Accounting-Record-Type', ?EVENT_RECORD}, {'Accounting-Record-Number', 5}], - ['ACA', {'Session-Id', _} | _] + ['ACA' | #{'Session-Id' := _}] = call(Config, Req). %% Send an ASR with an arbitrary non-mandatory AVP and expect success @@ -585,24 +764,37 @@ send_experimental_result(Config) -> send_arbitrary(Config) -> Req = ['ASR', {'AVP', [#diameter_avp{name = 'Product-Name', value = "XXX"}]}], - ['ASA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | Avps] + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS, + 'AVP' := [#diameter_avp{name = 'Product-Name', + value = V}]}] = call(Config, Req), - {'AVP', [#diameter_avp{name = 'Product-Name', - value = V}]} - = lists:last(Avps), "XXX" = string(V, Config). +%% Send Proxy-Info in an ASR that the peer answers with 3xxx, and +%% ensure that the AVP is returned. +send_proxy_info(Config) -> + H0 = ?B(?util:unique_string()), + S0 = ?B(?util:unique_string()), + Req = ['ASR', {'Proxy-Info', #{'Proxy-Host' => H0, + 'Proxy-State' => S0}}], + ['answer-message' | #{'Result-Code' := 3999, + 'Proxy-Info' := [#{'Proxy-Host' := H, + 'Proxy-State' := S}]}] + = call(Config, Req), + [H0, S0] = [?B(X) || X <- [H,S]]. + %% Send an unknown AVP (to some client) and check that it comes back. send_unknown(Config) -> Req = ['ASR', {'AVP', [#diameter_avp{code = 999, is_mandatory = false, data = <<17>>}]}], - ['ASA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | Avps] - = call(Config, Req), - {'AVP', [#diameter_avp{code = 999, - is_mandatory = false, - data = <<17>>}]} - = lists:last(Avps). + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS, + 'AVP' := [#diameter_avp{code = 999, + is_mandatory = false, + data = <<17>>}]}] + = call(Config, Req). %% Ditto, and point the AVP length past the end of the message. Expect %% 5014. @@ -613,28 +805,28 @@ send_unknown_short(Config, M, RC) -> Req = ['ASR', {'AVP', [#diameter_avp{code = 999, is_mandatory = M, data = <<17>>}]}], - ['ASA', {'Session-Id', _}, {'Result-Code', RC} | Avps] + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := RC, + 'Failed-AVP' := [#{'AVP' := [Avp]}]}] = call(Config, Req), - [#'diameter_base_Failed-AVP'{'AVP' = As}] - = proplists:get_value('Failed-AVP', Avps), - [#diameter_avp{code = 999, - is_mandatory = M, - data = <<17, _/binary>>}] %% extra bits from padding - = As. + #diameter_avp{code = 999, + is_mandatory = M, + data = <<17, _/binary>>} %% extra bits from padding + = Avp. %% Ditto but set the M flag. send_unknown_mandatory(Config) -> Req = ['ASR', {'AVP', [#diameter_avp{code = 999, is_mandatory = true, data = <<17>>}]}], - ['ASA', {'Session-Id', _}, {'Result-Code', ?AVP_UNSUPPORTED} | Avps] + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?AVP_UNSUPPORTED, + 'Failed-AVP' := [#{'AVP' := [Avp]}]}] = call(Config, Req), - [#'diameter_base_Failed-AVP'{'AVP' = As}] - = proplists:get_value('Failed-AVP', Avps), - [#diameter_avp{code = 999, - is_mandatory = true, - data = <<17>>}] - = As. + #diameter_avp{code = 999, + is_mandatory = true, + data = <<17>>} + = Avp. %% Ditto, and point the AVP length past the end of the message. Expect %% 5014 instead of 5001. @@ -647,15 +839,27 @@ send_unexpected_mandatory_decode(Config) -> Req = ['ASR', {'AVP', [#diameter_avp{code = 27, %% Session-Timeout is_mandatory = true, data = <<12:32>>}]}], - ['ASA', {'Session-Id', _}, {'Result-Code', ?AVP_UNSUPPORTED} | Avps] + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?AVP_UNSUPPORTED, + 'Failed-AVP' := [#{'AVP' := [Avp]}]}] + = call(Config, Req), + #diameter_avp{code = 27, + is_mandatory = true, + value = 12, + data = <<12:32>>} + = Avp. + +%% Try to two Auth-Application-Id in ASR expect 5009. +send_too_many(Config) -> + Req = ['ASR', {'Auth-Application-Id', [?APP_ID, 44]}], + + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?TOO_MANY, + 'Failed-AVP' := [#{'AVP' := [Avp]}]}] = call(Config, Req), - [#'diameter_base_Failed-AVP'{'AVP' = As}] - = proplists:get_value('Failed-AVP', Avps), - [#diameter_avp{code = 27, - is_mandatory = true, - value = 12, - data = <<12:32>>}] - = As. + #diameter_avp{name = 'Auth-Application-Id', + value = 44} + = Avp. %% Send an containing a faulty Grouped AVP (empty Proxy-Host in %% Proxy-Info) and expect that only the faulty AVP is sent in @@ -665,16 +869,13 @@ send_unexpected_mandatory_decode(Config) -> send_grouped_error(Config) -> Req = ['ASR', {'Proxy-Info', [[{'Proxy-Host', "abcd"}, {'Proxy-State', ""}]]}], - ['ASA', {'Session-Id', _}, {'Result-Code', ?INVALID_AVP_LENGTH} | Avps] + ['ASA' | #{'Session-Id' := _, + 'Result-Code' := ?INVALID_AVP_LENGTH, + 'Failed-AVP' := [#{'AVP' := [Avp]}]}] = call(Config, Req), - [#'diameter_base_Failed-AVP'{'AVP' = As}] - = proplists:get_value('Failed-AVP', Avps), - [#diameter_avp{name = 'Proxy-Info', - value = #'diameter_base_Proxy-Info' - {'Proxy-Host' = Empty, - 'Proxy-State' = undefined}}] - = As, - <<0>> = iolist_to_binary(Empty). + #diameter_avp{name = 'Proxy-Info', value = #{'Proxy-Host' := H}} + = Avp, + <<0>> = ?B(H). %% Send an STR that the server ignores. send_noreply(Config) -> @@ -702,7 +903,8 @@ send_error_bit(Config) -> %% Send a bad version and check that we get 5011. send_unsupported_version(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], - ['STA', {'Session-Id', _}, {'Result-Code', ?UNSUPPORTED_VERSION} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?UNSUPPORTED_VERSION}] = call(Config, Req). %% Send a request containing an AVP length > data size. @@ -722,16 +924,11 @@ send_zero_avp_length(Config) -> send_invalid_avp_length(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], - ['STA', {'Session-Id', _}, - {'Result-Code', ?INVALID_AVP_LENGTH}, - {'Origin-Host', _}, - {'Origin-Realm', _}, - {'User-Name', _}, - {'Class', _}, - {'Error-Message', _}, - {'Error-Reporting-Host', _}, - {'Failed-AVP', [#'diameter_base_Failed-AVP'{'AVP' = [_]}]} - | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?INVALID_AVP_LENGTH, + 'Origin-Host' := _, + 'Origin-Realm' := _, + 'Failed-AVP' := [#{'AVP' := [_]}]}] = call(Config, Req). %% Send a request containing 5xxx errors that the server rejects with @@ -747,14 +944,16 @@ send_invalid_reject(Config) -> send_unexpected_mandatory(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], - ['STA', {'Session-Id', _}, {'Result-Code', ?AVP_UNSUPPORTED} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?AVP_UNSUPPORTED}] = call(Config, Req). %% Send something long that will be fragmented by TCP. send_long(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}, {'User-Name', [binary:copy(<<$X>>, 1 bsl 20)]}], - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = call(Config, Req). %% Send something longer than the configure incoming_maxlen. @@ -797,7 +996,8 @@ send_any_2(Config) -> send_all_1(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], Realm = lists:foldr(fun(C,A) -> [C,A] end, [], ?REALM), - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = call(Config, Req, [{filter, {all, [{host, any}, {realm, Realm}]}}]). send_all_2(Config) -> @@ -826,13 +1026,13 @@ send_detach(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], Ref = make_ref(), ok = call(Config, Req, [{extra, [{self(), Ref}]}, detach]), - Ans = receive {Ref, T} -> T end, - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] - = Ans. + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] + = receive {Ref, T} -> T end. %% Send a request which can't be encoded and expect {error, encode}. send_encode_error(Config) -> - {error, encode} = call(Config, ['STR']). %% No Termination-Cause + {error, encode} = call(Config, ['STR', {'Termination-Cause', huh}]). %% Send with filtering and expect success. send_destination_1(Config) -> @@ -840,25 +1040,27 @@ send_destination_1(Config) -> = group(Config), Req = ['STR', {'Termination-Cause', ?LOGOUT}, {'Destination-Host', [?HOST(SN, ?REALM)]}], - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = call(Config, Req, [{filter, {all, [host, realm]}}]). send_destination_2(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}], - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = call(Config, Req, [{filter, {all, [host, realm]}}]). %% Send with filtering on and expect failure when specifying an %% unknown host or realm. send_destination_3(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}, - {'Destination-Realm', "unknown.org"}], + {'Destination-Realm', <<"unknown.org">>}], {error, no_connection} = call(Config, Req, [{filter, {all, [host, realm]}}]). send_destination_4(Config) -> #group{server_service = SN} = group(Config), Req = ['STR', {'Termination-Cause', ?LOGOUT}, - {'Destination-Host', [?HOST(SN, "unknown.org")]}], + {'Destination-Host', [?HOST(SN, ["unknown.org"])]}], {error, no_connection} = call(Config, Req, [{filter, {all, [host, realm]}}]). @@ -866,7 +1068,7 @@ send_destination_4(Config) -> %% an unknown host or realm. send_destination_5(Config) -> Req = ['STR', {'Termination-Cause', ?LOGOUT}, - {'Destination-Realm', "unknown.org"}], + {'Destination-Realm', [<<"unknown.org">>]}], ?answer_message(?REALM_NOT_SERVED) = call(Config, Req). send_destination_6(Config) -> @@ -908,7 +1110,8 @@ send_bad_filter(Config, F) -> %% Specify multiple filter options and expect them be conjunctive. send_multiple_filters_1(Config) -> Fun = fun(#diameter_caps{}) -> true end, - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = send_multiple_filters(Config, [host, {eval, Fun}]). send_multiple_filters_2(Config) -> E = {erlang, is_tuple, []}, @@ -919,7 +1122,8 @@ send_multiple_filters_3(Config) -> E2 = {erlang, is_tuple, []}, E3 = {erlang, is_record, [diameter_caps]}, E4 = [{erlang, is_record, []}, diameter_caps], - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = send_multiple_filters(Config, [{eval, E} || E <- [E1,E2,E3,E4]]). send_multiple_filters(Config, Fs) -> @@ -930,7 +1134,8 @@ send_multiple_filters(Config, Fs) -> %% only the return value from the prepare_request callback being %% significant. send_anything(Config) -> - ['STA', {'Session-Id', _}, {'Result-Code', ?SUCCESS} | _] + ['STA' | #{'Session-Id' := _, + 'Result-Code' := ?SUCCESS}] = call(Config, anything). %% =========================================================================== @@ -954,58 +1159,137 @@ call(Config, Req) -> call(Config, Req, Opts) -> Name = proplists:get_value(testcase, Config), - #group{client_service = CN, - client_encoding = ReqEncoding, - client_dict0 = Dict0} - = Group + #group{encoding = Enc, + client_service = CN, + client_dict = Dict0} = group(Config), diameter:call(CN, dict(Req, Dict0), - msg(Req, ReqEncoding, Dict0), - [{extra, [{Name, Group}, diameter_lib:now()]} | Opts]). + msg(Req, Enc, Dict0), + [{extra, [Name, diameter_lib:now()]} | Opts]). -origin({A,C}) -> - 2*codec(A) + container(C); +origin({D,E}) -> + 4*decode(D) + encode(E); origin(N) -> - {codec(N band 2), container(N rem 2)}. - -%% Map booleans, but the readable atoms are part of (constructed) -%% group names, so it's good that they're readable. - -codec(record) -> 0; -codec(list) -> 1; -codec(0) -> record; -codec(_) -> list. - -container(pkt) -> 0; -container(msg) -> 1; -container(0) -> pkt; -container(_) -> msg. + {decode(N bsr 2), encode(N rem 4)}. + +%% Map atoms. The atoms are part of (constructed) group names, so it's +%% good that they're readable. + +decode(record) -> 0; +decode(list) -> 1; +decode(map) -> 2; +decode(none) -> 3; +decode(record_from_map) -> 4; +decode(0) -> record; +decode(1) -> list; +decode(2) -> map; +decode(3) -> none; +decode(4) -> record_from_map. + +encode(record) -> 0; +encode(list) -> 1; +encode(map) -> 2; +encode(0) -> record; +encode(1) -> list; +encode(2) -> map. msg([H|_] = Msg, record = E, diameter_gen_base_rfc3588) when H == 'ACR'; H == 'ACA' -> msg(Msg, E, diameter_gen_base_accounting); + msg([H|_] = Msg, record = E, diameter_gen_base_rfc6733) when H == 'ACR'; H == 'ACA' -> msg(Msg, E, diameter_gen_acct_rfc6733); + msg([H|T], record, Dict) -> Dict:'#new-'(Dict:msg2rec(H), T); + +msg([H|As], map, _) + when is_list(As) -> + [H | maps:from_list(As)]; + msg(Msg, _, _) -> Msg. +to_map(#diameter_packet{msg = [_MsgName | Avps] = Msg}, + #group{server_decoding = map}) + when is_map(Avps) -> + Msg; + +to_map(#diameter_packet{msg = [MsgName | Avps]}, + #group{server_decoding = list}) -> + [MsgName | maps:from_list(Avps)]; + +to_map(#diameter_packet{header = H, msg = Rec}, + #group{server_decoding = D}) + when D == record; + D == record_from_map -> + rec_to_map(Rec, dict(H)); + +%% No record decode: do it ourselves. +to_map(#diameter_packet{header = H, + msg = Name, + bin = Bin}, + #group{server_decoding = none, + strings = B}) -> + Opts = #{decode_format => map, + string_decode => B, + avp_dictionaries => [diameter_gen_doic_rfc7683], + strict_mbit => true, + rfc => 6733}, + #diameter_packet{msg = [MsgName | _Map] = Msg} + = diameter_codec:decode(dict(H), Opts, Bin), + {MsgName, _} = {Name, Msg}, %% assert + Msg. + +dict(#diameter_header{application_id = Id, + cmd_code = Code}) -> + if Id == 1 -> + nas4005; + Code == 271 -> + diameter_gen_base_accounting; + true -> + diameter_gen_base_rfc3588 + end. + +rec_to_map(Rec, Dict) -> + [R | Vs] = Dict:'#get-'(Rec), + [Dict:rec2msg(R) | maps:from_list([T || {_,V} = T <- Vs, + V /= undefined, + V /= []])]. + +appdict(rfc4005) -> + nas4005; +appdict(D) -> + dict0(D). + dict0(D) -> ?A("diameter_gen_base_" ++ ?L(D)). -dict(Msg, Dict0) - when 'ACR' == hd(Msg); - 'ACA' == hd(Msg); - ?is_record(Msg, diameter_base_accounting_ACR); - ?is_record(Msg, diameter_base_accounting_ACA) -> +dict(Msg, Dict) -> + d(name(Msg), Dict). + +d(N, nas4005 = D) -> + if N == {list, 'answer-message'}; + N == {map, 'answer-message'}; + N == {record, 'diameter_base_answer-message'} -> + diameter_gen_base_rfc3588; + true -> + D + end; +d(N, Dict0) + when N == {list, 'ACR'}; + N == {list, 'ACA'}; + N == {map, 'ACR'}; + N == {map, 'ACA'}; + N == {record, diameter_base_accounting_ACR}; + N == {record, diameter_base_accounting_ACA} -> acct(Dict0); -dict(_, Dict0) -> +d(_, Dict0) -> Dict0. acct(diameter_gen_base_rfc3588) -> @@ -1014,53 +1298,60 @@ acct(diameter_gen_base_rfc6733) -> diameter_gen_acct_rfc6733. %% Set only values that aren't already. -set(_, [H|T], Vs) -> - [H | Vs ++ T]; -set(#group{client_dict0 = Dict0} = _Group, Rec, Vs) -> + +set(_, [N | As], Vs) -> + [N | if is_map(As) -> + maps:merge(maps:from_list(Vs), As); + is_list(As) -> + Vs ++ As + end]; + +set(#group{client_dict = Dict0} = _Group, Rec, Vs) -> Dict = dict(Rec, Dict0), lists:foldl(fun({F,_} = FV, A) -> - set(Dict, Dict:'#get-'(F, A), FV, A) + reset(Dict, Dict:'#get-'(F, A), FV, A) end, Rec, Vs). -set(Dict, E, FV, Rec) +reset(Dict, E, FV, Rec) when E == undefined; E == [] -> Dict:'#set-'(FV, Rec); -set(_, _, _, Rec) -> + +reset(_, _, _, Rec) -> Rec. %% =========================================================================== %% diameter callbacks -%% peer_up/3 +%% peer_up/4 -peer_up(_SvcName, _Peer, State) -> +peer_up(_SvcName, _Peer, State, _Group) -> State. %% peer_down/3 -peer_down(_SvcName, _Peer, State) -> +peer_down(_SvcName, _Peer, State, _Group) -> State. -%% pick_peer/6-7 +%% pick_peer/7-8 -pick_peer(Peers, _, [$C|_], _State, {Name, Group}, _) +pick_peer(Peers, _, [$C|_], _State, Group, Name, _) when Name /= send_detach -> find(Group, Peers). -pick_peer(_Peers, _, [$C|_], _State, {send_nopeer, _}, _, ?EXTRA) -> +pick_peer(_Peers, _, [$C|_], _State, _Group, send_nopeer, _, ?EXTRA) -> false; -pick_peer(Peers, _, [$C|_], _State, {send_detach, Group}, _, {_,_}) -> +pick_peer(Peers, _, [$C|_], _State, Group, send_detach, _, {_,_}) -> find(Group, Peers). -find(#group{client_service = CN, - server_encoding = A, - server_container = C}, +find(#group{encoding = E, + client_service = CN, + server_decoding = D}, [_|_] = Peers) -> - Id = {A,C}, + Id = {D,E}, [P] = [P || P <- Peers, id(Id, P, CN)], {ok, P}. @@ -1069,15 +1360,15 @@ id(Id, {Pid, _Caps}, SvcName) -> = diameter:service_info(SvcName, Pid), lists:member({id, Id}, Opts). -%% prepare_request/5-6 +%% prepare_request/6-7 -prepare_request(_Pkt, [$C|_], {_Ref, _Caps}, {send_discard, _}, _) -> +prepare_request(_Pkt, [$C|_], {_Ref, _Caps}, _, send_discard, _) -> {discard, unprepared}; -prepare_request(Pkt, [$C|_], {_Ref, Caps}, {Name, Group}, _) -> +prepare_request(Pkt, [$C|_], {_Ref, Caps}, Group, Name, _) -> {send, prepare(Pkt, Caps, Name, Group)}. -prepare_request(Pkt, [$C|_], {_Ref, Caps}, {send_detach, Group}, _, _) -> +prepare_request(Pkt, [$C|_], {_Ref, Caps}, Group, send_detach, _, _) -> {eval_packet, {send, prepare(Pkt, Caps, Group)}, [fun log/2, detach]}. log(#diameter_packet{bin = Bin} = P, T) @@ -1086,7 +1377,7 @@ log(#diameter_packet{bin = Bin} = P, T) %% prepare/4 -prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) +prepare(Pkt, Caps, N, #group{client_dict = Dict0} = Group) when N == send_unknown_short_mandatory; N == send_unknown_short -> Req = prepare(Pkt, Caps, Group), @@ -1106,7 +1397,7 @@ prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) <<H:Offset/binary, Len:24, T/binary>> = Bin, E#diameter_packet{bin = <<H/binary, (Len+9):24, T/binary>>}; -prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) +prepare(Pkt, Caps, N, #group{client_dict = Dict0} = Group) when N == send_long_avp_length; N == send_short_avp_length; N == send_zero_avp_length -> @@ -1132,7 +1423,7 @@ prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) T/binary, Hdr/binary, AL:24, Data/binary>>}; -prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) +prepare(Pkt, Caps, N, #group{client_dict = Dict0} = Group) when N == send_invalid_avp_length; N == send_invalid_reject -> Req = prepare(Pkt, Caps, Group), @@ -1147,7 +1438,7 @@ prepare(Pkt, Caps, N, #group{client_dict0 = Dict0} = Group) <<V, L:24, H/binary>> = H0, %% assert E#diameter_packet{bin = <<V, (L+4):24, H/binary, 16:24, 0:32, T/binary>>}; -prepare(Pkt, Caps, send_unexpected_mandatory, #group{client_dict0 = Dict0} +prepare(Pkt, Caps, send_unexpected_mandatory, #group{client_dict = Dict0} = Group) -> Req = prepare(Pkt, Caps, Group), #diameter_packet{bin = <<V, Len:24, T/binary>>} @@ -1157,7 +1448,7 @@ prepare(Pkt, Caps, send_unexpected_mandatory, #group{client_dict0 = Dict0} Avp = <<Code:32, Flags, 8:24>>, E#diameter_packet{bin = <<V, (Len+8):24, T/binary, Avp/binary>>}; -prepare(Pkt, Caps, send_grouped_error, #group{client_dict0 = Dict0} +prepare(Pkt, Caps, send_grouped_error, #group{client_dict = Dict0} = Group) -> Req = prepare(Pkt, Caps, Group), #diameter_packet{bin = Bin} @@ -1189,14 +1480,14 @@ prepare(Pkt, Caps, send_grouped_error, #group{client_dict0 = Dict0} Payload/binary, T/binary>>}; -prepare(Pkt, Caps, send_unsupported, #group{client_dict0 = Dict0} = Group) -> +prepare(Pkt, Caps, send_unsupported, #group{client_dict = Dict0} = Group) -> Req = prepare(Pkt, Caps, Group), #diameter_packet{bin = <<H:5/binary, _CmdCode:3/binary, T/binary>>} = E = diameter_codec:encode(Dict0, Pkt#diameter_packet{msg = Req}), E#diameter_packet{bin = <<H/binary, 42:24, T/binary>>}; -prepare(Pkt, Caps, send_unsupported_app, #group{client_dict0 = Dict0} +prepare(Pkt, Caps, send_unsupported_app, #group{client_dict = Dict0} = Group) -> Req = prepare(Pkt, Caps, Group), #diameter_packet{bin = <<H:8/binary, _ApplId:4/binary, T/binary>>} @@ -1223,93 +1514,120 @@ prepare(Pkt, Caps, _Name, Group) -> %% prepare/3 -prepare(#diameter_packet{msg = Req}, Caps, Group) - when ?is_record(Req, diameter_base_accounting_ACR); - 'ACR' == hd(Req) -> +prepare(#diameter_packet{msg = Req} = Pkt, Caps, Group) -> + set(name(Req), Pkt, Caps, Group). + +%% set/4 + +set(N, #diameter_packet{msg = Req}, Caps, Group) + when N == {record, diameter_base_accounting_ACR}; + N == {record, nas_ACR}; + N == {map, 'ACR'}; + N == {list, 'ACR'} -> #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, DR}} = Caps, - set(Group, Req, [{'Session-Id', diameter:session_id(OH)}, - {'Origin-Host', OH}, - {'Origin-Realm', OR}, - {'Destination-Realm', DR}]); + set(Group, Req, [{'Session-Id', [diameter:session_id(OH)]}, + {'Origin-Host', [OH]}, + {'Origin-Realm', [OR]}, + {'Destination-Realm', [DR]}]); -prepare(#diameter_packet{msg = Req}, Caps, Group) - when ?is_record(Req, diameter_base_ASR); - 'ASR' == hd(Req) -> +set(N, #diameter_packet{msg = Req}, Caps, Group) + when N == {record, diameter_base_ASR}; + N == {record, nas_ASR}; + N == {map, 'ASR'}; + N == {list, 'ASR'} -> #diameter_caps{origin_host = {OH, DH}, origin_realm = {OR, DR}} = Caps, - set(Group, Req, [{'Session-Id', diameter:session_id(OH)}, - {'Origin-Host', OH}, - {'Origin-Realm', OR}, - {'Destination-Host', DH}, - {'Destination-Realm', DR}, + set(Group, Req, [{'Session-Id', [diameter:session_id(OH)]}, + {'Origin-Host', [OH]}, + {'Origin-Realm', [OR]}, + {'Destination-Host', [DH]}, + {'Destination-Realm', [DR]}, {'Auth-Application-Id', ?APP_ID}]); -prepare(#diameter_packet{msg = Req}, Caps, Group) - when ?is_record(Req, diameter_base_STR); - 'STR' == hd(Req) -> +set(N, #diameter_packet{msg = Req}, Caps, Group) + when N == {record, diameter_base_STR}; + N == {record, nas_STR}; + N == {map, 'STR'}; + N == {list, 'STR'} -> #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, DR}} = Caps, - set(Group, Req, [{'Session-Id', diameter:session_id(OH)}, - {'Origin-Host', OH}, - {'Origin-Realm', OR}, - {'Destination-Realm', DR}, + set(Group, Req, [{'Session-Id', [diameter:session_id(OH)]}, + {'Origin-Host', [OH]}, + {'Origin-Realm', [OR]}, + {'Destination-Realm', [DR]}, {'Auth-Application-Id', ?APP_ID}]); -prepare(#diameter_packet{msg = Req}, Caps, Group) - when ?is_record(Req, diameter_base_RAR); - 'RAR' == hd(Req) -> +set(N, #diameter_packet{msg = Req}, Caps, Group) + when N == {record, diameter_base_RAR}; + N == {record, nas_RAR}; + N == {map, 'RAR'}; + N == {list, 'RAR'} -> #diameter_caps{origin_host = {OH, DH}, origin_realm = {OR, DR}} = Caps, - set(Group, Req, [{'Session-Id', diameter:session_id(OH)}, - {'Origin-Host', OH}, - {'Origin-Realm', OR}, - {'Destination-Host', DH}, - {'Destination-Realm', DR}, + set(Group, Req, [{'Session-Id', [diameter:session_id(OH)]}, + {'Origin-Host', [OH]}, + {'Origin-Realm', [OR]}, + {'Destination-Host', [DH]}, + {'Destination-Realm', [DR]}, {'Auth-Application-Id', ?APP_ID}]). -%% prepare_retransmit/5 +%% name/1 + +name([H|#{}]) -> + {map, H}; + +name([H|_]) -> + {list, H}; + +name(Rec) -> + try + {record, element(1, Rec)} + catch + error: badarg -> + false + end. -prepare_retransmit(_Pkt, false, _Peer, _Name, _Group) -> +%% prepare_retransmit/6 + +prepare_retransmit(_Pkt, false, _Peer, _Group, _Name, _) -> discard. -%% handle_answer/6-7 +%% handle_answer/7-8 -handle_answer(Pkt, Req, [$C|_], Peer, {Name, Group}, _) -> +handle_answer(Pkt, Req, [$C|_], Peer, Group, Name, _) -> answer(Pkt, Req, Peer, Name, Group). -handle_answer(Pkt, Req, [$C|_], Peer, {send_detach = Name, Group}, _, X) -> +handle_answer(Pkt, Req, [$C|_], Peer, Group, send_detach = Name, _, X) -> {Pid, Ref} = X, Pid ! {Ref, answer(Pkt, Req, Peer, Name, Group)}. -answer(Pkt, Req, _Peer, Name, #group{client_dict0 = Dict0}) -> +answer(Pkt, Req, _Peer, Name, #group{client_dict = Dict0}) -> #diameter_packet{header = H, msg = Ans, errors = Es} = Pkt, ApplId = app(Req, Name, Dict0), #diameter_header{application_id = ApplId} = H, %% assert - Dict = dict(Ans, Dict0), - [R | Vs] = Dict:'#get-'(answer(Ans, Es, Name)), - [Dict:rec2msg(R) | Vs]. + answer(Ans, Es, Name). %% Missing Result-Code and inappropriate Experimental-Result-Code. -answer(Rec, Es, send_experimental_result) -> +answer(Ans, Es, send_experimental_result) -> [{5004, #diameter_avp{name = 'Experimental-Result'}}, {5005, #diameter_avp{name = 'Result-Code'}}] = Es, - Rec; + Ans; %% An inappropriate E-bit results in a decode error ... -answer(Rec, Es, send_bad_answer) -> +answer(Ans, Es, send_bad_answer) -> [{5004, #diameter_avp{name = 'Result-Code'}} | _] = Es, - Rec; + Ans; %% ... while other errors are reflected in Failed-AVP. -answer(Rec, [], _) -> - Rec. +answer(Ans, [], _) -> + Ans. app(_, send_unsupported_app, _) -> ?BAD_APP; @@ -1317,25 +1635,29 @@ app(Req, _, Dict0) -> Dict = dict(Req, Dict0), Dict:id(). -%% handle_error/6 +%% handle_error/7 -handle_error(timeout = Reason, _Req, [$C|_], _Peer, _, Time) -> +handle_error(timeout = Reason, _Req, [$C|_], _Peer, _, _, Time) -> Now = diameter_lib:now(), {Reason, {diameter_lib:timestamp(Time), diameter_lib:timestamp(Now), diameter_lib:micro_diff(Now, Time)}}; -handle_error(Reason, _Req, [$C|_], _Peer, _, _Time) -> +handle_error(Reason, _Req, [$C|_], _Peer, _, _, _Time) -> {error, Reason}. -%% handle_request/3 +%% handle_request/4 %% Note that diameter will set Result-Code and Failed-AVPs if %% #diameter_packet.errors is non-null. -handle_request(#diameter_packet{header = H, msg = M, avps = As}, +handle_request(#diameter_packet{header = H, avps = As} + = Pkt, _, - {_Ref, Caps}) -> + {_Ref, Caps}, + #group{encoding = E, + server_decoding = D} + = Grp) -> #diameter_header{end_to_end_id = EI, hop_by_hop_id = HI} = H, @@ -1343,24 +1665,62 @@ handle_request(#diameter_packet{header = H, msg = M, avps = As}, V = EI bsr B, %% assert V = HI bsr B, %% #diameter_caps{origin_state_id = {_,[Id]}} = Caps, - answer(origin(Id), request(M, [H|As], Caps)). + {D,E} = T = origin(Id), %% assert + wrap(T, H, request(to_map(Pkt, Grp), [H|As], Caps)). + +wrap(Id, H, {Tag, Action, Post}) -> + {Tag, wrap(Id, H, Action), Post}; -answer(T, {Tag, Action, Post}) -> - {Tag, answer(T, Action), Post}; -answer(_, {reply, [#diameter_header{} | _]} = T) -> +wrap(_, _, {reply, [#diameter_header{} | _]} = T) -> T; -answer({A,C}, {reply, Ans}) -> - answer(C, {reply, msg(Ans, A, diameter_gen_base_rfc3588)}); -answer(pkt, {reply, Ans}) - when not is_record(Ans, diameter_packet) -> - {reply, #diameter_packet{msg = Ans}}; -answer(_, T) -> + +wrap({_,E}, H, {reply, Ans}) -> + Msg = base_to_nas(msg(Ans, E, diameter_gen_base_rfc3588), H), + {reply, wrap(Msg)}; + +wrap(_, _, T) -> T. +%% Randomly wrap the answer in a diameter_packet. + +wrap(#diameter_packet{} = Pkt) -> + Pkt; + +wrap(Msg) -> + case rand:uniform(2) of + 1 -> #diameter_packet{msg = Msg}; + 2 -> Msg + end. + +%% base_to_nas/2 + +base_to_nas(#diameter_packet{msg = Msg} = Pkt, H) -> + Pkt#diameter_packet{msg = base_to_nas(Msg, H)}; + +base_to_nas(Rec, #diameter_header{application_id = 1}) + when is_tuple(Rec), not ?is_record(Rec, 'diameter_base_answer-message') -> + D = case element(1, Rec) of + diameter_base_accounting_ACA -> + diameter_gen_base_accounting; + _ -> + diameter_gen_base_rfc3588 + end, + [R | Values] = D:'#get-'(Rec), + "diameter_base_" ++ N = ?L(R), + Name = ?A("nas_" ++ if N == "accounting_ACA" -> + "ACA"; + true -> + N + end), + nas4005:'#new-'([Name | Values]); + +base_to_nas(Msg, _) -> + Msg. + %% request/3 %% send_experimental_result -request(#diameter_base_accounting_ACR{'Accounting-Record-Number' = 5}, +request(['ACR' | #{'Accounting-Record-Number' := 5}], [Hdr | Avps], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> @@ -1393,14 +1753,14 @@ request(Msg, _Avps, Caps) -> %% request/2 %% send_nok -request(#diameter_base_accounting_ACR{'Accounting-Record-Number' = 0}, +request(['ACR' | #{'Accounting-Record-Number' := 0}], _) -> {eval_packet, {protocol_error, ?INVALID_AVP_BITS}, [fun log/2, invalid]}; %% send_bad_answer -request(#diameter_base_accounting_ACR{'Session-Id' = SId, - 'Accounting-Record-Type' = RT, - 'Accounting-Record-Number' = 2 = RN}, +request(['ACR' | #{'Session-Id' := SId, + 'Accounting-Record-Type' := RT, + 'Accounting-Record-Number' := 2 = RN}], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> Ans = ['ACA', {'Result-Code', ?SUCCESS}, @@ -1414,9 +1774,9 @@ request(#diameter_base_accounting_ACR{'Session-Id' = SId, msg = Ans}}; %% send_eval -request(#diameter_base_accounting_ACR{'Session-Id' = SId, - 'Accounting-Record-Type' = RT, - 'Accounting-Record-Number' = 3 = RN}, +request(['ACR' | #{'Session-Id' := SId, + 'Accounting-Record-Type' := RT, + 'Accounting-Record-Number' := 3 = RN}], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> Ans = ['ACA', {'Result-Code', ?SUCCESS}, @@ -1428,9 +1788,9 @@ request(#diameter_base_accounting_ACR{'Session-Id' = SId, {eval, {reply, Ans}, {erlang, now, []}}; %% send_ok -request(#diameter_base_accounting_ACR{'Session-Id' = SId, - 'Accounting-Record-Type' = RT, - 'Accounting-Record-Number' = 1 = RN}, +request(['ACR' | #{'Session-Id' := SId, + 'Accounting-Record-Type' := RT, + 'Accounting-Record-Number' := 1 = RN}], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> {reply, ['ACA', {'Result-Code', ?SUCCESS}, @@ -1441,48 +1801,69 @@ request(#diameter_base_accounting_ACR{'Session-Id' = SId, {'Accounting-Record-Number', RN}]}; %% send_protocol_error -request(#diameter_base_accounting_ACR{'Accounting-Record-Number' = 4}, +request(['ACR' | #{'Accounting-Record-Number' := 4}], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> + %% Include a DOIC AVP that will be encoded/decoded because of + %% avp_dictionaries config. + OLR = #{'OC-Sequence-Number' => 1, + 'OC-Report-Type' => 0, %% HOST_REPORT + 'OC-Reduction-Percentage' => [25], + 'OC-Validity-Duration' => [60], + 'AVP' => [{'OC-Supported-Features', []}]}, + %% Include a NAS Failed-AVP AVP that will only be decoded under + %% that application. Encode as 'AVP' since RFC 3588 doesn't list + %% Failed-AVP in the answer-message grammar while RFC 6733 does. + NP = #diameter_avp{data = {nas4005, 'NAS-Port', 44}}, + FR = #diameter_avp{name = 'Firmware-Revision', value = 12}, %% M=0 + AP = #diameter_avp{name = 'Auth-Grace-Period', value = 13}, %% M=1 + Failed = #diameter_avp{data = {diameter_gen_base_rfc3588, + 'Failed-AVP', + [{'AVP', [NP,FR,AP]}]}}, Ans = ['answer-message', {'Result-Code', ?TOO_BUSY}, {'Origin-Host', OH}, - {'Origin-Realm', OR}], + {'Origin-Realm', OR}, + {'AVP', [{'OC-OLR', OLR}, Failed]}], {reply, Ans}; -request(#diameter_base_ASR{'Session-Id' = SId, - 'AVP' = Avps}, +%% send_proxy_info +request(['ASR' | #{'Proxy-Info' := _}], + _) -> + {protocol_error, 3999}; + +request(['ASR' | #{'Session-Id' := SId} = Avps], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> {reply, ['ASA', {'Result-Code', ?SUCCESS}, {'Session-Id', SId}, {'Origin-Host', OH}, {'Origin-Realm', OR}, - {'AVP', Avps}]}; + {'AVP', maps:get('AVP', Avps, [])}]}; %% send_invalid_reject -request(#diameter_base_STR{'Termination-Cause' = ?USER_MOVED}, +request(['STR' | #{'Termination-Cause' := ?USER_MOVED}], _Caps) -> {protocol_error, ?TOO_BUSY}; %% send_noreply -request(#diameter_base_STR{'Termination-Cause' = T}, +request(['STR' | #{'Termination-Cause' := T}], _Caps) when T /= ?LOGOUT -> discard; %% send_destination_5 -request(#diameter_base_STR{'Destination-Realm' = R}, +request(['STR' | #{'Destination-Realm' := R}], #diameter_caps{origin_realm = {OR, _}}) when R /= undefined, R /= OR -> {protocol_error, ?REALM_NOT_SERVED}; %% send_destination_6 -request(#diameter_base_STR{'Destination-Host' = [H]}, +request(['STR' | #{'Destination-Host' := [H]}], #diameter_caps{origin_host = {OH, _}}) when H /= OH -> {protocol_error, ?UNABLE_TO_DELIVER}; -request(#diameter_base_STR{'Session-Id' = SId}, +request(['STR' | #{'Session-Id' := SId}], #diameter_caps{origin_host = {OH, _}, origin_realm = {OR, _}}) -> {reply, ['STA', {'Result-Code', ?SUCCESS}, @@ -1491,7 +1872,7 @@ request(#diameter_base_STR{'Session-Id' = SId}, {'Origin-Realm', OR}]}; %% send_error/send_timeout -request(#diameter_base_RAR{}, _Caps) -> +request(['RAR' | #{}], _Caps) -> receive after 2000 -> {protocol_error, ?TOO_BUSY} end. %% message/3 @@ -1505,8 +1886,8 @@ message(Dir, #diameter_packet{bin = Bin}, N) -> message(Dir, Bin, N); %% incoming request -message(recv, <<_:32, 1, _/bits>> = Bin, N) -> - [Bin, 1 < N, fun ?MODULE:message/3, N-1]; +message(recv, <<_:32, 1:1, _/bits>> = Bin, N) -> + [Bin, N < 16, fun ?MODULE:message/3, N+1]; %% incoming answer message(recv, Bin, _) -> @@ -1517,9 +1898,35 @@ message(send, Bin, _) -> [Bin]; %% sent request -message(ack, <<_:32, 1, _/bits>>, _) -> +message(ack, <<_:32, 1:1, _/bits>>, _) -> []; %% sent answer or discarded request message(ack, _, N) -> - [0 =< N, fun ?MODULE:message/3, N+1]. + [N =< 16, fun ?MODULE:message/3, N-1]. + +%% ------------------------------------------------------------------------ + +compile_and_load() -> + try + Path = hd([P || H <- [[here(), ".."], [code:lib_dir(diameter)]], + P <- [filename:join(H ++ ["examples", + "dict", + "rfc4005_nas.dia"])], + {ok, _} <- [file:read_file_info(P)]]), + {ok, [Forms]} + = diameter_make:codec(Path, [return, + forms, + {name, "nas4005"}, + {prefix, "nas"}, + {inherits, "common/diameter_gen_base_rfc3588"}]), + {ok, nas4005, Bin, []} = compile:forms(Forms, [debug_info, return]), + {module, nas4005} = code:load_binary(nas4005, "nas4005", Bin), + true + catch + E:R -> + {E, R, erlang:get_stacktrace()} + end. + +here() -> + filename:dirname(code:which(?MODULE)). diff --git a/lib/diameter/test/diameter_transport_SUITE.erl b/lib/diameter/test/diameter_transport_SUITE.erl index 9d981d0a2b..284d2b9566 100644 --- a/lib/diameter/test/diameter_transport_SUITE.erl +++ b/lib/diameter/test/diameter_transport_SUITE.erl @@ -349,35 +349,40 @@ rand_bytes(N) -> %% start_connect/3 start_connect(Prot, PortNr, Ref) -> - {ok, TPid, [?ADDR]} = start_connect(Prot, - {connect, Ref}, - ?SVC([]), - [{raddr, ?ADDR}, - {rport, PortNr}, - {ip, ?ADDR}, - {port, 0}]), - ?RECV(?TMSG({TPid, connected, _})), + {ok, TPid} = start_connect(Prot, + {connect, Ref}, + ?SVC([]), + [{raddr, ?ADDR}, + {rport, PortNr}, + {ip, ?ADDR}, + {port, 0}]), + connected(Prot, TPid), TPid. +connected(sctp, TPid) -> + ?RECV(?TMSG({TPid, connected, _})); +connected(tcp, TPid) -> + ?RECV(?TMSG({TPid, connected, _, [?ADDR]})). + start_connect(sctp, T, Svc, Opts) -> - diameter_sctp:start(T, Svc, [{sctp_initmsg, ?SCTP_INIT} | Opts]); + {ok, TPid, [?ADDR]} + = diameter_sctp:start(T, Svc, [{sctp_initmsg, ?SCTP_INIT} | Opts]), + {ok, TPid}; start_connect(tcp, T, Svc, Opts) -> diameter_tcp:start(T, Svc, Opts). %% start_accept/2 start_accept(Prot, Ref) -> - {Mod, Opts} = tmod(Prot), - {ok, TPid, [?ADDR]} = Mod:start({accept, Ref}, - ?SVC([?ADDR]), - [{port, 0} | Opts]), + {ok, TPid, [?ADDR]} + = start_accept(Prot, {accept, Ref}, ?SVC([?ADDR]), [{port, 0}]), ?RECV(?TMSG({TPid, connected})), TPid. -tmod(sctp) -> - {diameter_sctp, [{sctp_initmsg, ?SCTP_INIT}]}; -tmod(tcp) -> - {diameter_tcp, []}. +start_accept(sctp, T, Svc, Opts) -> + diameter_sctp:start(T, Svc, [{sctp_initmsg, ?SCTP_INIT} | Opts]); +start_accept(tcp, T, Svc, Opts) -> + diameter_tcp:start(T, Svc, Opts). %% =========================================================================== diff --git a/lib/diameter/test/diameter_util.erl b/lib/diameter/test/diameter_util.erl index 03f79096ac..d249b0e4fa 100644 --- a/lib/diameter/test/diameter_util.erl +++ b/lib/diameter/test/diameter_util.erl @@ -32,7 +32,8 @@ foldl/3, scramble/1, unique_string/0, - have_sctp/0]). + have_sctp/0, + eprof/1]). %% diameter-specific -export([lport/2, @@ -48,6 +49,16 @@ -define(L, atom_to_list). +%% --------------------------------------------------------------------------- + +eprof(start) -> + eprof:start(), + eprof:start_profiling([self()]); + +eprof(stop) -> + eprof:stop_profiling(), + eprof:analyze(), + eprof:stop(). %% --------------------------------------------------------------------------- %% name/2 diff --git a/lib/diameter/vsn.mk b/lib/diameter/vsn.mk index 4801f542fb..e6dfddb5b2 100644 --- a/lib/diameter/vsn.mk +++ b/lib/diameter/vsn.mk @@ -17,5 +17,5 @@ # %CopyrightEnd% APPLICATION = diameter -DIAMETER_VSN = 2.0 +DIAMETER_VSN = 2.1 APP_VSN = $(APPLICATION)-$(DIAMETER_VSN)$(PRE_VSN) |