Teardown, and why order matters

A subscriber leaves the CONNECTED state for several reasons, all funnelled through one teardown path that stamps a standard terminate cause so your billing system knows why the session ended:

Cause Meaning Trigger
1 User-Request Subscriber sent DHCP Release
4 Idle-Timeout No traffic for the idle period
5 Session-Timeout Plan time elapsed → goes to CAPTIVE, not down
6 Admin-Reset Operator disconnect, or RADIUS CoA disconnect
15 Service-Unavailable Recovered after a restart but too old to keep

The teardown does one thing in a very specific order: it reads the subscriber's final traffic counters from the data plane first, sends the closing accounting record second, and only then removes the session. If it removed the session first, the final bill would be stale or lost. This ordering is a genuine correctness rule, not an incidental detail.


The Life of a Subscriber, End to End

It is worth walking the whole arc once, because it ties the two planes together and mirrors what you will actually see in the session list and metrics.

  1. A new subscriber powers on. Their router sends a DHCP Discover; the edge router relays it to the BNG with Option 82. The data plane has no session for them, so it punts the packet to the controller. (A brief AUTHENTICATING state.)
  2. The controller admits them. AAA accepts, a plan is resolved to a pair of rates, an address is drawn from the pool, the data plane is programmed, and an Offer goes out. (OFFERING.)
  3. The subscriber confirms. Their DHCP Request comes back, the lease is created, accounting starts, timers are armed, and the Ack is sent. (CONNECTED.)
  4. They use the internet. Every packet now flows entirely through the data plane at line rate — looked up by IP, checked for spoofing, metered green/yellow/red, forwarded. The controller sees nothing. Meanwhile a background loop periodically reads their counters, sends interim accounting, and watches for idleness.
  5. Their prepaid time runs out. The session-timeout fires. Rather than disconnect them, the BNG closes accounting (cause 5), flips the captive flag, and moves them to CAPTIVEkeeping their IP. Their next web request lands on the "top up to continue" page. Nothing is disconnected; nothing re-DHCPs.
  6. They pay. The portal calls the BNG's activate endpoint. The captive flag clears, accounting restarts, timers re-arm, and they are CONNECTED again — same IP, no interruption.
  7. They go home / power off. Their router sends a DHCP Release (or they simply go idle, or an operator disconnects them). The BNG reads their final counters, sends the closing accounting record with the right cause, and removes the session. The address returns to the pool.

Every step above is visible to you: the state distribution in the metrics, the per-subscriber counters, the DHCP protocol counters, and the accounting records all narrate this arc.


The Captive Portal and the Prepaid Model

The captive portal is central to the target business — prepaid fixed-wireless — so it is worth understanding precisely.

How the walled garden works. When a subscriber is in CAPTIVE state, the data plane permits exactly three things and silently drops everything else:

  • DNS (UDP/53) to the portal,
  • HTTP (TCP/80) to the portal,
  • HTTPS (TCP/443) to the portal.

How the redirect works — DNS steering. A captive subscriber's DHCP lease hands out the portal's DNS server as their only resolver, and that server answers every lookup with the portal's address. Combined with the walled garden, any website the subscriber types resolves to — and can only reach — the portal. There is no in-path HTTP interception to configure; the redirect is a consequence of DNS plus the filter.

Why expiry goes to CAPTIVE, not disconnect. This is the prepaid-first rule. When a subscriber's plan time elapses, keeping their IP and pushing them behind the portal means their next browser action is a "top up to continue" page — the exact conversion path a prepaid operator depends on. A forced disconnect and re-DHCP would break that experience. So Session-Timeout is a demotion to captive, and only a Release, idle-out, or explicit disconnect actually removes the session.

Entering and leaving captive:

Into CAPTIVE Out of CAPTIVE
AAA rejected an unknown subscriber at onboarding Portal calls POST /bng/sessions/{ip}/activate after payment
A connected subscriber's plan time expired (which clears the flag, restarts accounting, re-arms timers → CONNECTED)

Accounting: Measuring What Subscribers Use

Accounting is how usage becomes billable records. The BNG emits three kinds:

  • Start — sent when a subscriber connects (counters at zero).
  • Interim — sent periodically during the session, carrying cumulative usage.
  • Stop — sent at teardown, carrying final usage and the terminate cause.

Two details shape what the numbers mean:

  • Only green + yellow are billed. Red traffic was dropped at the policer — the subscriber never received it — so it is excluded from the billed octet counts. Red is still reported through the API for diagnostics; it just is not on the bill.
  • Large counters are split for RADIUS. VPP counts in 64-bit, but RADIUS octet fields are 32-bit, so each count is split into the octet field plus a "gigawords" field (units of 4 GB). Your billing system reassembles them; byte counts stay exact well into the petabyte range.

