Reconstructing a VoNR Voice Call from a Bare srsRAN F1-U Capture

The companion to Inside F1-U. That walkthrough read the F1-U transport — the GTP-U tunnels a 5G base station uses for CU/DU data-plane communication. In this article, we strip the PDCP header off each downlink packet and extract a two-party VoNR (Voice over New Radio) IMS call. The packets used in the analysis were decoded and annotated with VisualEther, which turns a Wireshark PCAP into a sequence diagram.

💡 Read the walkthrough and the diagram side by side. Open the interactive diagram (or PDF) in a split-screen window so the captions stay in view as you read. In Edge, Chrome, or Firefox, right-click the link and pick your browser's split-screen option (Edge labels it "Open link in split screen window"). On macOS, prefer Chrome or Firefox — Safari has no in-browser split screen. Otherwise, open the diagram in a new tab and snap the two windows side by side.

Overview

A srsRAN issue reports that a capture taken on the F1-U interface of a 5G base station decodes as nothing but GTP — the RTP voice and the SIP signaling are invisible.

This is due to the F1-U architecture. F1-U is the user-plane leg between a gNB's Central Unit (CU-UP) and its Distributed Unit (DU) — the sibling article, Inside F1-U, reads that transport in full. Every F1-U packet is a bare GTP-U tunnel carrying a PDCP PDU, and in this srsRAN lab the PDCP layer uses the NEA0 null cipher — the payload is in the clear. So the tunnel is just a prefix to delete:

flowchart TB accTitle: Reconstructing the inner VoNR IP capture from the outer F1-U trace accDescr: The outer capture du_f1u.pcap is a bare GTP-U F1-U trace. Each GTP-U packet carries an NR-U / NRUP flow-control header and a T-PDU holding a PDCP-NR PDU with a 3-byte header and NEA0 null ciphering and no SDAP header. Stripping that 3-byte PDCP header from every T-PDU exposes the inner IPv4 packet, producing du_f1u_inner.pcap with 2,960 inner IPv4 packets. That inner capture contains SIP IMS registration, RTP and RTCP voice media, IPsec ESP carrying the hidden INVITE and 200 OK, and background DNS lookups. subgraph outer["du_f1u.pcap — the F1-U transport (sibling walkthrough)"] direction TB A["GTP-U tunnel · TS 29.281"] --> B["NR-U / NRUP flow control · TS 38.425"] B --> C["PDCP-NR · 3-byte header · NEA0 null cipher · TS 38.323"] end C -->|"strip the 3-byte PDCP header from every T-PDU"| D["IPv4"] subgraph inner["du_f1u_inner.pcap — 2,960 inner IPv4 packets"] direction TB D --> E["SIP · IMS registration"] D --> G["IPsec ESP · INVITE + 200 OK inside"] D --> F["RTP / RTCP · voice media"] D --> H["DNS · background lookups"] end
One capture becomes two. Each F1-U downlink packet is a GTP-U tunnel carrying an NR-U flow-control header and a PDCP-NR PDU. Because PDCP here is NEA0 (null cipher) with no SDAP header, stripping the 3-byte PDCP header off each T-PDU exposes the inner IPv4 packet — 2,960 of them, a complete IMS voice call.

That reconstruction — shown step by step in the next section — yields 2,960 inner IPv4 packets:

ProtocolFramesWhat it is
RTP1,670Voice media, dynamic payload type 107 (EVS or AMR-WB)
ESP208IPsec, transport mode — the encrypted SIP signaling (RFC 4303)
TLS273XCAP / Ut provisioning over HTTPS (TS 24.623)
DNS59Background app + IMS discovery
RTCP14Media sender/receiver reports
SIP6IMS signaling — REGISTER / 100 / 401, in the clear (TS 24.229)
HTTP4Provisioning / telemetry

The rest of this page walks the signaling spine of that call — a 17-frame slice scoped from the 2,960 with a Wireshark display filter, chosen to tell the whole story end to end without drowning in 1,670 RTP packets.

How the inner capture was reconstructed

The recipe is: for every F1-U packet that carries a T-PDU, drop the 3-byte PDCP header and re-emit the rest as a raw IP packet, keeping the original timestamp. The DL Data Delivery Status frames carry no T-PDU, so they fall away for free.

Everything hinges on one byte. Inspecting a single T-PDU shows exactly where the encryption boundary sits:

80 00 00 | 45 00 00 2c 8d b4 ...  0a 2e 00 be  0a 2a 00 b2  13 c4 13 c4
└─PDCP──┘   └── inner IPv4 (0x45): 10.46.0.190 → 10.42.0.178, TCP 5060 → 5060
Byte(s)ValueMeaning
00x80PDCP-NR data PDU (D/C bit set), 18-bit SN → 3-byte header
3..0x45Inner IPv4 packet begins immediately — no SDAP header

