Contents

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.

transport/internet/finalmask/finalmask.go transport/internet/config.proto

A mask is the last thing to touch your bytes

finalmask is two ordered lists on streamSettingstcp 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.

transport/internet/finalmask/finalmask.go transport/internet/memory_settings.go
// 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
}
finalmask masks wrap the connection in order; the server applies the same list in reverse

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.

MaskConnYour traffic becomes…
fragmentTCPA TLS handshake sliced into many small records — the SNI a filter wants to match never arrives whole.
header/dtlsUDPDTLS records — looks like a WebRTC/secure-datagram session.
header/srtpUDPSRTP media — looks like a live audio/video call.
header/utpUDPµTP — looks like BitTorrent traffic.
header/wechatUDPWeChat video packets.
header/wireguardUDPWireGuard datagrams.
header/dnsUDPDNS-shaped datagrams.
header/customUDPAnything 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.
xdnsUDPFull DNS queries to a chosen domain — a DNS tunnel.
xicmpUDPICMP packets to an IP/id — a ping tunnel.
noiseUDPYour real packets, but preceded by junk/padding datagrams that break statistical fingerprinting.
salamanderUDPHysteria2’s Salamander XOR-obfuscated packets (password-keyed).
sudokuUDPA password/table transform with random padding.
mkcpUDPmKCP framing applied as a mask.
transport/internet/finalmask/header transport/internet/finalmask/header/custom/config.proto

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.

transport/internet/finalmask/fragment/conn.go transport/internet/finalmask/fragment/config.proto
// 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.

tlshello fragmentation: the destination reassembles the ClientHello; the DPI box never sees a whole SNI

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.

SettingProto fieldsWhat it controls
packetspackets_from / packets_toWhich 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.
lengthlength_min / length_maxPayload bytes per fragment. Smaller = more records = harder to match, slower.
delaydelay_min / delay_maxMilliseconds between fragments — defeats time-windowed reassembly, at the cost of handshake latency.
maxSplitmax_split_min / max_split_maxCaps the number of pieces (0 = unlimited); the last piece takes the rest.
Don't

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.

Do

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.