Each session carries a stable accounting ID across its Start/Interim/Stop, and echoes back verbatim any opaque Class value the AAA server attached — the BNG never inspects it.

Alpha caveat. The counter substrate — the per-colour, per-direction byte and packet counts, refreshed from the data plane — is real and running today. The assembly of those counts into RADIUS accounting packets on the wire is part of the deferred production RADIUS client.


Timers and Scale: What Bounds a Node

Three time-based jobs run for every connected subscriber: interim accounting, session expiry, and idle detection. Done naïvely — a separate timer per job per subscriber — that would be hundreds of thousands of timers on a full node, all firing and all forcing the controller to save state, producing a stampede that has been measured to badly hurt latency even at a thousand sessions.

The BNG instead folds all of this into a single batched loop (informally, the tickler). On each pass it:

  • reads the counters for all due subscribers in one bulk request,
  • writes the updated state once, and
  • emits the interim accounting records that are due, and tears down any subscriber that tripped idle or session-timeout in that same pass.

Idle detection needs no timer at all: it is derived from the counters. If a subscriber's total traffic has not moved since the last poll, their idle streak advances; when it reaches the idle limit, they are torn down (cause 4). Session expiry is likewise a simple comparison of "now" against a stored deadline.

For you, the operational takeaways are:

  • A node carries on the order of 100,000 concurrent subscribers on commodity R360-class hardware. The limit is memory/cache behaviour, not raw CPU. (A future per-worker-table option would raise this to roughly 400,000–500,000, but that is out of scope for this release.)
  • Growth is horizontal. One controller can drive multiple data-plane nodes; you add capacity by adding a node, under the same operator-facing configuration, not by replacing the box.
  • The controller measures its own save timing (see Observability). Those numbers are your early warning that a node is approaching its ceiling.

Persistence and Recovery: Restarts Don't Drop Subscribers

Because the controller and the data plane are separate processes, a restart of one must not disconnect subscribers being carried by the other. An integrated single-box BNG can afford to lose all sessions on restart; a disaggregated one cannot, because a controller restart that dropped every session would knock 100,000 paying customers offline while the data plane was still happily forwarding their traffic.

The BNG therefore treats session state as durable. It is written to a local database on every state change. The clever part is that the things you cannot save — live timers, sockets — are reconstructed on start-up from absolute expiry timestamps stored with each session. On restart, every timer is simply rebuilt from its deadline; a deadline already in the past just fires on the next pass.

Recovery then takes one of three forms depending on what actually restarted:

Situation What the BNG does
Controller restarted, data plane stayed up Rebuild the controller's in-memory state and re-arm timers from the saved deadlines. No data-plane changes — VPP still holds the live sessions.
Controller and data plane both fresh Rebuild controller state, then re-program every connected session into the data plane so the two match.
Data plane diverged (its state no longer trusted) Tear down the untrusted data-plane state and re-program it from the controller's session records.

Two more operational guarantees:

  • Upgrades preserve live sessions. A package upgrade keeps all state, so applying an update does not disconnect subscribers. (A fresh install starts clean.) This is a deliberate BNG-specific behaviour.
  • Stale sessions are closed cleanly. After a long outage, any session older than a recovery-age limit (default 24 hours) is discarded with a proper closing accounting record (cause 15), so the billing ledger always sees a clean boundary.

Configuration: What You Set, and Where

Configuration lives in two places, split by ownership, not convenience. A setting lives in exactly one place; there is no merge logic to reason about.

File-owned (bng.json, read once at start-up). The security-sensitive and rarely-changed settings: RADIUS server lists and shared secrets (injected from environment variables, held only in memory, and never written to the database or exposed through the API, so a state dump cannot leak them), the captive-portal address, upstream DNS servers, and non-AAA defaults. Changing file-owned config means a controller restart — there is no live reload.

API-managed (in the database, changed only through REST). The things you tune day to day:

What Endpoint (orientation only) Notes
Address pools PUT/DELETE /bng/config/pools/{name} Range and lease time. Must exist before subscribers can get addresses.
Service plans PUT/DELETE /bng/config/plans/{name} Editing a plan live re-rates every connected subscriber on it.
Edge routers PUT/DELETE /bng/config/edgeRouters/{giaddr} Optional map of edge-router → gateway IP; falls back to the relay address if absent.

The guiding principle across all integration is poll over push: the BNG exposes a stable API surface and your other systems (OSS/BSS, CRM, IPAM, NMS, the portal) query it on their own schedule. The BNG does not push data outward (its only outbound initiation is RADIUS), which keeps it from being coupled to any one vendor's schema. Every API call is authenticated with an API key.


Operating the BNG: What to Watch

This is the section to keep by the NOC dashboard. Everything here is available on the management NIC via the REST API and Prometheus.