PDCP here is the NEA0 null cipher and there is no SDAP header; the inner IP packet starts at a fixed 3-byte offset. Strip those three bytes, and you have a raw IP packet. This short Python script does it for the whole capture — tshark pulls each T-PDU with its timestamp, the header is dropped, and the remainder is written as a text2pcap hex dump:

#!/usr/bin/env python3
"""Reconstruct the inner VoNR IP pcap from a bare srsRAN F1-U (GTP-U / DLT=156)
capture whose T-PDUs are [3-byte PDCP-NR header][inner IPv4], NEA0, no SDAP."""
import subprocess, sys, datetime

PCAP   = "du_f1u.pcap"
TSHARK = r"C:\Program Files\Wireshark\tshark.exe"
DLT    = 'uat:user_dlts:"User 9 (DLT=156)","gtp","0","","0",""'
OUT_TXT = sys.argv[1] if len(sys.argv) > 1 else "inner.txt"
PDCP_HDR_LEN = 3  # PDCP-NR 18-bit SN data PDU header, no SDAP

def main():
    cmd = [TSHARK, "-r", PCAP, "-o", DLT, "-T", "fields",
           "-e", "frame.time_epoch", "-e", "gtp.tpdu_data"]
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        sys.stderr.write(proc.stderr); sys.exit(1)

    kept = skipped_empty = skipped_bad = 0
    with open(OUT_TXT, "w", newline="\n") as out:
        for line in proc.stdout.splitlines():
            parts = line.split("\t")
            if len(parts) < 2:
                continue
            epoch, tpdu = parts[0].strip(), parts[1].strip()
            if not tpdu:                          # DL Data Delivery Status: no T-PDU
                skipped_empty += 1; continue
            inner_hex = tpdu[PDCP_HDR_LEN * 2:]   # drop the 3-byte PDCP header
            if not inner_hex.startswith("45"):    # keep only inner IPv4
                skipped_bad += 1; continue
            raw = bytes.fromhex(inner_hex)
            ts = datetime.datetime.fromtimestamp(float(epoch), datetime.timezone.utc)
            tsstr = ts.strftime("%Y-%m-%d %H:%M:%S.%f")
            first = True
            for off in range(0, len(raw), 16):    # emit a text2pcap hexdump
                chunk = raw[off:off + 16]
                hexbytes = " ".join(f"{b:02x}" for b in chunk)
                out.write(f"{(tsstr + ' ') if first else ''}{off:06x} {hexbytes}\n")
                first = False
            out.write("\n")
            kept += 1
    sys.stderr.write(
        f"kept={kept} skipped_empty={skipped_empty} skipped_non_ipv4={skipped_bad}\n")

if __name__ == "__main__":
    main()

Then reframe the hex dump as a LINKTYPE_RAW (101) pcap, so the bytes are read as bare IP:

python reconstruct_inner.py inner.txt
text2pcap -t "%Y-%m-%d %H:%M:%S.%f" -l 101 inner.txt du_f1u_inner.pcap

The result: 2,960 inner IPv4 packets, timestamps intact, 34,911 flow-control frames correctly skipped, zero malformed. One extra flag matters when you open the reconstructed file — enable Wireshark's heuristic RTP dissector (rtp.heuristic_rtp:TRUE), because the SDP that would normally pin the media ports is itself inside the IPsec ESP.

The same three-step pattern — tshark -e <payload field> → delete a known header prefix → text2pcap -l 101 — recovers the inner IP from many "opaque tunnel" captures, not just F1-U. The one hard requirement is that the payload be unciphered: NEA0 here is what makes strip-and-replay possible; a real cipher (NEA1 / NEA2) would need the UE's keys and cannot be recovered this way. For the full step-by-step over both the outer transport and this inner call, see the srsRAN F1-U reconstruction case study.

The flow at a glance

Two Huawei handsets (10.46.0.190 and .191) register to an IMS core at 10.42.0.178, home domain ims.mnc070.mcc001.3gppnetwork.org (the test PLMN MCC 001 / MNC 070). What you can read in the capture is only the opening move of each registration — because IMS deliberately pulls everything after it behind IPsec:

