Skip to main content

rovs_openflow/
packet_in.rs

1//! OpenFlow Packet-In message parsing.
2//!
3//! Packet-In messages are sent from the switch to the controller when a packet
4//! matches a flow with `actions=CONTROLLER` or when there's a table miss.
5
6use bytes::{Buf, Bytes};
7
8use crate::match_fields::Match;
9use crate::{Error, Result};
10
11/// Buffer ID indicating no buffering (packet data is in the message).
12pub const OFP_NO_BUFFER: u32 = 0xffff_ffff;
13
14/// Reason codes for Packet-In messages.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[repr(u8)]
17pub enum PacketInReason {
18    /// No matching flow (table-miss).
19    NoMatch = 0,
20    /// Action explicitly output to controller.
21    Action = 1,
22    /// Packet has invalid TTL.
23    InvalidTtl = 2,
24    /// Action set explicitly output to controller (OF 1.4+).
25    ActionSet = 3,
26    /// Group bucket explicitly output to controller (OF 1.4+).
27    Group = 4,
28    /// Packet sent for controller processing (OF 1.4+).
29    PacketOut = 5,
30}
31
32impl TryFrom<u8> for PacketInReason {
33    type Error = Error;
34
35    fn try_from(v: u8) -> Result<Self> {
36        match v {
37            0 => Ok(Self::NoMatch),
38            1 => Ok(Self::Action),
39            2 => Ok(Self::InvalidTtl),
40            3 => Ok(Self::ActionSet),
41            4 => Ok(Self::Group),
42            5 => Ok(Self::PacketOut),
43            _ => Err(Error::Parse(format!("unknown packet-in reason: {v}"))),
44        }
45    }
46}
47
48/// Parsed Packet-In message.
49#[derive(Debug, Clone)]
50pub struct PacketIn {
51    /// Buffer ID assigned by the switch, or `OFP_NO_BUFFER` if not buffered.
52    pub buffer_id: u32,
53    /// Total length of the packet (before truncation).
54    pub total_len: u16,
55    /// Reason the packet was sent to the controller.
56    pub reason: PacketInReason,
57    /// ID of the table that generated the Packet-In.
58    pub table_id: u8,
59    /// Cookie of the flow entry that caused the Packet-In.
60    pub cookie: u64,
61    /// Match fields from the packet.
62    pub match_fields: Match,
63    /// Packet data (may be truncated based on controller max_len).
64    pub data: Vec<u8>,
65}
66
67impl PacketIn {
68    /// Parse a Packet-In message from raw bytes (after the OpenFlow header).
69    pub fn parse(mut buf: Bytes) -> Result<Self> {
70        // Minimum size: buffer_id(4) + total_len(2) + reason(1) + table_id(1)
71        //              + cookie(8) + match_type(2) + match_len(2) = 20 bytes
72        if buf.remaining() < 20 {
73            return Err(Error::Parse("packet-in too short".into()));
74        }
75
76        let buffer_id = buf.get_u32();
77        let total_len = buf.get_u16();
78        let reason = PacketInReason::try_from(buf.get_u8())?;
79        let table_id = buf.get_u8();
80        let cookie = buf.get_u64();
81
82        // Parse match (OXM format)
83        // Match header: type(2) + length(2)
84        let match_type = buf.get_u16();
85        let match_len = buf.get_u16();
86
87        if match_type != 1 {
88            // OFPMT_OXM = 1
89            return Err(Error::Parse(format!(
90                "unsupported match type: {match_type}",
91            )));
92        }
93
94        // match_len includes the 4-byte header, OXM fields follow
95        let oxm_len = match_len.saturating_sub(4) as usize;
96        if buf.remaining() < oxm_len {
97            return Err(Error::Parse("packet-in match truncated".into()));
98        }
99
100        let oxm_bytes = buf.copy_to_bytes(oxm_len);
101        let match_fields = Match::decode_oxm(&oxm_bytes)?;
102
103        // Skip padding to 8-byte alignment
104        // Total match size with header is match_len, padded to 8 bytes
105        let padded_match_len = (match_len as usize + 7) & !7;
106        let padding = padded_match_len - match_len as usize;
107        if buf.remaining() < padding {
108            return Err(Error::Parse("packet-in padding missing".into()));
109        }
110        buf.advance(padding);
111
112        // Skip 2 bytes of padding before data
113        if buf.remaining() < 2 {
114            return Err(Error::Parse("packet-in data padding missing".into()));
115        }
116        buf.advance(2);
117
118        // Remaining bytes are packet data
119        let data = buf.to_vec();
120
121        Ok(Self {
122            buffer_id,
123            total_len,
124            reason,
125            table_id,
126            cookie,
127            match_fields,
128            data,
129        })
130    }
131
132    /// Check if the packet is buffered on the switch.
133    pub fn is_buffered(&self) -> bool {
134        self.buffer_id != OFP_NO_BUFFER
135    }
136
137    /// Get the input port from the match fields.
138    pub fn in_port(&self) -> u32 {
139        self.match_fields.in_port.unwrap_or(0)
140    }
141
142    /// Get the buffer ID, or None if not buffered.
143    pub fn buffer_id(&self) -> Option<u32> {
144        if self.buffer_id == OFP_NO_BUFFER {
145            None
146        } else {
147            Some(self.buffer_id)
148        }
149    }
150
151    /// Get the packet data.
152    pub fn data(&self) -> &[u8] {
153        &self.data
154    }
155
156    /// Create a PacketIn for testing.
157    #[cfg(any(test, feature = "test-support"))]
158    pub fn new_for_test(buffer_id: u32, in_port: u32, data: Vec<u8>) -> Self {
159        Self {
160            buffer_id,
161            total_len: data.len() as u16,
162            reason: PacketInReason::Action,
163            table_id: 0,
164            cookie: 0,
165            match_fields: Match::new().in_port(in_port),
166            data,
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn parse_packet_in_reason() {
177        assert_eq!(PacketInReason::try_from(0).unwrap(), PacketInReason::NoMatch);
178        assert_eq!(PacketInReason::try_from(1).unwrap(), PacketInReason::Action);
179        assert!(PacketInReason::try_from(99).is_err());
180    }
181}