Running a server
Anti-xray
Anti-xray keeps x-ray clients and texture packs from finding ores and buried loot. A block on a world's hidden list that no player could see, because every side of it is covered, is sent to players as the ordinary rock around it. The moment one of its sides is opened, it is sent as itself.
The world on disk is never changed, and plugins always read the real blocks.
Turning it on
Anti-xray is set per world in rustmc.yml and is off in every world by default. To turn it on in the overworld:
worlds:
minecraft:overworld:
anti-xray:
enabled: true
hidden-blocks: null
Each world (minecraft:overworld, minecraft:the_nether, minecraft:the_end) has its own
anti-xray section, and turning it on in one leaves the others off. The settings are read at
startup, so a change takes a restart.
Settings
| Key | Default | What it does |
|---|---|---|
worlds.<world>.anti-xray.enabled | false | Hides the listed blocks in this world when nothing can see them. |
worlds.<world>.anti-xray.hidden-blocks | null | What is hidden, as block names separated by commas. null is the default list below. |
hidden-blocks takes names with or without the minecraft: prefix, so
diamond_ore, deepslate_diamond_ore is the same as
minecraft:diamond_ore, minecraft:deepslate_diamond_ore. A name that is not a block, or a list
that names no blocks at all, stops the server at startup with an error saying which. Every state
of a listed block is hidden: a lit redstone ore as well as an unlit one, a chest whichever way it
faces.
The default list (null) is every ore, ancient debris, chests, barrels and spawners:
- coal, iron, copper, gold, redstone, lapis, diamond and emerald ore, each in stone and deepslate
- nether gold ore and nether quartz ore
- ancient debris
- chest, trapped chest, barrel and spawner
Containers and spawners are on it because a buried one is worth as much to an x-ray as an ore.
How it behaves
The rule. A hidden block is sent as rock when all six of its neighbours cover it. A neighbour covers it when it is a full, opaque block, or when it is itself on the hidden list, so two buried chests side by side do not give each other away. Air, water, glass, slabs, leaves and anything else that is not a full opaque block leave a face open, and so does the top or bottom of the world.
Which rock. The masked block is sent as the first ordinary full block beside it, so an ore in granite looks like granite rather than a speck of stone. When nothing beside it will do (it is surrounded by other hidden blocks, or by blocks such as furnaces that hold contents), it is sent as stone in the overworld, deepslate below y 0, netherrack in the Nether and end stone in the End.
Revealing. Whenever a block changes in a way that opens or closes a face, from mining, an
explosion, a piston, flowing water, /fill, a placed structure or a plugin, its hidden neighbours
are checked again and sent to nearby players as they really are with that tick's block updates.
Everything that leaves the server agrees. Chunk packets, block updates and anything else that names a block at a position follow the same rule:
- Chunk packets are masked before they are sent. One masked packet is built per chunk and shared by every player, and plugin packet hooks and Bedrock players see that same masked packet.
- A container's or spawner's contents are left out of the chunk while it is masked, and sent when it is revealed.
- Replies about a position a client named (a refused break or place, the debug stick, pick block) answer with what the client sees, so a client cannot ask what is behind a mask. Pick block in creative does not copy a masked block's contents.
- Level events (such as a spawner's flames), block events (such as a chest lid opening) and block-breaking cracks at a masked block are not sent.
Pregenerated worlds. A world with anti-xray on builds its own chunk packets instead of serving
the prepared ones that pregenerate writes. The prepared files are still written, unmasked, and a
world with anti-xray off still uses them.
Limits
- There is no bypass for staff. Every player in a world shares one packet, so operators and spectators see rock where the ores are too.
- A player can still break a block they cannot see, within reach, and learn from what drops.
- A modified client can open a masked container within reach: the server does not check line of sight before opening one, and neither does the game.
- An explosion's drops show where an ore was, as they would in the game.
- Where a neighbouring chunk is not available when a chunk is sent, blocks along that edge stay masked for that send. That packet is not kept, so the next send decides again.
- Which rock a masked block is drawn as can lag behind the world: if a neighbour is swapped for a different solid block, the masked block may show the old rock until its chunk is next sent. Whether it is masked never lags.
- Light is not masked. An enclosed space normally holds no light, with two exceptions: a hidden block that is not a full cube next to an exposed one (a buried chest beside an open chest), where light reaches it, and a lit redstone ore. A client reading the light level at such a spot sees a value rock would not have.
- A particle a plugin chooses to play at a masked block is still sent.
- The MCP endpoint and plugins read the real blocks, not the masked ones.
Cost
Measured on one thread over 441 chunks of the same seed, taking turns with anti-xray off and on:
| Off | On | |
|---|---|---|
| Building a chunk, mean | 1.531 ms | 1.976 ms |
| Median | 1.551 ms | 2.015 ms |
| p99 | 2.123 ms | 2.717 ms |
| Packet size | 5534 bytes | 4912 bytes |
| Fresh view of 10 | 34.1 s, 441 chunks | 39.9 s, 524 chunks |
The machine was busy with other work, so compare the columns with each other rather than with your own hardware: building a chunk costs about 29% more with anti-xray on, and a quiet machine has given 1.07 and 1.37 ms, the same ratio. Packet sizes and chunk counts do not depend on the machine.
A chunk is built once and its packet shared by every player, so this cost is paid once per chunk, not once per player. With anti-xray off it costs next to nothing. A fresh view costs more because a chunk has to know what is across its edges, so the ring of chunks just outside a player's view is generated too.
Block changes, every block turned to air, three rounds:
| Off | On | |
|---|---|---|
| A million block changes | 1.15 to 1.27 s | 1.61 to 2.02 s |
| A sphere of radius 30, 113,081 changes | 96 to 148 ms | 103 to 156 ms |
| Sending that million, sampled 1 in 16 | 12 to 15 ms | 172 to 227 ms, 26,469 reveals |
Only a change that opens or closes a face costs anything extra. For the sphere the two rows overlap: at that size the difference is smaller than the variation between runs.
On a pregenerated world, a chunk that would have been served from its prepared file (about 15 microseconds) is built instead the first time it is sent.
Security and privacy
Anti-xray changes only what is sent to players. It stores nothing and writes nothing to the world.
From docs/anti-xray.md in the RustMC repository at bbd81209ad.