01UserInitial request

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.

02ClaudeCross-vantage walkthrough

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-TMSI 462d9650), Cause mo-Data. First time the network sees this UE.
  • Frame 3: RRC Connection Setup (Msg4) with SRB1 config.
  • Frame 4 (T = .353895): RRC Connection Setup CompleteNAS 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/EIA2 with 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 srsmme01 5 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_mac frames 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

VantageSees Attach Req asHeaders/IDs unique to itBlind to
ue_nasclear NAS PDU, frame 1, full IEsnone — just NASradio, S1, EPC; can't see Attach Accept came inside InitialContextSetup
enb_macopaque NAS blob piggybacked in RRC Conn Setup Complete (frame 4)RNTI, RAPID, RLC SN, PDCP COUNT, MAC-I, BSR/PHR, SFN/SF, AS securitywhat MME does with it; later NAS becomes ciphered and invisible
enb_s1apS1AP InitialUEMessage (frame 3)eNB-UE / MME-UE-S1AP-ID, InitialContextSetup, UE Cap Info, Pagingradio details; S6a / S11 / S5
epcidentical S1AP set, ~50 µs latertimestamps confirm wire latencyS6a / 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) and enb_s1ap/epc (as S1AP DL/UL NAS Transport pairs). enb_mac shows 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 creationno capture in this set shows it, but the timing gaps in epc.pcap reveal where it happened.
03UserBroaden scope

Please look at the session level flows as well.

04ClaudePer-pcap session flows

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 typeenb_macenb_s1apepcue_nasWhat the session keys on
nas-eps3997NAS dialogue per UE (M-TMSI / IMSI)
s1ap-ue55(eNB-UE-S1AP-ID, MME-UE-S1AP-ID) pair
s1ap-global33S1 SCTP association (eNB ↔ MME, non-UE)
rrc-connection3C-RNTI lifetime on the cell
pdcp-bearer7SRB/DRB ciphering context
rlc-bearer7LCID per UE
mac-ue5C-RNTI on PDSCH / PUSCH
mac-paging2P-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-eps session. Clean — the UE only knows itself.
  • enb_s1ap / epc: 1 attach fans out into 1 s1ap-ue + 1 nas-eps (plus the always-on s1ap-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 + 1 mac-ue + 2 rlc-bearer (SRB1 + DRB) + 2 pdcp-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.

05UserScheduler reverse-engineering

What can we learn about the uplink scheduling algorithm from the PCAPs we have analyzed?

06ClaudeUL scheduler — inferred from BSR / PHR / grant patterns

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

BytesCountWhat it is
73Msg3 (RRC Conn Request) — fixed RAR grant. Once per attach attempt.
131Short-BSR sized grant (one-off)
1588Idle “keep-alive” grant — just enough for a Long BSR (4 bytes) + PHR + padding
69, 157, 3491, 3, 1Msg4 follow-ups: SetupComplete (157), reestablishment (69), paging response (349)
51766Baseline active grant — ~25 PRBs at a conservative MCS; the default once SR/BSR fires
114320Burst 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 = 1 on 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.

07UserSynthesise

Please write high-level pseudo code for the UL scheduler.

08ClaudePseudocode — strictly inferred from observed behaviours

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 = 1 everywhere).
  • 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/sf320 in 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.