jkipsec 0.1.0

Userspace IKEv2/IPsec VPN responder for terminating iOS VPN tunnels and exposing the inner IP traffic. Pairs with jktcp for a fully userspace TCP/IP stack.
Documentation
//! A parsed IKEv2 message: header + payload chain (still in raw byte form).

#![allow(missing_docs)]

use super::header::{ExchangeType, Flags, HEADER_LEN, Header, IKE_VERSION};
use super::payload::{PAYLOAD_HEADER_LEN, PayloadKind, RawPayload, walk_chain};

#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("{what} truncated: need {need} bytes, got {got}")]
    Truncated {
        what: &'static str,
        need: usize,
        got: usize,
    },
    #[error("{what} length field is invalid: {value}")]
    BadLength { what: &'static str, value: usize },
    #[error("unsupported IKE version 0x{0:02x} (expected 0x20)")]
    BadVersion(u8),
    #[error("declared length {declared} does not match buffer length {actual}")]
    LengthMismatch { declared: usize, actual: usize },
}

#[derive(Debug)]
pub struct Message<'a> {
    pub header: Header,
    pub payloads: Vec<RawPayload<'a>>,
    /// The full datagram bytes, retained because IKEv2 AUTH and Encrypted
    /// payload integrity checks are computed over the original wire bytes.
    pub raw: &'a [u8],
}

impl<'a> Message<'a> {
    pub fn parse(bytes: &'a [u8]) -> Result<Self, ParseError> {
        let header = Header::parse(bytes)?;

        if header.version != IKE_VERSION {
            return Err(ParseError::BadVersion(header.version));
        }

        let declared = header.length as usize;
        if declared < HEADER_LEN {
            return Err(ParseError::BadLength {
                what: "ike header.length",
                value: declared,
            });
        }
        if declared != bytes.len() {
            return Err(ParseError::LengthMismatch {
                declared,
                actual: bytes.len(),
            });
        }

        let payloads = walk_chain(header.next_payload, &bytes[HEADER_LEN..])?;

        Ok(Self {
            header,
            payloads,
            raw: bytes,
        })
    }
}

/// One outgoing payload - its kind plus its body bytes (without the 4-byte
/// generic payload header, which the builder writes for you).
pub struct OutPayload {
    pub kind: PayloadKind,
    pub body: Vec<u8>,
}

