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 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/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 creation — no 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-ue–55–(eNB-UE-S1AP-ID, MME-UE-S1AP-ID) pair
s1ap-global–33–S1 SCTP association (eNB ↔ MME, non-UE)
rrc-connection3–––C-RNTI lifetime on the cell
pdcp-bearer7–––SRB/DRB ciphering context
rlc-bearer7–––LCID per UE
mac-ue5–––C-RNTI on PDSCH / PUSCH
mac-paging2–––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-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.