-
Notifications
You must be signed in to change notification settings - Fork 4
Encryption and Compression
Both are on by default, both are negotiated during the handshake, and both are decided by one side rather than agreed between them.
void ConfigureSecurity() {
ServerConfig config{"0.0.0.0", 25000};
// read only on the accepting side. the server announces its choice during
// the handshake and the client adopts it, so a client cannot downgrade a
// server that requires encryption.
config.child_options.common.encryption = true;
// negotiated the same way. runs before encryption, so it compresses the
// plaintext and works on encrypted and unencrypted sessions alike.
config.child_options.common.compression = CompressionType::Zstandard;
// below this, compressing costs more than it saves: at 64 bytes zstd makes
// most traffic larger, and still pays for building the coder tables.
config.child_options.common.compression_threshold = 128;
}The accepting side. On a server that is child_options; setting
encryption in a client's ClientConfig::options has no effect, because the
server announces its choice during the handshake and the client adopts it.
The practical consequence: a client cannot downgrade a server that requires
encryption. A server with encryption = true will not serve an unencrypted
session, whatever the client asks for.
For a P2P pair the dialer marks exactly one peer as the initiator, so the other
one decides. p2p::IsInitiator tells you which you are.
A 2048-bit Diffie-Hellman exchange during the handshake, then AES-256-CBC on every message. Handled entirely inside the session — no keys to manage, no certificates to install, nothing to call.
Turn it off only on an already-trusted transport, or to measure what the crypto costs. On the benchmark machine it costs roughly 1.1x at 8 KiB payloads.
Confidentiality against a passive observer. Someone capturing traffic cannot read it.
That is the whole of it, and the two gaps below are worth understanding before you rely on this for anything that matters.
No peer authentication. The Diffie-Hellman exchange is unsigned: there are no certificates, no public-key pinning, and no identity check of any kind. It protects against someone listening, not against someone positioned between you, who can complete a separate exchange with each side and read everything.
No integrity protection. CBC mode is not authenticated and znet adds no MAC, so modified ciphertext is decrypted and delivered rather than rejected. An attacker who can alter bytes in flight can corrupt your messages undetectably at the transport layer. ZDT's checksums catch accidental corruption, not deliberate tampering.
Despite the README's wording, this is not TLS and should not be treated as equivalent to it.
For a game talking to your own server over the internet, this is usually acceptable: it defeats casual packet sniffing, which is the realistic threat.
If you need more:
- Authenticate at the application layer. Send a token in your first packet and check it before swapping in your real handler. This tells you who the peer is, though it does not fix the MITM gap on its own — an interceptor can relay the token.
- For anything genuinely sensitive — credentials, payment data, personal information — run znet inside a transport that authenticates, or do not send it over znet.
- Do not rely on message integrity. Validate what you deserialize. A packet that decrypts is not a packet you can trust.
zstd, applied to outgoing messages once the session is ready. It runs before encryption, so it compresses the plaintext, which is what makes it effective; compressing ciphertext would achieve nothing.
| Value | Effect |
|---|---|
CompressionType::Default |
Whatever the build supports. Never appears on the wire |
CompressionType::Zstandard |
zstd |
CompressionType::None |
Off |
Compression is compiled in whenever a zstd target is available; if none is found,
CMake reports zstd not found, compression disabled and Default resolves to
None. ZNET_USE_EXTERNAL_ZSTD selects which zstd, not whether to use one.
compression_threshold (128 bytes) exists because small messages cannot pay
back the frame header. At 64 bytes zstd makes essentially every kind of traffic
about 12% larger, and still costs a full pass to build the coder tables.
Measured break-even is near 96 bytes for text and 128 for binary game state, so
the default sits where compressing stops being actively harmful.
The compression type is recorded per message, so one session freely mixes compressed and uncompressed messages — there is no renegotiation and no cost to crossing the threshold in either direction. Setting it to zero compresses everything, which is almost always worse.
Outgoing:
your packet -> serialize -> compress (if over threshold) -> encrypt -> transport
Incoming reverses it. Compression sits inside encryption, so an observer sees neither your data nor its compressibility.
There is no "is encryption on" flag to read. If you need certainty, run one
session with encryption = false and compare the byte counters, or take a
capture:
SessionMetrics m = session->metrics();
m.common.message_bytes_sent; // after encode, before transport framing
m.common.wire_bytes_sent; // including transport framing