Skip to content

Prepaid Charging

The prepaid path implements a DIAMETER Credit-Control Application server. It speaks RFC 6733 base protocol on the wire and RFC 4006 semantics for credit control, listens on a configured TCP port, and accepts long-lived sessions from network elements (GGSN, PGW, MSC, or any DIAMETER peer). Each session runs in a dedicated thread.

Standards Coverage

Constants in include/diameter/diameter_constants.hpp enumerate the subset of the standard implemented:

  • Base protocol — RFC 6733. Header version, R/P/E/T flags, AVP flags (V, M, P), AVP padding to 4-byte boundaries, command code 272 (Credit-Control), result codes for protocol errors (3xxx), transient failures (4xxx), and permanent failures (5xxx).
  • Credit-Control Application — RFC 4006. CC-Request-Type values 1–4 (INITIAL_REQUEST, UPDATE_REQUEST, TERMINATION_REQUEST, EVENT_REQUEST); CC AVPs Subscription-Id, Subscription-Id-Type, Subscription-Id-Data, Service-Identifier, Rating-Group, Requested-Service-Unit, Granted-Service-Unit, Used-Service-Unit, Validity-Time, CC-Money, CC-Time, CC-Total-Octets, CC-Input-Octets, CC-Output-Octets; CC-specific result codes END_USER_SERVICE_DENIED (4010), CREDIT_CONTROL_NOT_APPLICABLE (4011), CREDIT_LIMIT_REACHED (4012), USER_UNKNOWN (5030), RATING_FAILED (5031).

Stack Layout

The DIAMETER protocol is implemented as a standalone shared library (libdiameter.dylib) so it can be reused outside the OCS — a mock client built from the same library is shipped in bin/diameter_client for load testing.

graph TB
    subgraph App["Application layer (libmicrobss)"]
        PrepSrv["PrepaidServer<br/>(ACE_Svc_Handler)"]
        BR["BalanceReserve<br/>::fromCCR()"]
        CH["Charge<br/>::fromCCR()"]
        Sub["Subscriber"]
        Rater["Rater"]
    end

    subgraph DiamLib["libdiameter"]
        Stack["DiameterStack"]
        Adapter["DiameterAdapter"]
        Msg["DiameterMessage / CCR / CCA"]
        AVP["DiameterAVP / AVPList"]
        Const["diameter_constants.hpp"]
    end

    subgraph Net["Network"]
        Peer["DIAMETER peer<br/>(GGSN/PGW/MSC)"]
    end

    Peer -- "TCP, RFC 6733 framing" --> PrepSrv
    PrepSrv --> Stack
    Stack --> Msg
    Msg --> AVP
    AVP --> Const
    PrepSrv --> Adapter
    Adapter --> BR
    Adapter --> CH
    BR --> Sub
    CH --> Sub
    BR --> Rater
    CH --> Rater
    Sub --> DB[(MySQL)]

DiameterStack is responsible for encoding, decoding, and session bookkeeping; DiameterAdapter provides static helpers to extract MSISDN, B-number, service id, requested units, and used units from a DiameterCCR. The application-side BalanceReserve::fromCCR and Charge::fromCCR factories use these helpers to populate the internal protobuf objects.

Session Lifecycle

sequenceDiagram
    autonumber
    participant Peer as DIAMETER Peer
    participant PS as PrepaidServer
    participant Stack as DiameterStack
    participant Adapter as DiameterAdapter
    participant Sub as Subscriber
    participant R as Rater
    participant DB as MySQL

    Peer->>PS: TCP connect
    PS->>Stack: new DiameterStack(host, realm)

    Peer->>PS: CCR (Type=1 INITIAL, Requested-Units=N)
    PS->>Stack: decodeMessage(buffer)
    Stack-->>PS: DiameterCCR
    PS->>Adapter: extractMSISDN, extractServiceId, extractRequestedUnits
    PS->>Sub: Subscriber(MSISDN); load balance, plan, prepaid flag, status
    alt sub.id == 0
        PS-->>Peer: CCA Result-Code=5030 USER_UNKNOWN
    else sub.status SUSPENDED or TERMINATED
        PS-->>Peer: CCA Result-Code=4010 END_USER_SERVICE_DENIED
    else sub.prepaid == 0
        PS-->>Peer: CCA Result-Code=4010 END_USER_SERVICE_DENIED
    else
        PS->>R: rate(Balance_reserve)
        alt rate < 0
            PS-->>Peer: CCA Result-Code=5031 RATING_FAILED
        else has_balance(rate)
            PS->>Sub: reserve_balance()
            Sub->>DB: UPDATE balance.reserved
            PS->>DB: INSERT balance_reserve
            PS-->>Peer: CCA Result-Code=2001<br/>Granted-Units=N, Validity-Time=30
        else
            PS-->>Peer: CCA Result-Code=4012 CREDIT_LIMIT_REACHED
        end
    end

    loop UPDATE_REQUEST (mid-session)
        Peer->>PS: CCR (Type=2 UPDATE)
        PS->>R: rate(Balance_reserve)
        PS->>Sub: reserve_balance()
        PS-->>Peer: CCA Result-Code=2001
    end

    Peer->>PS: CCR (Type=3 TERMINATION, Used-Units=M)
    PS->>R: rate(Charge)
    PS->>Sub: debit_balance(final_price)
    PS->>DB: INSERT charge
    PS-->>Peer: CCA Result-Code=2001

    Peer->>PS: TCP close
    PS->>PS: handle_close — thread exits