/// Serialise an IKEv2 message: the 28-byte header followed by the payload
/// chain. Sets `next_payload` and per-payload `next` fields automatically and
/// fills in the total `length`.
pub fn build_message(
    initiator_spi: u64,
    responder_spi: u64,
    exchange_type: ExchangeType,
    flags: Flags,
    message_id: u32,
    payloads: &[OutPayload],
) -> Vec<u8> {
    let body_len: usize = payloads
        .iter()
        .map(|p| PAYLOAD_HEADER_LEN + p.body.len())
        .sum();
    let total = HEADER_LEN + body_len;

    let first_kind = payloads
        .first()
        .map(|p| p.kind)
        .unwrap_or(PayloadKind::None);

    let header = Header {
        initiator_spi,
        responder_spi,
        next_payload: first_kind,
        version: IKE_VERSION,
        exchange_type,
        flags,
        message_id,
        length: total as u32,
    };

    let mut out = Vec::with_capacity(total);
    let mut hdr_buf = [0u8; HEADER_LEN];
    header.write_into(&mut hdr_buf);
    out.extend_from_slice(&hdr_buf);

    for (i, p) in payloads.iter().enumerate() {
        let next = payloads
            .get(i + 1)
            .map(|n| n.kind)
            .unwrap_or(PayloadKind::None);
        let plen = (PAYLOAD_HEADER_LEN + p.body.len()) as u16;
        out.push(next.as_u8());
        out.push(0); // critical=0, reserved
        out.extend_from_slice(&plen.to_be_bytes());
        out.extend_from_slice(&p.body);
    }

    debug_assert_eq!(out.len(), total);
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ike::payload::PayloadKind;

    #[test]
    fn parse_minimal_init() {
        // Nonce payload: 4-byte generic header + 8-byte body = 12 bytes total.
        let mut payload = vec![0x00u8, 0x00, 0x00, 0x0c]; // next=None, reserved=0, len=12
        payload.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04]);

        let total_len = (HEADER_LEN + payload.len()) as u32;
        let mut hdr = [0u8; HEADER_LEN];
        // initiator SPI
        hdr[0..8].copy_from_slice(&0x1122_3344_5566_7788u64.to_be_bytes());
        // responder SPI = 0 (first message)
        hdr[16] = PayloadKind::Nonce.as_u8();
        hdr[17] = IKE_VERSION;
        hdr[18] = 34; // IKE_SA_INIT
        hdr[19] = 0x08; // INITIATOR
        hdr[20..24].copy_from_slice(&0u32.to_be_bytes());
        hdr[24..28].copy_from_slice(&total_len.to_be_bytes());

        let mut datagram = hdr.to_vec();
        datagram.extend_from_slice(&payload);

        let msg = Message::parse(&datagram).expect("parse");
        assert_eq!(msg.header.initiator_spi, 0x1122_3344_5566_7788);
        assert!(msg.header.flags.is_initiator());
        assert!(!msg.header.flags.is_response());
        assert_eq!(msg.payloads.len(), 1);
        assert_eq!(msg.payloads[0].kind, PayloadKind::Nonce);
        assert_eq!(msg.payloads[0].body.len(), 8);
    }

    #[test]
    fn rejects_truncated_header() {
        let short = [0u8; 10];
        assert!(matches!(
            Message::parse(&short),
            Err(ParseError::Truncated { .. })
        ));
    }

    /// Real IKE_SA_INIT request captured from iOS. Validates we walk the full
    /// payload chain (SA, KE, Nonce, four Notifys) without complaint.
    #[test]
    fn parse_real_ios_ike_sa_init() {
        let bytes = hex_literal();
        let msg = Message::parse(&bytes).expect("parse");
        assert_eq!(
            msg.header.exchange_type,
            super::super::ExchangeType::IkeSaInit
        );
        assert!(msg.header.flags.is_initiator());
        assert!(!msg.header.flags.is_response());
        assert_eq!(msg.header.message_id, 0);
        assert_eq!(msg.header.responder_spi, 0);
        // expected chain: SA, KE, Ni, then six Notify payloads (NAT_DETECTION
        // source/dest, FRAGMENTATION_SUPPORTED, SIGNATURE_HASH_ALGORITHMS, etc.)
        let kinds: Vec<_> = msg.payloads.iter().map(|p| p.kind).collect();
        assert_eq!(
            kinds,
            vec![
                PayloadKind::SecurityAssociation,
                PayloadKind::KeyExchange,
                PayloadKind::Nonce,
                PayloadKind::Notify,
                PayloadKind::Notify,
                PayloadKind::Notify,
                PayloadKind::Notify,
                PayloadKind::Notify,
                PayloadKind::Notify,
            ]
        );
    }

    fn hex_literal() -> Vec<u8> {
        const HEX: &str = "
            36 19 5E 76 EB 4B 9A 11 00 00 00 00 00 00 00 00
            21 20 22 08 00 00 00 00 00 00 02 3A 22 00 01 64
            02 00 00 2C 01 01 00 04 03 00 00 0C 01 00 00 14
            80 0E 01 00 03 00 00 08 02 00 00 05 03 00 00 08
            06 00 00 24 00 00 00 08 04 00 00 13 02 00 00 2C
            02 01 00 04 03 00 00 0C 01 00 00 14 80 0E 01 00
            03 00 00 08 02 00 00 05 03 00 00 08 06 00 00 24
            00 00 00 08 04 00 00 0E 02 00 00 34 03 01 00 05
            03 00 00 0C 01 00 00 0C 80 0E 01 00 03 00 00 08
            03 00 00 0C 03 00 00 08 02 00 00 05 03 00 00 08
            06 00 00 24 00 00 00 08 04 00 00 13 02 00 00 34
            04 01 00 05 03 00 00 0C 01 00 00 0C 80 0E 01 00
            03 00 00 08 03 00 00 0C 03 00 00 08 02 00 00 05
            03 00 00 08 06 00 00 24 00 00 00 08 04 00 00 0E
            02 00 00 24 05 01 00 03 03 00 00 0C 01 00 00 14
            80 0E 01 00 03 00 00 08 02 00 00 05 00 00 00 08
            04 00 00 13 02 00 00 24 06 01 00 03 03 00 00 0C
            01 00 00 14 80 0E 01 00 03 00 00 08 02 00 00 05
            00 00 00 08 04 00 00 0E 02 00 00 2C 07 01 00 04
            03 00 00 0C 01 00 00 0C 80 0E 01 00 03 00 00 08
            03 00 00 0C 03 00 00 08 02 00 00 05 00 00 00 08
            04 00 00 13 00 00 00 2C 08 01 00 04 03 00 00 0C
            01 00 00 0C 80 0E 01 00 03 00 00 08 03 00 00 0C
            03 00 00 08 02 00 00 05 00 00 00 08 04 00 00 0E
            28 00 00 48 00 13 00 00 76 4C 55 51 F0 73 E8 8E
            AE 62 8C 82 10 32 D3 DB 10 7A 24 27 9C B4 A0 F0
            A0 31 C3 FF 8E D5 10 7A 01 43 CC 3F 51 0A 66 4A
            15 7D EF 81 D0 55 FD 58 60 BC 71 9A C7 FA 2C 13
            EB 8A DD CA 3E 71 53 2D 29 00 00 14 08 CE DD 13
            DE 02 33 7B 8A 72 3F 21 7B 07 2C C2 29 00 00 08
            00 00 40 16 29 00 00 1C 00 00 40 04 FE 75 8E 84
            ED 11 8A 37 8D FB F9 05 0B CB 40 D8 9C 88 24 25
            29 00 00 1C 00 00 40 05 D2 DC 28 01 6E 4C 2F 7B
            2B 61 97 BD 71 00 13 35 8C 6C 4D B1 29 00 00 08
            00 00 40 2E 29 00 00 0E 00 00 40 2F 00 04 00 03
            00 02 00 00 00 08 00 00 40 36
        ";
        HEX.split_ascii_whitespace()
            .map(|h| u8::from_str_radix(h, 16).unwrap())
            .collect()
    }

    #[test]
    fn rejects_length_mismatch() {
        let mut hdr = [0u8; HEADER_LEN];
        hdr[16] = 0; // no payloads
        hdr[17] = IKE_VERSION;
        hdr[18] = 34;
        hdr[24..28].copy_from_slice(&100u32.to_be_bytes()); // claim 100 bytes
        assert!(matches!(
            Message::parse(&hdr),
            Err(ParseError::LengthMismatch { .. })
        ));
    }
}