This trace captures the same UE attach from four vantage points: eNB MAC (enb_mac.pcap), eNB S1AP (enb_s1ap.pcap), EPC core (epc.pcap), UE NAS (ue_nas.pcap). Render all four and walk me through how a single NAS Attach Request fans out across the layers — who sees it first, where it gets tunneled, which round-trips happen on which interface, and what each pcap is missing that the others fill in.
Rendered all four with visualether explore. Same EPS attach (UE M-TMSI 0x462d9650, IMSI 001010123456780) appears in every capture, but each vantage strips or adds layers. Walking it in the order packets actually travel:
1. UE NAS — the “intent” layer
- Frame 1: NAS Attach Request is created here (T = 09:26:17.122 UE clock), with all IEs in clear — KSI=0, GUTI (MME-Group 1 / MME-Code 26 / M-TMSI
0x462d9650), PDN type IPv4, ESM “Initial request”. - The whole NAS dialogue (Attach Req → Identity Req/Rsp → Auth Req/Rsp → SMC / SMC-Complete → Attach Accept → Attach Complete, 9 frames) is present end-to-end with zero transport context: no S1AP IDs, no eNB-UE-S1AP-ID, no RNTI, no radio timing.
- Missing: how it got out of the UE (RRC / PDCP / RLC / MAC), and what the network did between Attach Accept and Attach Complete (the InitialContextSetup / E-RAB setup on S1).
2. eNB MAC — the radio layer
This is where the NAS PDU first hits the wire. 271 frames; the NAS payload is buried inside ~30–40 frames of MAC / RLC / PDCP overhead:
- Frame 1: RAR (Msg2) on RA-RNTI 2, grants Temp C-RNTI 70, TA = 0.
- Frame 2: RRC Connection Request (Msg3) on CCCH — carries S-TMSI (MMEC
1a, M-TMSI462d9650), Causemo-Data. First time the network sees this UE. - Frame 3: RRC Connection Setup (Msg4) with SRB1 config.
- Frame 4 (T = .353895): RRC Connection Setup Complete — NAS Attach Request is piggybacked here as octet blob
178ed88e52040741…. Still unciphered (AS security not yet active, PDCP MAC-I =0x00000000). - Frame 14: AS Security Mode Command (EEA2 / EIA2). From frame 17 on, every PDCP PDU is tagged
Sec EEA2/EIA2with a real MAC-I — the AS layer now hides NAS payloads from anyone capturing the air interface.
What only this pcap shows: PRACH/RAR ladder, contention resolution, Temp→C-RNTI 70 promotion, RLC AM ACK_SN bookkeeping, BSR / PHR / UL-grant sizing, the exact moment AS ciphering turns on, DRB2 coming up (frame 23 onward) — i.e. the bearer that the Attach actually built.
3. eNB S1AP — the eNB ↔ MME tunnel
- Frames 1–2: S1Setup Request / Response — eNB introduces itself to
srsmme015 s before any UE shows up. Neither UE NAS nor MAC have this. - Frame 3 (T = .354847, ~1 ms after RRC Setup Complete on the air): NAS Attach Request reappears, now wrapped as S1AP InitialUEMessage with eNB-UE-S1AP-ID = 1. Same NAS PDU you saw clear in
ue_nas, but air-interface IDs (C-RNTI, RLC SN, RAPID) are gone — replaced by S1AP UE-association IDs. - Frames 4–9: each NAS round trip is an S1AP DL NAS Transport / UL NAS Transport pair. MME-UE-S1AP-ID = 1 appears in the first DL message (frame 4) and is used by both sides from there on — that's the S1-side handshake equivalent of “C-RNTI established”.
- Frame 10: InitialContextSetup Request (carrying NAS Attach Accept piggybacked) — the UE NAS pcap can't see this as a distinct event; from the UE's view it's just “Attach Accept arrived”.
- Frame 11: UE Capability Info Indication. Only visible here.
- Frame 12: InitialContextSetup Response. The 200 ms gap from frame 11 is the eNB doing AS security activation + DRB reconfiguration round-trips you can watch in detail in
enb_macframes 14–24.
4. EPC core — identical S1AP, different vantage
Same 33-frame S1AP dialogue as enb_s1ap.pcap, captured on the MME side of the SCTP association rather than the eNB side. Timestamps differ by ~50 µs (S1Setup Req .869930 vs .869871) — that delta is the eNB↔MME wire latency. Useful as confirmation; not adding new protocol content.
What it would normally add but doesn't here: S6a (MME↔HSS — Authentication-Information Request/Answer between Identity Response and Auth Request, ~20 ms in frame 5→6), S11/GTPv2-C (MME↔SGW Create Session that must complete before Attach Accept can be sent in frame 8), and S5/GTP-U user plane to the PGW. Those Diameter / GTPv2 hops are clearly happening — unexplained ~50 ms gap between Auth Response and SMC, another ~50 ms between SMC Complete and Attach Accept being prepared — but no protocol packet for them is in this pcap. This capture point is only the S1-MME SCTP leg, not the full EPC.
Same Attach Request, four projections
| Vantage | Sees Attach Req as | Headers/IDs unique to it | Blind to |
|---|---|---|---|
ue_nas | clear NAS PDU, frame 1, full IEs | none — just NAS | radio, S1, EPC; can't see Attach Accept came inside InitialContextSetup |
enb_mac | opaque NAS blob piggybacked in RRC Conn Setup Complete (frame 4) | RNTI, RAPID, RLC SN, PDCP COUNT, MAC-I, BSR/PHR, SFN/SF, AS security | what MME does with it; later NAS becomes ciphered and invisible |
enb_s1ap | S1AP InitialUEMessage (frame 3) | eNB-UE / MME-UE-S1AP-ID, InitialContextSetup, UE Cap Info, Paging | radio details; S6a / S11 / S5 |
epc | identical S1AP set, ~50 µs later | timestamps confirm wire latency | S6a / S11 / S5 in this particular capture |
Round-trip ownership
- PRACH → RRC Conn Setup → SetupComplete (Msg1–Msg4) — only
enb_mac. - NAS Identity / Auth / SMC — visible in
ue_nas(semantic content) andenb_s1ap/epc(as S1AP DL/UL NAS Transport pairs).enb_macshows them as ciphered PDCP PDUs only after SMC. - AS Security Mode (RRC, EEA2/EIA2) — only
enb_mac(frames 14–15). NAS sees the NAS SMC, not the AS one — different layer, same name. - InitialContextSetup Req/Rsp + UE Cap Info — only the two S1AP captures.
- HSS authentication vector fetch + SGW/PGW session creation — no capture in this set shows it, but the timing gaps in
epc.pcapreveal where it happened.
Please look at the session level flows as well.
Ran extract_sessions on all four with the supplied sessions.fxt.xml. One index.html per pcap under /sessions/<stem>/.
Session counts per pcap
| Session type | enb_mac | enb_s1ap | epc | ue_nas | What the session keys on |
|---|---|---|---|---|---|
| nas-eps | 3 | 9 | 9 | 7 | NAS dialogue per UE (M-TMSI / IMSI) |
| s1ap-ue | – | 5 | 5 | – | (eNB-UE-S1AP-ID, MME-UE-S1AP-ID) pair |
| s1ap-global | – | 3 | 3 | – | S1 SCTP association (eNB ↔ MME, non-UE) |
| rrc-connection | 3 | – | – | – | C-RNTI lifetime on the cell |
| pdcp-bearer | 7 | – | – | – | SRB/DRB ciphering context |
| rlc-bearer | 7 | – | – | – | LCID per UE |
| mac-ue | 5 | – | – | – | C-RNTI on PDSCH / PUSCH |
| mac-paging | 2 | – | – | – | P-RNTI broadcast windows |
All sessions close as timeout::exceeded idle timeout or incomplete::mid-capture — srsRAN tears the UE down by inactivity, not by an explicit release we keyed on. Tracking artifact, not a real bug.
What the session view changes about the story
One Attach maps to vastly different session footprints depending on vantage:
- ue_nas: 1 attach ≈ 1
nas-epssession. Clean — the UE only knows itself. - enb_s1ap / epc: 1 attach fans out into 1
s1ap-ue+ 1nas-eps(plus the always-ons1ap-global). The MME/eNB sees the same NAS session wrapped in an explicit UE-association context that has its own lifetime — UEContextRelease ends the S1AP session ~53 s after attach while the NAS session lingers until the next idle timeout. - enb_mac: 1 attach explodes into 1
rrc-connection+ 1mac-ue+ 2rlc-bearer(SRB1 + DRB) + 2pdcp-bearer(SRB1 + DRB) — five concurrent sessions per UE just to carry the NAS dialogue, each with its own SN/ciphering state.
Mismatched session counts are the diagnostic
The enb_s1ap and epc captures show 5 s1ap-ue sessions for ~3 attaches because each idle→active transition (paging, mo-Data) reopens a fresh (eNB-UE, MME-UE) S1AP-ID pair while NAS state continues across them. The enb_mac shows 5 mac-ue sessions because it allocates a new C-RNTI on every RA. Same UE, different MAC identity. That's why the radio session count > NAS session count > S1 global count.
incomplete::mid-capture flags context that was already live when the capture began — the S1AP global association and a residual MAC/RLC/PDCP context. This is how you spot that the trace doesn't start at boot.
What can we learn about the uplink scheduling algorithm from the PCAPs we have analyzed?
The enb_mac pcap is the only one with scheduler-visible data; the other three are above the MAC layer. Here is what 271 frames reveal about the srsRAN/srsENB UL scheduler in this single-UE, unloaded trace.
Grant-size palette
| Bytes | Count | What it is |
|---|---|---|
| 7 | 3 | Msg3 (RRC Conn Request) — fixed RAR grant. Once per attach attempt. |
| 13 | 1 | Short-BSR sized grant (one-off) |
| 15 | 88 | Idle “keep-alive” grant — just enough for a Long BSR (4 bytes) + PHR + padding |
| 69, 157, 349 | 1, 3, 1 | Msg4 follow-ups: SetupComplete (157), reestablishment (69), paging response (349) |
| 517 | 66 | Baseline active grant — ~25 PRBs at a conservative MCS; the default once SR/BSR fires |
| 1143 | 20 | Burst grant — only when Short/Long BSR reports a non-zero LCG bucket |
Only two values dominate: 15 B (idle) and 517 B (active). The 1143 B grant fires precisely on the moments when there is real data to drain.
The scheduling state machine (cleanly visible at frames 217–229)
SFN 438.5 Long BSR all-zero UL grant 15 B ← idle polling SFN 440.8 Short BSR LCG=2, BS=967–1132 B UL grant 517 B ← grant in flight; UE reports buffer SFN 441.7 Long BSR (data drained) UL grant 1143 B ← scheduler reacts: jumps TBS to cover the bucket SFN 442.5 Long BSR all-zero UL grant 1143 B ← one wasted overshoot (padding) SFN 443.3 Long BSR all-zero UL grant 15 B ← immediate decay back to idle ... ~1 second of silence ... SFN 534.7 DL data arrives → next round of the same dance
What this tells us about the algorithm
- Polling-based, not SR-driven. The eNB issues a 15 B grant every ~8 ms (~ every 8 subframes) even when BSR = 0. Fast enough that the UE almost never needs to use PUCCH SR — it just piggybacks its Short BSR on the next polled grant. Almost every “UL grant 517 B” frame is responding to the previous polled BSR, not to an SR.
- One subframe of grant-size lag. From the BSR arriving (SFN 440.8) to the matched 1143 B grant (SFN 441.7) is ~9 ms — one polling cycle. The scheduler reacts at the polling rate, not faster; there is no fast path that grows the grant within the same TTI as the BSR.
- Overshoot by one grant, then snap to idle. When the LCG empties, the eNB grants 1143 B one more time before recognizing the buffer is empty, then drops straight back to 15 B in the next polling cycle. No tapered decay — it's binary (burst vs. polling).
- Two-tier TBS ladder. 517 B and 1143 B are the only “real” sizes. 517 is given when something is expected but BS unknown; 1143 is the max TBS the link adaptation has chosen (likely 25 PRBs × MCS ~16 on a 10 MHz cell). There is no continuous 100 / 200 / 300 B granularity — the scheduler picks one of two prepared TBS classes.
- Periodic-BSR config is irrelevant in this trace. RRC Setup advertised
periodicBSR=sf20, retxBSR=sf320, but every BSR you see is event-triggered (data arrival, padding-BSR opportunity, or eNB-polled). The 20 ms periodic timer never wins because polled grants arrive faster. - Closed-loop power control is silent. Every TPC bit we have is 0 dB. PHR LCG0–3 are all index 0 — the UE is never near its power limit. The whole capture is in a benign RF regime.
- MCS = 0 only in RAR. Msg3's UL grant uses MCS 0 (most robust). Derived TBS sizes suggest MCS ~6–8 over 25 PRBs for 517 B and MCS ~16 over 25 PRBs for 1143 B — i.e. link adaptation moves up two steps once the UE is authenticated and on a DRB.
- Single-UE blind spot.
UL UEs in TTI = 1on every frame means we see the scheduler with no contention. Anything about fairness, priority weighting, or proportional-fair allocation is not testable from this pcap. - One RA per ECM-IDLE→CONNECTED transition. 3 PRACH/RAR (RNTI 70, 71, 72), each followed by the same 7→157→517… ladder. The scheduler appears stateless across RRC connections — no warm-start using prior MCS or grant size.
TL;DR: event-polled scheduler with two TBS classes, ~8 ms polling cadence, one-cycle reaction lag, and a one-grant overshoot when buffers drain. Consistent with srsENB's default time-domain RR scheduler in single-UE mode. The other three pcaps tell you nothing about scheduling because they're all above PDCP.
Please write high-level pseudo code for the UL scheduler.
Based strictly on the behaviours observable in enb_mac.pcap — a polling cadence, a two-tier TBS ladder, one-cycle reaction lag, and one-grant overshoot:
# srsENB-style UL scheduler — inferred from a single-UE trace
# Observed only: polling cadence, 2 TBS classes, 1-TTI reaction lag,
# 1-grant overshoot on drain. Multi-UE fairness, link adaptation,
# and TPC loops are NOT observable in this pcap.
TBS_POLL = 15 # bytes — enough for Long BSR + PHR + padding
TBS_ACTIVE = 517 # bytes — ~25 PRBs, mid MCS (baseline once BSR/SR fires)
TBS_BURST = 1143 # bytes — ~25 PRBs, max MCS (drain a non-empty LCG)
POLL_PERIOD_TTI = 8 # ~8 ms between polled grants when UE has no data
IDLE_BACKOFF_TTI = 1000 # ~1 s of silence before re-polling, after a drain cycle
# Per-UE scheduler state
class UEState:
rnti
last_grant_tti
last_reported_bs_per_lcg # from most recent BSR
pending_burst # True after a non-zero BSR, until first drain
in_ra # True between PRACH and first RRC SetupComplete
# --------------- Random Access path (special-case, pre-RRC) ---------------
on PRACH_preamble(rapid):
ue = allocate_temp_crnti()
ue.in_ra = True
schedule_RAR(
ra_rnti = derive_ra_rnti(prach_sf),
temp_crnti = ue.crnti,
ta = measured_ta,
ul_grant = { rba: 6_PRBs, mcs: 0, tbs: 7 } # Msg3: fixed, most robust
)
on Msg3_received(ue): # RRC Connection Request on CCCH
grant_ul(ue, tbs = 157) # Msg4 follow-up size for SetupComplete
ue.in_ra = False
# --------------- Steady-state UL scheduler (per TTI, per UE) --------------
def schedule_ul_tti(tti):
for ue in connected_ues:
# 1. Decide whether this UE deserves a grant this TTI.
if not due_for_grant(ue, tti):
continue
# 2. Pick a TBS class based on last-known buffer state.
tbs = choose_tbs(ue)
# 3. Issue the grant. (PRB allocation + MCS not visible in trace;
# we only see the resulting TBS.)
issue_pusch_grant(ue.rnti, tbs, tti + K2)
ue.last_grant_tti = tti
def due_for_grant(ue, tti):
elapsed = tti - ue.last_grant_tti
if ue.pending_burst:
return elapsed >= 1 # back-to-back while draining
if any_lcg_nonzero(ue.last_reported_bs_per_lcg):
return elapsed >= 1 # active: every TTI eligible
return elapsed >= POLL_PERIOD_TTI # idle: ~8 ms keep-alive poll
def choose_tbs(ue):
bs = ue.last_reported_bs_per_lcg
if all_zero(bs) and not ue.pending_burst:
return TBS_POLL # 15 B — just enough for a BSR
if ue.pending_burst:
# Overshoot rule: keep granting BURST until we *see* an all-zero BSR,
# then grant one more, then collapse.
if all_zero(bs):
ue.pending_burst = False # next TTI will fall back to POLL
return TBS_BURST # 1143 B
# Non-zero BS just reported, but burst path not yet armed:
# first reaction is the baseline ACTIVE grant; BURST kicks in
# one polling cycle later (the observed 1-TTI reaction lag).
ue.pending_burst = True
return TBS_ACTIVE # 517 B
# --------------- BSR ingestion (UE → eNB, on PUSCH) -----------------------
on BSR_received(ue, bsr):
ue.last_reported_bs_per_lcg = bsr.bs_per_lcg
# No immediate re-grant here — reaction happens at the next
# schedule_ul_tti() pass, which gives the observed ~1 TTI lag.
What is deliberately not in the pseudocode
Because the trace cannot justify it:
- PF / RR Proportional-fair or round-robin weighting across UEs (only 1 UE in the trace;
UL UEs in TTI = 1everywhere). - TPC Closed-loop power control (every observed TPC = 0 dB; PHR always index 0).
- MCS Link adaptation / MCS selection (only the resulting TBS in bytes is visible).
- HARQ Retransmission policy (no NACK→retx pairs surfaced).
- PRB Allocator (we see TBS in bytes, not the RB / MCS combination that produced it).
- timers Periodic-BSR / retx-BSR (configured
sf20/sf320in RRC, but polled-grant cadence preempts them).
Anything more detailed would be invented, not inferred.
Method. Four pcaps rendered with VisualEther via its MCP integration with Claude — explore for the master sequence diagram per vantage, then extract_sessions for per-session outcome buckets. Scheduler inference cross-checked by tabulating UL grant sizes, TPC, MCS and BSR fields directly from the rendered NDJSON of enb_mac.pcap (271 frames). Every behaviour claimed in the pseudocode is observable in those tables; everything else is explicitly left out.