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-Typevalues 1–4 (INITIAL_REQUEST,UPDATE_REQUEST,TERMINATION_REQUEST,EVENT_REQUEST); CC AVPsSubscription-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 codesEND_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::fromCCRorCharge::fromCCRreturnDIAMETER_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, orparallelmode 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.