The subscriber lifecycle gate is the first business check after the MSISDN lookup. Subscribers whose status is SUBSCRIBER_STATUS_SUSPENDED or SUBSCRIBER_STATUS_TERMINATED are rejected with DIAMETER_END_USER_SERVICE_DENIED (4010) — the same result code used for non-prepaid accounts, so the network simply tears down the bearer regardless of cause. Legacy commercial sub-states such as Pending and Roamer are explicitly serviceable. The gate runs uniformly across all four CC-Request-Types, including EVENT_REQUEST and TERMINATION_REQUEST — a session that began against an active subscriber who was then suspended mid-call cannot debit further units.

The single-event flow (EVENT_REQUEST, type 4) is identical to TERMINATION_REQUEST from the server's perspective: rate, debit, insert, answer. SMS and MMS use this path; voice and data use the four-step session.

Acceptor and Service Handler

// include/acceptor.hpp
class PrepaidServer : public ACE_Svc_Handler<_SOCK_Stream, ACE_MT_SYNCH> {
  int svc(void) override;
  int open(void *) override;
  int close(u_long flags = 0) override;
  int handle_input(ACE_HANDLE) override;
  int handle_close(ACE_HANDLE, ACE_Reactor_Mask) override;
  int send_answer(const diameter::DiameterCCA &cca);
private:
  _SOCK_Stream *c_client{nullptr};
  Rater *rater{nullptr};
  std::unique_ptr<diameter::DiameterStack> diameter_stack;
};
typedef ACE_Acceptor<PrepaidServer, _SOCK_Acceptor> Acceptor;

Acceptor is registered with the reactor in main.cpp only when modules.prepaid = 1. On accept, PrepaidServer::open constructs the per-session DiameterStack, creates a fresh Rater, and calls activate(THR_DETACHED, 1, 0). The detached thread then drives svc(), which is a while (ok) loop reading the 20-byte DIAMETER header and the rest of the message via c_client->recv_n().

Defensive checks

  • The reported message length is decoded from bytes 1–3 of the header and validated against DIAMETER_HEADER_SIZE (rejected if smaller).
  • Messages larger than 1 MB (MAX_DIAMETER_MSG) are rejected before allocation, blocking length-field DoS.
  • Truncated message bodies (header says N, only K read) close the session.
  • Unknown command codes return DIAMETER_COMMAND_UNSUPPORTED (3001).
  • Missing AVPs needed for BalanceReserve::fromCCR or Charge::fromCCR return DIAMETER_MISSING_AVP (5005).

Granted Units and Validity

When a request is approved, the CCA carries Granted-Service-Unit equal to the network's requested units and Validity-Time = 30 seconds. The peer is expected to issue an UPDATE_REQUEST before the validity expires. There is no per-session maximum reservation: every reservation is rated against the live balance and either approved or refused on the spot.

Result-Code Mapping

Internal condition DIAMETER result code Constant
Successful reserve / charge 2001 DIAMETER_SUCCESS
MSISDN not in subscribers 5030 DIAMETER_USER_UNKNOWN
Subscriber suspended or terminated 4010 DIAMETER_END_USER_SERVICE_DENIED
Subscriber exists but is not prepaid 4010 DIAMETER_END_USER_SERVICE_DENIED
Rater could not price the event 5031 DIAMETER_RATING_FAILED
Balance below requested reservation 4012 DIAMETER_CREDIT_LIMIT_REACHED
Required AVP missing in CCR 5005 DIAMETER_MISSING_AVP
Command other than CCR or non-request 3001 DIAMETER_COMMAND_UNSUPPORTED

Mock DIAMETER Client

bin/diameter_client is a load-test client built from the same libdiameter library. Configuration in config/diameter_client.toml selects:

  • [connection] — server host/port and the realms to send.
  • [execution]single, sequential, or parallel mode and a target request count.
  • [msisdns] — pools of A-numbers (calling) and B-numbers (called) to randomise.
  • [call] / [sms] / [data] — per-service unit ranges and service IDs.
  • [simulation] — percentage split across event types and the number of mid-session updates per voice call.

The client picks an event type per request according to the configured percentages, draws a random duration / payload size, opens a TCP connection, and walks through the appropriate CCR sequence. It is invoked from the deployment via scripts/client.sh.