This repository is part of the implementation for the SIGCOMM 2026 paper "Synchronizing with the Scheduler: Dual-Loop Congestion Control for 5G Uplink on Commodity Devices."
This repository implements GBR-CC in MsQuic/secnetperf. GBR-CC uses cellular scheduler telemetry from the phone modem to guide QUIC uplink pacing rate and congestion window control.
Phone modem
-> cellninjia_mobile on Android
-> adb forward to host
-> LTE/5G DIAG parser on host
-> GBR ratio calculation
-> MsQuic/secnetperf sender
-> GBR-CC_receiver cloud server
GBR-CC_sender
+-- tools/cellninjia/cellninjia_mobile/
| +-- Android-side DIAG bridge for live modem telemetry
+-- tools/cellninjia/
| +-- diag_get_lte_msquic.py
| +-- diag_get_5g_msquic.py
| +-- Host-side LTE/5G parsers and GBR ratio calculation
+-- src/perf/lib/
| +-- Ratio receiver and secnetperf integration
+-- src/core/
| +-- GBR-CC control hook in the MsQuic congestion-control path
+-- scripts/
| +-- Build and run helpers
+-- analysis/
| +-- Offline log analysis utilities
+-- docs/
+-- Architecture and implementation notes
This repository is the phone-side, host-parser, and sender-side GBR-CC implementation. It is designed to run together with the companion cloud receiver repository:
https://github.com/NEMO-LAB-repo/GBR-CC_receiver
Use GBR-CC_sender for the phone DIAG bridge, host DIAG parser, GBR ratio
delivery, and the MsQuic/secnetperf sender. Use GBR-CC_receiver on the
cloud machine to build and start the remote secnetperf receiver. The cloud
server IP from that repository is the <server-ip> used by the sender commands
below.
On phones with Android Linux kernels after 5.4.61, Qualcomm DIAG is no longer
exposed as the /dev/diag character device. On those phones, both modem message
reads and buffer-drain commands have to go through diagrouter instead of the
legacy DIAG ioctl device.
The diagrouter open path is in
tools/cellninjia/cellninjia_mobile/cellninjia_mobile.c:
check_system_version()probes the abstract Unix socket\0diagwithsocket(AF_UNIX, SOCK_SEQPACKET, 0)andconnect(). If that succeeds,cellninjia_mobileenters socket mode.main()opens the real diagrouter connection the same way and stores it indiag_sock. All host DIAG commands from the parser are written to this socket, and live modem DIAG messages are read from it.
The drain path is different for the two kernel interfaces:
- Socket/diagrouter mode, used on newer kernels without
/dev/diag: the host parser performs the drain operation. Bothdiag_get_lte_msquic.pyanddiag_get_5g_msquic.pydetect theSocket modewelcome message fromcellninjia_mobile, send the socket-mode initialization commands, then startdrain_buffer_thread(). That thread sendsDRAIN_BUFFER_COMMAND = b'\x24\x00\x00\x00\x00\x00\x00\x00'every 100 microseconds over the TCP bridge.cellninjia_mobileforwards those bytes todiag_sock, so the buffer-drain command reaches diagrouter. - Legacy
/dev/diagmode, used on older kernels: the phone bridge opens/dev/diag, switches the DIAG driver into memory-device logging mode, and configures the peripheral buffer. Android 10+ usesstruct diag_logging_mode_param_tplusDIAG_IOCTL_QUERY_CON_ALL; Android 9 and older usestruct diag_logging_mode_param_t_9and the older remote-device ioctl path. After the final DIAG configuration pattern60 00 12 6a 7e,start_drain_thread()startsdrain_thread_func(), which callsDIAG_IOCTL_PERIPHERAL_BUF_DRAINevery 100 microseconds.
In both modes, cellninjia_mobile is not reading a saved DIAG log. It opens the
live DIAG endpoint, immediately forwards each successful DIAG read() to the
host TCP client, and prepends an 8-byte Unix timestamp to the raw DIAG payload.
The host parser therefore sees modem telemetry as an online, low-latency stream
rather than an offline dump.
The live forwarding path is:
diagrouter socket or /dev/diag
-> read() in diag_read_thread()
-> prepend host read timestamp
-> write() to TCP clients on port 43555
-> adb forward to the host parser
After this live DIAG stream reaches the host, the LTE or 5G parser connects to
127.0.0.1:43555, computes the GBR ratio from BSR demand and grant/PUSCH
allocation, and sends one little-endian double to MsQuic through
/tmp/msquic_cellular_ratio.sock.
Use one host parser based on the radio mode:
tools/cellninjia/diag_get_lte_msquic.py: LTE DIAG parser.tools/cellninjia/diag_get_5g_msquic.py: 5G NR DIAG parser.
Fresh checkout:
git clone https://github.com/NEMO-LAB-repo/GBR-CC_sender.git
cd GBR-CC_sender
git submodule update --init --recursiveExisting checkout:
cd /home/qwu26/GBR-CC_sender
git submodule sync --recursive
git submodule update --init --recursivecd /home/qwu26/GBR-CC_sender
rm -rf build
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DQUIC_BUILD_PERF=ON \
-DQUIC_ENHANCED_PACKET_LOGGING=ON \
-DQUIC_TLS_LIB=openssl
cmake --build build --target secnetperf -j"$(nproc)"Expected binary:
/home/qwu26/GBR-CC_sender/build/bin/Release/secnetperf
cd /home/qwu26/GBR-CC_sender
make -C tools/cellninjia/cellninjia_mobile clean all
make -C tools/cellninjia/cellninjia_mobile pushThe default Android NDK path in the Makefile is:
/home/qwu26/android-ndk-r26d
Override it when needed:
make -C tools/cellninjia/cellninjia_mobile clean all ANDROID_NDK_HOME=/path/to/android-ndkThe push target installs the binary to:
/data/local/tmp/cellninjia_mobile
Keep this running while collecting cellular telemetry:
adb forward tcp:43555 tcp:43555
adb shell su -c /data/local/tmp/cellninjia_mobileRun exactly one parser for the current radio mode:
cd /home/qwu26/GBR-CC_sender
python3 tools/cellninjia/diag_get_lte_msquic.pyor:
cd /home/qwu26/GBR-CC_sender
python3 tools/cellninjia/diag_get_5g_msquic.pyThe parser connects to 127.0.0.1:43555, computes the ratio, and sends it to
MsQuic as one double on /tmp/msquic_cellular_ratio.sock.
First start the receiver on the cloud server from the companion repository:
git clone https://github.com/NEMO-LAB-repo/GBR-CC_receiver.git
cd GBR-CC_receiverFollow that repository's README to build and start the cloud-side receiver on
port 4433. Then use that cloud machine's IP address as <server-ip> below.
A minimal upload run:
cd /home/qwu26/GBR-CC_sender
./build/bin/Release/secnetperf \
-target:<server-ip> \
-port:4433 \
-cc:bbr \
-upload:20mb \
-ptput:1 \
-cellular:1-cc:bbr selects the MsQuic congestion-control path where GBR-CC is integrated.
-cellular:1 enables ratio-driven pacing and cwnd control.
-cellular:0: receive and log GBR ratio samples without applying control.-cellular:1: apply GBR ratio to pacing rate and congestion window control.
src/perf/lib/cellular_ratio.c: Unix datagram receiver for ratio samples.src/perf/lib/cellular_ratio.h: receiver API and shared stats.src/core/bbr.c: MsQuic congestion-control hook where GBR-CC control is applied.src/perf/lib/SecNetPerfMain.cpp:secnetperfintegration and-cellularoption.tools/cellninjia/diag_get_lte_msquic.py: LTE DIAG parser for MsQuic.tools/cellninjia/diag_get_5g_msquic.py: 5G NR DIAG parser for MsQuic.tools/cellninjia/cellninjia_mobile/: phone-side DIAG bridge source.analysis/transport/: transport-log analysis scripts.docs/gbr-cc/: GBR-CC notes and repository layout documentation.