TUIC v5 explained: how the QUIC proxy protocol works
TUIC is a proxy protocol that relays TCP and UDP over QUIC, and v5 (version byte 0x05) is the current version. A client proves who it is with a UUID and a 32-byte token derived from its password through the TLS session, so the password itself never travels, and relaying starts at once because the server never sends replies. The spec now lives on its own at tuic-protocol/tuic, and the main maintained servers are sing-box, Mihomo and the Itsusinn/tuic fork.
ST Muntaha10 min read
TUIC gets less attention than Hysteria2, its QUIC neighbour, and many guides still point at software that has since moved. This post goes back to the specification and to the implementations that are maintained in November 2026. Configs are for sing-box 1.14.x and current Mihomo.
What is TUIC?
TUIC is a network proxy protocol for relaying TCP and UDP traffic. The TUIC specification is short: one page, five commands, and an address format. The current protocol version is 0x05, which is why people say "TUIC v5". The project states that different protocol versions are not guaranteed to work together, and in practice v4 and v5 do not. Mihomo still has a token field for v4, and NekoBox has dropped v4 support. Anything new should be v5.
The goals listed in the repository are:
- relaying TCP and UDP with no extra round trips ("0-RTT");
- full cone NAT behaviour for UDP, so games and calls work through the proxy;
- both lossy and lossless UDP relaying;
- the QUIC benefits: built-in encryption, user-space congestion control, stream multiplexing and connection migration.
The spec says it does not care about the transport underneath, but it is designed for QUIC, and every implementation runs it over QUIC.
Which TUIC implementations are maintained?
The original TUIC repository by EAimTY is now tuic-protocol/tuic. It holds only the specification. Its README says there is no official implementation in that repository, so the project can focus on the protocol design. Older guides that link to EAimTY/tuic releases for tuic-server are out of date.
The repository lists these server implementations, among others:
| Implementation | Role | State in November 2026 |
|---|---|---|
| sing-box | Server and client | Stable 1.14.4 (24 October 2026) |
| Mihomo (Clash Meta) | Server and client | Latest release v1.19.33 (21 October 2026) |
| Itsusinn/tuic | tuic-server and tuic-client |
Describes itself as the successor of the official implementation. Stable line 1.8.x (1.8.11 in July 2026); 2.0.0 is in development builds (2.0.0-dev10 on 29 October 2026) |
On the client side, TUIC v5 works in sing-box and the apps built on it, Mihomo, NekoBox, v2rayN and Shadowrocket. Xray-core does not support TUIC: its inbound and outbound lists include Hysteria, but not TUIC. If your fleet runs only Xray, TUIC needs a second core next to it.
The Itsusinn fork has also added features the spec does not define, such as built-in ACME, a REST API and a masquerade section, and its config moved to TOML (older JSON files are still read). The rest of this post uses sing-box, because it has a stable release line and its options are documented in English.
How does a TUIC v5 connection work?
The client opens a normal QUIC connection with TLS 1.3. Then everything is a "command": a version byte, a type byte, and data. There are five:
| Command | Type | Sent over | Purpose |
|---|---|---|---|
Authenticate |
0x00 |
Unidirectional stream | UUID and token |
Connect |
0x01 |
Bidirectional stream | Open a TCP relay to an address |
Packet |
0x02 |
Datagram or unidirectional stream | One UDP packet, or a fragment of one |
Dissociate |
0x03 |
Unidirectional stream | End a UDP session |
Heartbeat |
0x04 |
Datagram | Keep the connection alive |
Authentication is the clever part. The Authenticate command carries the 16-byte UUID and a 32-byte token. The token is not the password. It is computed with the TLS keying material exporter from RFC 5705: the label is the UUID and the context is the raw password, on the current TLS session. The server does the same computation with the password it has stored for that UUID and compares the results.
Two things follow. The password never crosses the network, not even inside TLS. And the token belongs to one TLS session: copy it into another connection and it does not match.
The client does not have to wait for authentication to finish. It can send Connect commands at the same moment; the server holds them until the Authenticate command checks out, then runs them.
TCP relaying is simple. The client opens a bidirectional stream, writes Connect with the target address, and starts sending data straight away. The server never answers the command. It connects to the target and copies bytes both ways.
What happens when TUIC authentication fails?
The spec has no responses at all, so there is no "wrong password" message. What happens on errors is up to the implementation; the server may close the connection or ignore the command.
In sing-box, whose TUIC code is in its sing-quic library, a bad token or an unknown UUID closes the whole QUIC connection, and the server logs one of these at error level:
connection failed: handle uni stream: authentication: token mismatch
connection failed: handle uni stream: authentication: unknown user 7c1e4b2a-9f3d-4c8e-a6b5-2d0f8e1a3c97
A client that completes QUIC but never sends Authenticate is closed after auth_timeout (3 seconds by default) with authentication timeout. On the client side, you usually only see the connection close and requests fail. That is why, with TUIC, the server log is the first place to look.
What does "0-RTT" mean in TUIC?
The word means two different things, and people mix them up.
The first is in the protocol. Because commands get no reply and authentication runs in parallel, a TCP relay costs no extra round trip after the QUIC handshake. This is always on. It is what TUIC was designed for.
The second is the QUIC 0-RTT handshake, where a returning client sends data in its very first packet, before the handshake completes. It is an option: zero_rtt_handshake in sing-box, reduce-rtt in Mihomo. The sing-box docs recommend leaving it disabled, because 0-RTT data can be replayed by an attacker, and they note that it does not change performance much since the protocol is fully multiplexed. I leave it off.
How does TUIC relay UDP: native or quic mode?
TUIC gives each UDP session a 16-bit associate ID chosen by the client. The server opens one UDP socket per ID and sends replies from any source back with the same ID. That is how it achieves full cone NAT behaviour. A UDP packet that is too large is split into fragments, and the Packet command carries the fragment numbers.
The client chooses how Packet commands travel:
| Mode | Carried by | Behaviour |
|---|---|---|
native |
QUIC datagrams | Lossy, like real UDP; lowest overhead. Default in sing-box |
quic |
QUIC unidirectional streams | Lossless and ordered per packet; more overhead |
The server answers in the same mode the session started with. native is right for almost everything, because applications that use UDP (DNS, games, voice, QUIC itself) already expect loss. sing-box also has udp_over_stream, which is a sing-box extension and not part of TUIC; it needs a sing-box server and conflicts with udp_relay_mode.
Which congestion control should a TUIC server use?
sing-box and Mihomo offer three: cubic, new_reno and bbr. sing-box uses cubic when you set nothing. Each side controls only the data it sends, so the server's setting shapes what users download, and the client's setting shapes their uploads.
cubic and new_reno treat packet loss as a sign of congestion and slow down. On a clean path that is correct and fair to other traffic. On a long path with random loss, such as a mobile network or an international link, they slow down for loss that has nothing to do with congestion. bbr estimates the bandwidth and round-trip time instead, so random loss affects it much less. That is why many TUIC configs set bbr on both sides. Mihomo also accepts a BBR profile (standard, conservative, aggressive).
TUIC has nothing like Hysteria2's Brutal, which sends at a fixed rate you choose; I explained Brutal in the Hysteria2 protocol post. With TUIC, the congestion controller always adapts.
What about ALPN, certificates and SNI?
TUIC runs over QUIC, so it always uses TLS. Three settings must line up.
- ALPN. Both sides must offer a common value, or the TLS handshake fails.
h3is the value most configs use. If the server setsalpn, put the same value in every client config and every link. - The certificate. Use a real certificate for a domain that points at the server. With a self-signed one, clients must skip verification (
insecurein sing-box,skip-cert-verifyin Mihomo,allow_insecure=1in links), which also means they cannot detect a man in the middle. - SNI. The client sends the server name from its config. If you connect by IP, still set the name of the certificate.
The spec defines no masquerade. A TUIC server does not pretend to be a website: to a probe it is a QUIC endpoint that closes the connection when no valid Authenticate arrives.
What does a minimal TUIC v5 config look like?
Generate a UUID and a password with sing-box:
sing-box generate uuid
sing-box generate rand --hex 16
Server, /etc/sing-box/config.json:
{
"log": { "level": "info" },
"inbounds": [
{
"type": "tuic",
"tag": "tuic-in",
"listen": "::",
"listen_port": 443,
"users": [
{
"name": "alice",
"uuid": "7c1e4b2a-9f3d-4c8e-a6b5-2d0f8e1a3c97",
"password": "4f9a1c7e2b8d6a30d5c1e9f04b7a2c68"
}
],
"congestion_control": "bbr",
"zero_rtt_handshake": false,
"tls": {
"enabled": true,
"alpn": ["h3"],
"certificate_path": "/etc/ssl/vpn.yourdomain.com/fullchain.pem",
"key_path": "/etc/ssl/vpn.yourdomain.com/privkey.pem"
}
}
],
"outbounds": [
{ "type": "direct", "tag": "direct" }
]
}
Open UDP 443 in the host firewall and in your provider's panel. TCP is not used.
Client, with a local SOCKS5 port (field reference: sing-box TUIC outbound):
{
"log": { "level": "info" },
"inbounds": [
{ "type": "socks", "tag": "socks-in", "listen": "127.0.0.1", "listen_port": 1080 }
],
"outbounds": [
{
"type": "tuic",
"tag": "tuic-out",
"server": "vpn.yourdomain.com",
"server_port": 443,
"uuid": "7c1e4b2a-9f3d-4c8e-a6b5-2d0f8e1a3c97",
"password": "4f9a1c7e2b8d6a30d5c1e9f04b7a2c68",
"congestion_control": "bbr",
"udp_relay_mode": "native",
"zero_rtt_handshake": false,
"tls": {
"enabled": true,
"server_name": "vpn.yourdomain.com",
"alpn": ["h3"]
}
}
]
}
The same server as a Mihomo proxy (keys from the Mihomo TUIC docs):
proxies:
- name: tuic-fra-1
type: tuic
server: vpn.yourdomain.com
port: 443
uuid: 7c1e4b2a-9f3d-4c8e-a6b5-2d0f8e1a3c97
password: 4f9a1c7e2b8d6a30d5c1e9f04b7a2c68
alpn: [h3]
sni: vpn.yourdomain.com
congestion-controller: bbr
udp-relay-mode: native
reduce-rtt: false
How do I read a tuic:// link?
The TUIC spec defines no URI. The tuic:// link is a convention that clients agreed on in 2023, and the common form is:
tuic://7c1e4b2a-9f3d-4c8e-a6b5-2d0f8e1a3c97:4f9a1c7e2b8d6a30d5c1e9f04b7a2c68@vpn.yourdomain.com:443?sni=vpn.yourdomain.com&alpn=h3&congestion_control=bbr&udp_relay_mode=native#tuic-fra-1
| Part | Meaning |
|---|---|
uuid:password |
The user's UUID and password, before the @ |
host:port |
Server address and UDP port |
sni |
TLS server name |
alpn |
ALPN list, comma-separated; must match the server |
congestion_control |
cubic, new_reno or bbr |
udp_relay_mode |
native or quic |
allow_insecure |
1 to skip certificate checks |
disable_sni |
1 to send no server name |
#name |
Display name |
Because it is only a convention, clients read different parts of it. NekoBox reads every field above. v2rayN reads sni, alpn, allow_insecure and congestion_control, but not udp_relay_mode, so its users get the default mode whatever the link says. Put the ALPN in every link you hand out. A missing ALPN is the mismatch I would check first when a link works in one app and not in another.
How do I check that a TUIC server works?
A port check cannot tell a healthy TUIC server from a dead one: the server gives nothing to a random UDP packet. The check has to do what a client does.
-
Validate the client config:
sing-box check -c client.json. This catches typos only; it says nothing about the network. -
Start the client with
sing-box run -c client.json, then send a real request through it:curl --socks5-hostname 127.0.0.1:1080 -sS -o /dev/null -w '%{http_code}\n' https://www.gstatic.com/generate_204204means QUIC, TLS, authentication, the relay and the server's outbound all work. -
If it fails, read the server log (
journalctl -u sing-box -eon a systemd install) while you retry.token mismatchmeans a wrong password,unknown usera wrong UUID,authentication timeouta client that connected but never authenticated. -
If the server log shows nothing, watch the wire on the server:
tcpdump -ni any udp port 443. No packets at all means the firewall, the provider panel or the network path is dropping UDP before it reaches you.
Run step 2 from outside your own network, ideally from the networks your users are on. Checks from the server itself skip the part that breaks most often. The VPN server monitoring guide covers how often to check and what to alert on.
Full disclosure: TunnelHQ runs this exchange for you. You paste the tuic:// link, and every 1 to 5 minutes (depending on plan) a checker completes the QUIC handshake, authenticates with the UUID and password, and sends real requests through the tunnel. Alerts go to 20 channels. The free tester checks one link once, without an account.

