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:
That reconstruction — shown step by step in the next section — yields 2,960 inner IPv4 packets:
| Protocol | Frames | What it is |
|---|---|---|
| RTP | 1,670 | Voice media, dynamic payload type 107 (EVS or AMR-WB) |
| ESP | 208 | IPsec, transport mode — the encrypted SIP signaling (RFC 4303) |
| TLS | 273 | XCAP / Ut provisioning over HTTPS (TS 24.623) |
| DNS | 59 | Background app + IMS discovery |
| RTCP | 14 | Media sender/receiver reports |
| SIP | 6 | IMS signaling — REGISTER / 100 / 401, in the clear (TS 24.229) |
| HTTP | 4 | Provisioning / 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) | Value | Meaning |
|---|---|---|
| 0 | 0x80 | PDCP-NR data PDU (D/C bit set), 18-bit SN → 3-byte header |
| 3.. | 0x45 | Inner 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.(cmd, capture_output=True, text=True)
if proc.returncode != 0:
sys.stderr.(proc.stderr); sys.(1)
kept = skipped_empty = skipped_bad = 0
with open(OUT_TXT, "w", newline="\n") as out:
for line in proc.stdout.():
parts = line.("\t")
if len(parts) < 2:
continue
epoch, tpdu = parts[0].(), parts[1].()
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.("45"): # keep only inner IPv4
skipped_bad += 1; continue
raw = bytes.(inner_hex)
ts = datetime.datetime.(float(epoch), datetime.timezone.utc)
tsstr = ts.("%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 = " ".(f"{b:02x}" for b in chunk)
out.(f"{(tsstr + ' ') if first else ''}{off:06x} {hexbytes}\n")
first = False
out.("\n")
kept += 1
sys.stderr.(
f"kept={kept} skipped_empty={skipped_empty} skipped_non_ipv4={skipped_bad}\n")
if __name__ == "__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:
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.
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.
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:
WWW-Authenticate: … algorithm=AKAv1-MD5, nonce="…"— this is IMS-AKA (3GPP TS 33.203). Thenonceis not a random string; it packs the AKARANDandAUTNthe UE feeds to its USIM to authenticate the network and derive the session keys.Security-Server: ipsec-3gpp; … spi-s=4111; port-s=6107; alg=hmac-md5-96; ealg=aes-cbc— the P-CSCF hands the UE the IPsec security-association parameters to establish. The UE will compute the AKA response and send its authenticated re-REGISTER inside IPsec.
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:
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?
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:
- The codec is wideband voice. Dynamic payload type 107, SSRC
0x470ce6a2, with the RTP marker bit set on the first packet of each talkspurt — and the RTP timestamp advances by 320 per packet, i.e. 20 ms of audio at a 16 kHz clock (EVS or AMR-WB). That 20 ms cadence is exactly the packetization that rode, invisibly, inside the F1-U DL User Data of the outer capture — the two walkthroughs meet here. - The media is anchored in the core. The same RTP packet appears twice — UE-B → media anchor (
10.42.0.183), then anchor (10.42.0.1) → UE-A — with identical SSRC, sequence number, and timestamp. The voice isn't sent UE-to-UE directly; it traverses an IMS media anchor, as IMS voice normally does.
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
- A "GTP-only" trace is often a stack of tunnels. When a dissector stalls, read a few payload bytes by hand: here,
0x45right after the PDCP header marks plaintext IPv4 waiting underneath. - 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.
- With IPsec SAs, the INVITE is ciphered. After the
401challenge 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. - The 401 tells you which tunnel to expect. The
Security-Serverheader'sspi-sreappears 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. - 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.