sequenceDiagram accTitle: VoNR call at a glance — visible signaling versus encrypted signaling accDescr: The UE sends a SIP REGISTER with no credentials to the IMS core P-CSCF. The P-CSCF replies 100 Trying and then 401 Unauthorized, carrying an IMS-AKA challenge and IPsec security-association parameters. IPsec ESP security associations are then established, after which the authenticated re-REGISTER, the INVITE, and the 200 OK all travel encrypted inside ESP and are not visible in the capture. Finally, the IMS media plane sends RTP voice media to the UE using dynamic payload type 107, and 1,670 RTP frames flow even though no INVITE was ever visible in the clear. participant UE as UE (IMS) participant IMS as IMS core (P-CSCF) participant M as IMS media UE->>IMS: SIP REGISTER (no credentials) IMS-->>UE: 401 Unauthorized — IMS-AKA challenge + IPsec SA params Note over UE,IMS: IPsec ESP SAs come up — all further SIP is encrypted UE->>IMS: re-REGISTER, INVITE, 200 OK (inside ESP, not visible) M->>UE: RTP voice media — payload type 107 Note over UE,M: 1,670 RTP frames flow — yet no INVITE was seen in the clear
The whole call in one view. Only the first REGISTER and its 401 challenge are visible in the clear; once the IPsec security associations come up, every subsequent SIP message — the authenticated re-REGISTER, the INVITE, the 200 OK — rides inside ESP. The media plane is not IPsec-protected, so RTP stays readable even though the INVITE that set it up is not.

The walkthrough follows that arc in five phases: background DNS (with a resolver-failover), the SIP registration and its IMS-AKA challenge, the IPsec ESP associations that make the signaling go dark, the RTP media that flows anyway, and the RTCP reports that ride alongside it.

Phase 1 — Background DNS, and a resolver that never answers

The very first inner frame isn't voice at all — it's the handset's background app traffic.

sequenceDiagram accTitle: DNS resolver failover accDescr: The UE on its data APN sends a DNS query with transaction ID 0xda1e to its primary resolver 8.8.8.8. No response arrives. After about 1.95 seconds, the UE retransmits the identical query, same transaction ID, to its secondary resolver 8.8.4.4, which answers immediately. participant UE as UE (data APN) participant P as DNS 8.8.8.8 (primary) participant S as DNS 8.8.4.4 (secondary) UE->>P: DNS query — txn 0xda1e (connectivitycheck…) Note over UE,P: ~1.95 s of silence — primary never answers UE->>S: DNS query — same txn 0xda1e (retransmit) S-->>UE: DNS response — resolved

On its data APN (10.45.0.181), the UE queries its primary resolver 8.8.8.8 for connectivitycheck.platform.hicloud.com — a Huawei connectivity probe — with transaction ID 0xda1e. No answer comes back. About 1.95 seconds later, the UE retransmits the identical request — same transaction ID, same name — to its secondary resolver 8.8.4.4, which answers at once with a CNAME chain into Huawei's CDN and six A records.

This is classic resolver failover, not packet loss. Aggregated over the whole capture, the pattern is unambiguous: 8.8.8.8 receives 13 queries and answers zero; 8.8.4.4 answers all 23. The primary is silently unreachable on this testbed's egress, so every lookup eats a fixed ~2-second timeout before succeeding on the backup. The tell that separates failover from loss is that both query packets carry the same DNS transaction ID. So correlating on that ID collapses the retry and the response into one logical lookup. A silent primary resolver is easy to miss, because every name still resolves — just always two seconds slow.

Phase 2 — IMS registration, and the IMS-AKA challenge

On its IMS APN (10.46.0.190), the UE sends a SIP REGISTER over TCP to the P-CSCF.

sequenceDiagram accTitle: SIP REGISTER and the IMS-AKA 401 challenge accDescr: The UE sends a SIP REGISTER with CSeq 1 and an empty Authorization digest to the P-CSCF. The P-CSCF replies with 100 Trying, then 401 Unauthorized carrying a WWW-Authenticate header with algorithm AKAv1-MD5 and a Security-Server header advertising the IPsec security-association parameters. participant UE as UE (IMS) participant IMS as P-CSCF UE->>IMS: REGISTER (CSeq 1, empty digest) IMS-->>UE: 100 Trying IMS-->>UE: 401 Unauthorized — AKAv1-MD5, spi-s=4111

REGISTER. The request is for sip:001700000000004@ims.mnc070.mcc001.3gppnetwork.org. Its Contact header advertises the MMTEL IMS communication service (+g.3gpp.icsi-ref=…mmtel) and an audio media feature tag — this device is registering specifically to make voice calls. CSeq: 1 with an empty Authorization digest (nonce="", response="") marks it as the first, unauthenticated attempt: the UE has no credentials to offer yet because it hasn't been challenged.

100 Trying, then 401. The P-CSCF acknowledges with 100 Trying, then rejects the bare REGISTER with 401 Unauthorized — the status line literally reads Challenging the UE. Two headers make this an IMS registration rather than a plain SIP one:

Both handsets run this same exchange; the second UE (10.46.0.191, a video-capable client — its Contact advertises audio and video) gets its own 401, its own challenge, and its own SA parameters.

Phase 3 — IPsec ESP: the signaling goes dark

Watch spi-s=4111 in the following flow:

sequenceDiagram accTitle: IPsec ESP security associations come up accDescr: About 0.4 seconds after the 401, the first ESP packet flows from the UE to the P-CSCF on SPI 0x0000100f, which is 4111 in decimal, exactly the spi-s value advertised in the 401. The P-CSCF replies with ESP on a different SPI. From here, the re-REGISTER, INVITE, and 200 OK all ride inside ESP. participant UE as UE (IMS) participant IMS as P-CSCF UE->>IMS: ESP — SPI 0x0000100f (= spi-s 4111) IMS-->>UE: ESP — SPI 0x00ad2f75 Note over UE,IMS: re-REGISTER, INVITE, 200 OK all ride inside ESP

Roughly 0.4 seconds after the 401, the first ESP packet flows from the UE to the P-CSCF on SPI 0x0000100f — decimal 4111, exactly the spi-s value the P-CSCF advertised in the 401's Security-Server header. The security association is up, and the header ties the challenge to the tunnel that answers it. The reverse direction uses a different SPI (0x00ad2f75); a third (0x0473f2ae) appears shortly after, because TS 33.203 establishes two pairs of SAs (on protected client and server ports), each end choosing the inbound SPI for its own direction.

From this point, every SIP message is encrypted: the AKA-authenticated re-REGISTER, its 200 OK, and — crucially — the INVITE and 200 OK that negotiate the call. ESP shows only its SPI and a sequence number; the payload is ciphered with AES-CBC. There are 208 of these frames across the trace, and not one byte of them is readable.

Phase 4 — Voice with no visible INVITE

The cleartext SIP shows only REGISTER → 100 → 401 — yet 1,670 RTP voice frames flow. Where's the INVITE that set up the call?

sequenceDiagram accTitle: RTP voice media with anchored media plane accDescr: UE-B sends an RTP packet with the marker bit set, dynamic payload type 107, to the IMS media plane. The media anchor relays the same packet, with identical SSRC, sequence number, and timestamp, on to UE-A. UE-B then sends the next RTP packet with the marker bit clear and a timestamp 20 milliseconds later. participant B as UE-B participant M as IMS media anchor participant A as UE-A B->>M: RTP — PT 107, marker set (talkspurt start) M->>A: RTP — same SSRC, relayed (anchored media) B->>M: RTP — marker clear, +20 ms of audio

It's inside the IPsec ESP from Phase 3. In IMS (TS 33.203), once registration establishes the SAs, all subsequent SIP signaling moves inside ESP — including the INVITE/200 OK that negotiated the media. The media plane itself is not IPsec-protected, so RTP stays visible while the signaling that created it is opaque. The "incomplete" SIP dialog isn't a failure — it's IMS working exactly as specified. If you're debugging a VoNR call and the INVITE seems to be missing, this is the first thing to check.

The media itself has two details worth reading:

Phase 5 — RTCP: the quality reports riding alongside

Every couple of seconds, each endpoint emits an RTCP Sender Report on the paired control port. UE-B's SR (SSRC 0x470ce6a2) reports its running totals — 65 packets, 3,811 octets — with an NTP wall-clock stamp that lets the far end align this stream against others for lip-sync and jitter calculation. Like the media, the RTCP is relayed through the anchor: the same report, identical counts and timestamp, reappears on the leg to UE-A. RTCP is how the endpoints track loss and jitter without ever touching the media itself.

Takeaways

  1. A "GTP-only" trace is often a stack of tunnels. When a dissector stalls, read a few payload bytes by hand: here, 0x45 right after the PDCP header marks plaintext IPv4 waiting underneath.
  2. Treat a reconstructed capture as a new problem. Peeling the F1-U tunnel hands you a different stack — SIP, RTP, ESP, DNS — with its own endpoints, sessions, and failure modes.
  3. With IPsec SAs, the INVITE is ciphered. After the 401 challenge and the IPsec SAs, all SIP rides inside ESP. Visible RTP with no visible INVITE is IMS working as specified (TS 33.203), not a broken call.
  4. The 401 tells you which tunnel to expect. The Security-Server header's spi-s reappears as the SPI on the very first ESP packet — a clean way to tie an IMS-AKA challenge to the security association that answers it.
  5. Correlate DNS on the transaction ID. Two queries and one answer isn't loss when both queries share an ID — it's resolver failover, and a silent primary that costs every lookup a fixed timeout.

The other half of the trace

This was the inner call. The outer F1-U transport that carried it — the NR-U flow-control loop between the gNB's CU-UP and DU, where these voice packets traveled as GTP-U DL User Data — is read frame by frame in the sibling walkthrough, Inside F1-U: NR-U Flow Control Between the gNB's CU and DU.

Try it on your own capture

Every arrow in this walkthrough was decoded and captioned by VisualEther from a raw Wireshark PCAP — no manual diagramming. Point it at your own 5G, LTE, or IMS trace and read it the same way.