Protocol is trivial. Both peers has shared 256-bit key. SHA3 is used to derive four more keys from it: SHAKE128("go.cypherpunks.ru/udpobfs" || key) -> 256-bit InitiatorEncryptionKey || 256-bit InitiatorObfuscationKey || 256-bit ResponderEncryptionKey || 256-bit ResponderObfuscationKey Each side has 64-bit packet number counter, that is used as a nonce. That counter is kept in memory and only its lower 24 bits are sent. When remote side receives 24-bit counter with lower value, then it increments in-memory counter's remaining part. Completely the same as Extended Sequence Numbers are done in IPsec's ESP. ChaCha20 is initialised with corresponding EncryptionKey and nonce equal to the full sequence number value. Its first 256-bit of output will be Poly1305's one-time key. Next 256-bits are ignored. Further ones are XORed with the plaintext (UDP's payload). Poly1305 is calculated over the full 64-bit sequence number value and the whole ciphertext. Higher 40-bits of the resulting tag with lower 24-bits of the sequence number are prepended to the ciphertext, encrypted with Blowfish: Blowfish(Seq || MAC) || Ciphertext Blowfish is initialized with ObfuscationKey. 40-bit MAC is rather weak, but enough for obfuscation purposes. 24-bit part of sequence number is enough for medium data-rate transmission where reordering or packets drops may occur.