shadowsocks/shadowsocks-rust · error

Packet header length mismatch

Error message

Packet header length mismatch

What it means

Panic from `<&mut Block as TryFrom<&mut [u8]>>::try_from(packet_header).expect("Packet header length mismatch")` in UDP `encrypt_message`. The packet header carrying SessionID+PacketID must be exactly one AES block (16 bytes); the conversion fails when the buffer passed as `packet_header` is not exactly 16 bytes.

Source

Thrown at crates/shadowsocks/src/relay/udprelay/aead_2022.rs:229

            let cipher = get_cipher(method, key, session_id);

            // Encrypt the rest of the packet with AEAD cipher (AES-*-GCM)
            let (packet_header, mut message) = packet.split_at_mut(16);
            let nonce = &packet_header[4..16];

            if eih_len > 0 {
                message = &mut message[eih_len..];
            }

            cipher.encrypt_packet(nonce, message);

            // [SessionID + PacketID] is encrypted with AES-ECB with PSK
            // No padding is required because these 2 fields are 128-bits, which is exactly the same as AES's block size
            match method {
                CipherKind::AEAD2022_BLAKE3_AES_128_GCM => {
                    let cipher = Aes128::new_from_slice(ipsk).expect("AES-128 init");
                    let block = <&mut Block as TryFrom<&mut [u8]>>::try_from(packet_header)
                        .expect("Packet header length mismatch");
                    cipher.encrypt_block(block);
                }
                CipherKind::AEAD2022_BLAKE3_AES_256_GCM => {
                    let cipher = Aes256::new_from_slice(ipsk).expect("AES-256 init");
                    let block = <&mut Block as TryFrom<&mut [u8]>>::try_from(packet_header)
                        .expect("Packet header length mismatch");
                    cipher.encrypt_block(block);
                }
                _ => unreachable!("{} is not an AES-*-GCM cipher", method),
            }
        }
        _ => unreachable!("{} is not an AEAD 2022 cipher", method),
    }
}

fn decrypt_message(
    _context: &Context,
    method: CipherKind,

View on GitHub (pinned to 8eb0f0a65b)

Solutions

  1. Build the packet header as exactly 16 bytes: 8-byte session ID + 8-byte packet ID, and pass only that slice.
  2. Verify the split of the outgoing buffer: header = first 16 bytes, remainder is the AEAD-encrypted message.
  3. Check the serialization of SessionID/PacketID field widths against the shadowsocks-2022 spec.
  4. Add a length check `packet_header.len() == 16` with a clear error before the TryFrom.

Example fix

// before
let block = <&mut Block as TryFrom<&mut [u8]>>::try_from(packet_header)
    .expect("Packet header length mismatch");
// after
assert_eq!(packet_header.len(), 16, "UDP AEAD-2022 header (session_id + packet_id) must be 16 bytes");
let block = <&mut Block as TryFrom<&mut [u8]>>::try_from(packet_header)
    .expect("Packet header length mismatch");
Defensive patterns

Strategy: validation

Validate before calling

if packet_header.len() != 16 {
    return Err(format!("UDP AEAD-2022 header must be 16 bytes, got {}", packet_header.len()));
}

Type guard

fn is_valid_header(buf: &[u8]) -> bool { buf.len() == 16 }

Prevention

When it happens

Trigger: Calling `encrypt_message` (or `encrypt_client_payload_aead_2022` / `encrypt_server_payload_aead_2022`) with a `packet_header` slice that is not 16 bytes — e.g. a header built with wrong field sizes (SessionID or PacketID of unexpected width) or an offset/split error on the payload buffer.

Common situations: Hand-rolled packet builders using 4-byte or 8-byte session/packet ID fields instead of the AEAD-2022 sizes; passing the whole datagram instead of the 16-byte header prefix; protocol fuzzing/custom interop with a non-conformant peer format.

Related errors


AI-assisted analysis of shadowsocks/shadowsocks-rust@8eb0f0a65b (2026-09-09). Data as JSON: /api/errors/077291d423862185. Report an issue: GitHub.