
This article is an excellent piece from the KX Forum
KX Forum Author ID: xwtwho
Documenting the process of learning the QUIC protocol, the testing project is the testing app mentioned in the previous post:
Some video app (V15.7) and web analysis record | bbs.pediy.com
Learning record of some video app (V15.7-V18.4) | bbs.pediy.com
Software and hardware environment:
Some audio android (Yingyongbao channel)
LineageOS 17.1 (Android 10)
It is understood that QUIC also started from some audio, previously saw related posts mentioning QUIC, saying that using Cronet means using the QUIC protocol. Now it feels like there is a conceptual issue, Cronet is a protocol stack component,
supporting many protocols such as HTTP, FTP, etc., and QUIC is also supported. Using Cronet does not mean that it will use the QUIC protocol.
Using some audio libsscronet.so to access the same link, comparing the returned data whether the QUIC protocol is enabled, as shown in the following two debugging screenshots:
When QUIC is enabled, mNegotiatedProtocol is http/2+quic/43;
When QUIC is disabled, it is h2.


At first, I was confused when I saw QUIC and checked some materials:
Cronet Network Protocol Selection: HTTP2 vs QUIC Race – Bilibili
QUIC Exploration (2): Compiling the First QUIC Project – Jianshu
[Translation] QUIC and HTTP/3: Too Big to Fail? – Factors Leading to QUIC’s Failure – Foreign Translation – KX Forum – Security Community | Security Recruitment | bbs.pediy.com
Practical | QUIC Protocol Helps Tencent Business Speed Up by 30% (qq.com)
Practical | QUIC Protocol Implementation in Ant Group (qq.com)
Influenced by a previous post about some audio, I thought that the protocol on mobile phones at the transport layer all used QUIC. I directly intercepted some audio packets, performed common operations (like watching videos, liking, following, etc.), but did not find any packets related to QUIC, still TLS 1.2, which left me completely puzzled.
Later, I thought of directly using the testing app to call libsscronet.so to connect to the local server. Considering that QUIC requires encryption, I needed to use HTTPS to connect (to connect to the local server, the SSL certificate verification at the Java layer and native layer needs to be bypassed; it has been mentioned in posts on this forum that if the server has a formal certificate, the Java layer does not need modification):
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder( "https://10.0.117.217/about", new MyUrlRequestCallback(), executor);
As a result, I found that it was still TLS 1.2, indicating that it was still using TCP:

Here is a note on the JA3 fingerprint:

tls.handshake.ja3_full:771,64250-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,2570-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-10794-21,2570-29-23-24,0[JA3: ca6385bf726b3d5f2b4db30186da0fec] This JA3 is actually md5(ja3_full).
Comma-separated into 5 parts, corresponding as follows:
771: Version: TLS 1.2 (0x0303)64250-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53:
2570-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-10794-21:
2570-29-23-24: Supported Groups (4 groups) Supported Group: Reserved (GREASE) (0x0a0a) Supported Group: x25519 (0x001d) Supported Group: secp256r1 (0x0017) Supported Group: secp384r1 (0x0018)
Speculated to correspond to Extension: padding
Later I saw the following image:
I wondered if it was because I didn’t use TLS 1.3, so I planned to use TLS 1.3 to request aweme.snssdk.com locally to see what the return was,Node requests options need to be set as follows, requiring version 14.18.2 or higher:
secureOptions: m_constants.SSL_OP_NO_TLSv1 | m_constants.SSL_OP_NO_TLSv1_1 | m_constants.SSL_OP_NO_TLSv1_2
This looks like the TLSv1.3 protocol, but I still didn’t see any information related to QUIC.
Later I looked at some materials mentioning that YouTube (https://www.youtube.com) already supports QUIC access, so I tested it:
Indeed, there is a header field Alt-Svc, as mentioned in the article below
https://blog.csdn.net/wangyiyungw/article/details/81746901
This strange way of accessing QUIC services requires the terminal to know in advance the protocol and service port used to access a specific website. This is very inflexible, and thus it is mainly used in cases lacking proper protocol negotiation mechanisms during the experimental phase of the QUIC protocol. Recently, an HTTP mechanism called Alternative Services was standardized, with its specification document RFC7838.
This mechanism allows access to an HTTP resource to be redirected to another network location, even accessed with a different protocol configuration. Specifically, this mechanism adds a new header field Alt-Svc to HTTP, allowing the HTTP server to inform the client of the service information to which it is redirected, including protocol, port number, etc.
Accessing YouTube via a mobile browser, I also intercepted packets and saw QUIC-related protocols:
However, I found that some were GQUIC, so I checked the information:
Created by Google and submitted to IETF under the name QUIC, the protocol is completely different from the one subsequently created in IETF (although the name is the same). The original Google QUIC (also known as gQUIC) is strictly a protocol for sending HTTP/2 frames over encrypted UDP, while the IETF-created QUIC is a general transport protocol, meaning that other protocols (such as SMTP, DNS, SSH, Telnet, NTP) can also use it.
It is important to note and remember their differences. Since 2012, the version of QUIC used by Google in its services and Chrome (until February 2019) was Google QUIC. Over time, it is gradually becoming similar to IETF QUIC (also known as iQUIC).
Knowing that QUIC protocol can be used to access YouTube, I wanted to use Cronet in the testing app to access it, needing to set enableQuic to true and call addQuicHint to add the domain name used for QUIC:
public CronetEngineBuilderImpl(Context context) { this.mApplicationContext = context.getApplicationContext(); enableQuic(true); // Here you can set to enable QUIC enableHttp2(true); enableBrotli(false);
addQuicHint(“m.youtube.com”,443,443); // If this is not set, it will use TLSv1.3 protocol; only when set will it use QUIC protocol.
Some audio had a version upgrade to QUICVersion:c9f35932 2021-09-15, then downgraded to QUICVersion:68cae75d 2021-08-12, and the latest version 19.5 of QUIC is still this version.
It can also be done through:
cronetEngine.startNetLogToFile(“/data/local/tmp/cronet_log.json”,true);
Output logs, and then use the following address to analyze this log:https://netlog-viewer.appspot.com/#events
This can also be used to intercept packets in some audio:
Java.choose("org.chromium.CronetClient", { onMatch: function (instance) { instance.getCronetEngine().startNetLogToFile("/data/local/tmp/cronet_log.json", true); }, onComplete: function () { }});
At this point, I was able to call libsscronet.so to use the QUIC protocol, and I wanted to go back and continue to access the some audio protocol and analyzed the data from the beginning. I found QUIC packets when some audio started:

I saw this domain name api5-core-c-lf.amemv.com.
Here, it should be mentioned that QUIC has a racing mode, and it will also send a TCP link. If the network or server does not support QUIC/UDP, the client marks QUIC as broken and will try to retry QUIC later. Once it successfully adopts QUIC again, it will cancel the broken mark:
Later, I discovered this URL, which happened to be consistent with the domain name found earlier:
https://api5-core-c-lf.amemv.com/top/v1?Action=ApplyUploadInner&Region=CN&SpaceName=aweme&UseQuic=false&Version=
There is also a field UseQuic=false, further indicating that the functions related to this protocol will use QUIC (later found to be related to video uploads), although this URL request itself may not necessarily use QUIC, but this domain name is consistent with the previously intercepted QUIC packet, which can be used for testing.
Testing access to this domain with the app also allows normal access to the QUIC protocol.
At this point, I need to continue to look at the protocol format and the QUIC request process in the Cronet module,
During debugging, I found QUIC packets at sendto:
Here I encountered a knowledge blind spot, finding that the sendto call did not set the destination address parameter, which left me puzzled about how to know where the data is sent. I tried to break at sendmsg, but found it was not, and after checking the information, I found:
UDP can also have a connect connection
If the sendto() and sendmsg() functions send messages to a connected process, they ignore the parameters dest_addr, addrlen, and the members of the msg structure used to pass the address. If the parameters dest_addr and addrlen are not NULL, an error EISCONN or 0 may be returned;
Connecting sockets incurs certain overhead, such as looking up routing table information.
Therefore, the UDP client program can obtain a certain performance improvement through connect.
Breaking at connect, I found the target IP and port:
During the debugging and analysis process, I found that having a local server would be more convenient. I saw online that Node supports QUIC, so I planned to set up a QUIC-supported server with Node.js.
I directly downloaded version V16+, but found it unsupported. Later, I checked the submission log and found that there was a submission that removed this QUIC support:
quic: remove experimental quic by jasnell · Pull Request #37067 · nodejs/node (github.com)
I need to compile the 15.X code with experimental_quic
git clone https://github.com/nodejs/node
After setting up the TCP and UDP servers locally and testing, it was verified that enabling QUIC request URLs indeed sends both TCP and UDP requests:
During the protocol debugging process, I found that it was not working. After flipping through the code, I found the ngtcp2-related information and checked:
ngtcp2/ngtcp2: ngtcp2 project is an effort to implement IETF QUIC protocol (github.com)
The ngtcp2 project implements the IETF QUIC protocol, which is different from some audio. The minimum data length of the protocol frame is also inconsistent, and actually testing this requires modification based on the version of some audio.
Currently, QUIC is still not unified, and many materials are not based on gQUIC, and even if they are, they may be different versions.
The header formats currently have several types:
enum PacketHeaderFormat : uint8_t {
IETF_QUIC_LONG_HEADER_PACKET,
IETF_QUIC_SHORT_HEADER_PACKET,
I thought of using the Google version directly, so I pulled down quiche:
https://chromium.googlesource.com/chromium/src;
https://quiche.googlesource.com/quiche。
The protocol in here is consistent with the QUIC protocol of some audio. I should mention the bit3 of Public Flags:
0x08: Bit3 being set indicates that the header contains an 8-byte connection ID, this flag should be set in all packets.
// All versions of Google QUIC up to and including Q043 set
// FLAGS_DEMULTIPLEXING_BIT to one on all client-to-server packets. Q044
// and Q045 were never default-enabled in production. All subsequent
// versions of Google QUIC (starting with Q046) require FLAGS_FIXED_BIT to
// be set to one on all packets. All versions of IETF QUIC (since
// draft-ietf-quic-transport-17 which was earlier than the first IETF QUIC
// version that was deployed in production by any implementation) also
// require FLAGS_FIXED_BIT to be set to one on all packets. If a packet
// has the FLAGS_LONG_HEADER bit set to one, it could be a first flight
// from an unknown future version that allows the other two bits to be set
// to zero. Based on this, packets that have all three of those bits set
// to zero are known to be invalid.
It is said that Google removed the compilation support on Windows, so I planned to make some changes, hoping to compile with VS for easier debugging. However, the more I modified, the more work it seemed, so I gave up. I found lsquic, which is still implemented in C, and I pulled down the code to compile a version (basically following the documentation, needing to compile boringssl first; both perl and go need to be installed, and after installing perl, you need to manually add it to the environment variable; I got stuck here before):
litespeedtech/lsquic: LiteSpeed QUIC and HTTP/3 Library (github.com)
Tutorial — lsquic 3.0.4 documentation

After starting the built-in test program’s server, the app initiated a connection, and the server received the request normally. Remember to set the certificate to start, otherwise, processing CHLO Frame data will be abnormal:
http_server.exe -r ./ -s 10.0.117.217:443 -c 10.0.117.217,file.crt,private.pem
After reconnecting, I found there was interactive data:
However, it was subsequently disconnected by the client, finding that it was a certificate verification error. The test certificate I generated was self-signed:
I had already modified the certificate verification (both at the Java layer and the SO), and I could connect to my local HTTPS server, but I couldn’t connect to the QUIC server. Even setting enablePublicKeyPinningBypassForLocalTrustAnchors attribute didn’t work, indicating that there is still a verification when using QUIC, and by searching for the string, I found the following place:
In the Google code, I also found the corresponding:
Finding modification points according to the code:
At this point, the server did not receive the packets from the client due to verification disconnection, and intercepting the packets showed that the authentication had passed, unlike before where it would disconnect:

At this point, I basically had a complete debugging environment, and later needed to analyze the QUIC protocol algorithm, which would allow for easy debugging with source code.
Currently, it seems that some audio has not widely adopted QUIC. From the tested URL, it seems that uploading videos might use it, but my testing environment has not found QUIC in use. In recent versions, I have seen the following information added to the protocol:
current_network_quality_info={"http_rtt":27,"tcp_rtt":15,"quic_rtt":15,"downstream_throughput_kbps":1600,"net_effective_connection_type":7,"video_download_speed":3311,"quic_receive_loss_rate":-1,"quic_send_loss_rate":-1}
This looks related to QUIC, but I don’t know if it is collecting user environment information, evaluating user network conditions, and then deciding whether to deploy QUIC.
Even if some functions in the HTTP protocol shift to QUIC later, there are many entry points:
1. For those using Cronet, it is still possible to obtain request data through corresponding JNI functions and Cronet callback functions at the JAVA layer.
2. Cronet ultimately still goes through the native layer, and the corresponding functions for sending and receiving data at the SO layer can also obtain data.
3. Using the cronetEngine.startNetLogToFile mentioned above to directly write network logs (this has been found in practical tests to be incomplete, and it is still uncertain whether it is a parameter setting issue).
4. Directly retrieve data from the send and receive functions in the QUIC implementation part.
5. After understanding the QUIC protocol process, the encryption keys at various communication levels can also be recorded to directly parse the UDP intercepted packet data.
6. As tested above, set up a local QUIC server to redirect requests to the local server.