finalmask & fragment
The layer that decides what your traffic pretends to be — split TLS to beat SNI filters, or dress UDP up as DTLS, WebRTC, BitTorrent, DNS, ICMP, or a game.
Every transport in Xray puts some recognizable shape on the wire — a TLS handshake, a QUIC
packet, an HTTP request. finalmask is the layer below all of them where you decide what that
shape should be instead. It is a chain of small, composable masks you attach to streamSettings, and between them they can shred a TLS handshake so no SNI filter can read it, or
make a UDP tunnel present itself to a DPI box as a video call, a torrent, a DNS lookup, a ping —
or a packet sequence you hand-craft to look like literally any protocol, a game handshake
included.
A mask is the last thing to touch your bytes
finalmask is two ordered lists on streamSettings — tcp for stream connections and udp for packet connections — and each entry wraps the connection in one more layer. The manager
walks the list and calls WrapConnClient on each mask in turn, so mask #1 wraps the socket, mask
#2 wraps mask #1, and your bytes pass through them outward on the client and unwrap in reverse on
the server.
// TcpmaskManager.WrapConnClient — each mask wraps the previous one
func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
for _, mask := range m.tcpmasks {
raw, _ = mask.WrapConnClient(raw)
}
return raw
} Because a mask is just a net.Conn (TCP) or net.PacketConn (UDP) wrapper, it is independent of
the transport above it. You can put a mask under raw TLS, under REALITY, under VLESS-over-TCP, or
under a WireGuard/Hysteria UDP tunnel — the mask only ever sees the finished bytes on their way to
the kernel, and the far side peels the same list off in reverse.
Pick what your traffic pretends to be
The mask you choose is a choice of disguise, and the menu is wide. Some masks defend a TCP stream (fragmentation), most disguise UDP packets as a protocol a censor is unwilling or unable to block. You chain as many as you like.
| Mask | Conn | Your traffic becomes… |
|---|---|---|
fragment | TCP | A TLS handshake sliced into many small records — the SNI a filter wants to match never arrives whole. |
header/dtls | UDP | DTLS records — looks like a WebRTC/secure-datagram session. |
header/srtp | UDP | SRTP media — looks like a live audio/video call. |
header/utp | UDP | µTP — looks like BitTorrent traffic. |
header/wechat | UDP | WeChat video packets. |
header/wireguard | UDP | WireGuard datagrams. |
header/dns | UDP | DNS-shaped datagrams. |
header/custom | UDP | Anything you can describe as bytes — a hand-written client/server packet sequence, e.g. a game’s handshake. This is the “my proxy is now Minecraft” knob. |
xdns | UDP | Full DNS queries to a chosen domain — a DNS tunnel. |
xicmp | UDP | ICMP packets to an IP/id — a ping tunnel. |
noise | UDP | Your real packets, but preceded by junk/padding datagrams that break statistical fingerprinting. |
salamander | UDP | Hysteria2’s Salamander XOR-obfuscated packets (password-keyed). |
sudoku | UDP | A password/table transform with random padding. |
mkcp | UDP | mKCP framing applied as a mask. |
Deep dive: fragment cuts the ClientHello no filter can read
On TCP the mask you’ll actually reach for is fragment, and its whole job is to make sure the
SNI a censor wants to match never arrives in one TCP segment. In its tlshello mode it waits
for the first write, checks that it is a TLS handshake record (p[0] == 22), then chops that
record’s payload into random-length chunks — each re-wrapped as its own valid TLS record — and
writes them out with an optional delay between each.
// fragmentConn.Write, tlshello mode (trimmed)
data := p[5:recordLen] // the ClientHello record payload
for from := 0; ; {
to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax))
if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) {
to = len(data)
}
copy(buff[:3], p) // reuse type(0x16) + TLS version
copy(buff[5:], data[from:to]) // this slice of the ClientHello
buff[3] = byte((to - from) >> 8) // …with its own record length
buff[4] = byte(to - from)
c.Conn.Write(buff[:5+(to-from)]) // one small, valid TLS record
time.Sleep(RandBetween(DelayMin, DelayMax) * time.Millisecond)
if from == len(data) { break }
} The wire now carries several perfectly legal TLS records that the destination reassembles
transparently — but a DPI box doing cheap, stateless matching for server_name = example.com sees the hostname split across record and segment boundaries and has nothing to match. It sits below TLS, so it never sees your keys — it re-frames the encrypted envelope, not the
plaintext, which is why it composes with raw TLS, REALITY, VLESS, or Trojan alike.
Real world: the fragment preset that unblocks VLESS-over-TCP in Russia
When Russia’s TSPU throttles or resets your VLESS-REALITY handshake, one fragment mask on the
client is the whole fix — no server change, no new transport. Durev VPN ships exactly this preset
for the Russian segment. It isn’t a widely-known recipe so much as a measure of how precisely
finalmask can be dialed in to a single network’s DPI: three small ranges, tuned to the TSPU, and a
blocked handshake goes through.
{
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": { "serverName": "example.com", "fingerprint": "chrome" },
"tcp": [
{
"type": "fragment",
"settings": { "packets": "tlshello", "length": "100-200", "delay": "10-20" }
}
]
}
} packets: tlshello fragments only the outgoing ClientHello, length: 100-200 cuts it into
100–200-byte TLS records, and delay: 10-20 spaces them 10–20 ms apart. Because masks are a client-side transform and the destination reassembles standard TLS, the server needs nothing
added. Every value is a range, rolled fresh with crypto.RandBetween so no two connections
fragment identically.
| Setting | Proto fields | What it controls |
|---|---|---|
packets | packets_from / packets_to | Which writes to cut. tlshello = 0..1 (only the first, and only if it’s a TLS record). A range like 1-2 fragments the 1st–2nd writes regardless of content. |
length | length_min / length_max | Payload bytes per fragment. Smaller = more records = harder to match, slower. |
delay | delay_min / delay_max | Milliseconds between fragments — defeats time-windowed reassembly, at the cost of handshake latency. |
maxSplit | max_split_min / max_split_max | Caps the number of pieces (0 = unlimited); the last piece takes the rest. |
Fragment every packet, tiny and slow
packets: "1-999", length: "1-10", delay: "50-100" shreds the whole stream into single-digit
records with long sleeps. It hides the SNI, but you gutted throughput and minted a distinctive
“everything is tiny and paced” signature of its own.
Fragment only the handshake
packets: "tlshello" touches just the first record where the SNI lives; the rest of the
connection splices at full speed. You defeat the SNI filter and stay fast.
UDP masks stack sizes; TCP masks stack conns
The two managers differ. TCP masks (fragment) simply wrap net.Conn in order. UDP masks that
add a fixed-size prefix are collected into a headerManagerConn that tracks each mask’s byte size,
so several packet-prefix disguises can be layered and peeled in exactly the right order on read —
you can be a WireGuard-looking, noise-padded, Salamander-obfuscated flow all at once. Reach for fragment first on TCP; on a UDP transport, pick the header disguise your network is least willing
to touch.