Skip to main content

rovs_openflow/action/
nicira.rs

1//! Nicira extension actions for OpenFlow.
2//!
3//! This module contains Nicira (now part of VMware/OVS) vendor extensions
4//! including learn, resubmit, connection tracking, move, and reg_load actions.
5
6use super::types::{NxActionSubtype, NICIRA_VENDOR_ID, ActionType};
7
8/// Learn action flags.
9#[allow(dead_code)]
10pub mod learn_flags {
11    /// Send flow removed message when learned flow expires
12    pub const SEND_FLOW_REM: u16 = 1 << 0;
13    /// Delete matching flows instead of adding
14    pub const DELETE_LEARNED: u16 = 1 << 1;
15    /// Write result to the action set (vs apply immediately)
16    pub const WRITE_RESULT: u16 = 1 << 2;
17}
18
19/// NxLearn action (Nicira extension).
20///
21/// The learn action creates flows dynamically based on packet content.
22/// This is commonly used for MAC learning in OVS.
23#[derive(Debug, Clone, Default)]
24pub struct NxLearn {
25    /// Idle timeout for learned flows (0 = no timeout)
26    pub idle_timeout: u16,
27    /// Hard timeout for learned flows (0 = no timeout)
28    pub hard_timeout: u16,
29    /// Priority of learned flows
30    pub priority: u16,
31    /// Cookie for learned flows
32    pub cookie: u64,
33    /// Learn flags
34    pub flags: u16,
35    /// Table to install learned flows
36    pub table_id: u8,
37    /// Idle timeout when FIN received
38    pub fin_idle_timeout: u16,
39    /// Hard timeout when FIN received
40    pub fin_hard_timeout: u16,
41    /// Flow modification specs (match and action specifications)
42    pub specs: Vec<LearnSpec>,
43}
44
45/// A single learn specification.
46///
47/// Learn specs define how to construct match fields and actions
48/// in the learned flow.
49#[derive(Debug, Clone)]
50pub enum LearnSpec {
51    /// Match: copy field from packet to match field
52    MatchField {
53        /// Source field
54        src_field: u32,
55        /// Destination field (in learned flow's match)
56        dst_field: u32,
57        /// Number of bits
58        n_bits: u16,
59    },
60    /// Match: use immediate value
61    MatchImmediate {
62        /// Destination field
63        dst_field: u32,
64        /// Value to match
65        value: Vec<u8>,
66        /// Number of bits
67        n_bits: u16,
68    },
69    /// Action: copy field from packet to action's field
70    LoadField {
71        /// Source field
72        src_field: u32,
73        /// Destination field (in learned flow's actions)
74        dst_field: u32,
75        /// Number of bits
76        n_bits: u16,
77    },
78    /// Action: load immediate value
79    LoadImmediate {
80        /// Destination field
81        dst_field: u32,
82        /// Value to load
83        value: Vec<u8>,
84        /// Number of bits
85        n_bits: u16,
86    },
87    /// Output to port from field
88    OutputField {
89        /// Source field containing port number
90        src_field: u32,
91        /// Number of bits
92        n_bits: u16,
93    },
94}
95
96impl NxLearn {
97    /// Create a new learn action with defaults.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Set idle timeout for learned flows.
103    pub fn idle_timeout(mut self, timeout: u16) -> Self {
104        self.idle_timeout = timeout;
105        self
106    }
107
108    /// Set hard timeout for learned flows.
109    pub fn hard_timeout(mut self, timeout: u16) -> Self {
110        self.hard_timeout = timeout;
111        self
112    }
113
114    /// Set priority for learned flows.
115    pub fn priority(mut self, priority: u16) -> Self {
116        self.priority = priority;
117        self
118    }
119
120    /// Set cookie for learned flows.
121    pub fn cookie(mut self, cookie: u64) -> Self {
122        self.cookie = cookie;
123        self
124    }
125
126    /// Set table for learned flows.
127    pub fn table(mut self, table_id: u8) -> Self {
128        self.table_id = table_id;
129        self
130    }
131
132    /// Set flags.
133    pub fn flags(mut self, flags: u16) -> Self {
134        self.flags = flags;
135        self
136    }
137
138    /// Add a spec to match a field from the packet.
139    pub fn match_field(mut self, src_field: u32, dst_field: u32, n_bits: u16) -> Self {
140        self.specs.push(LearnSpec::MatchField { src_field, dst_field, n_bits });
141        self
142    }
143
144    /// Add a spec to match an immediate value.
145    pub fn match_immediate(mut self, dst_field: u32, value: Vec<u8>, n_bits: u16) -> Self {
146        self.specs.push(LearnSpec::MatchImmediate { dst_field, value, n_bits });
147        self
148    }
149
150    /// Add a spec to load a field from packet into action.
151    pub fn load_field(mut self, src_field: u32, dst_field: u32, n_bits: u16) -> Self {
152        self.specs.push(LearnSpec::LoadField { src_field, dst_field, n_bits });
153        self
154    }
155
156    /// Add a spec to load an immediate value into action.
157    pub fn load_immediate(mut self, dst_field: u32, value: Vec<u8>, n_bits: u16) -> Self {
158        self.specs.push(LearnSpec::LoadImmediate { dst_field, value, n_bits });
159        self
160    }
161
162    /// Add a spec to output to port from field.
163    pub fn output_field(mut self, src_field: u32, n_bits: u16) -> Self {
164        self.specs.push(LearnSpec::OutputField { src_field, n_bits });
165        self
166    }
167}
168
169// ============================================================================
170// Nicira Action Encoding
171// ============================================================================
172
173/// Encode Nicira action header.
174pub(crate) fn encode_nx_header(subtype: NxActionSubtype, len: u16) -> Vec<u8> {
175    let mut buf = Vec::with_capacity(16);
176    buf.extend((ActionType::Experimenter as u16).to_be_bytes());
177    buf.extend(len.to_be_bytes());
178    buf.extend(NICIRA_VENDOR_ID.to_be_bytes());
179    buf.extend((subtype as u16).to_be_bytes());
180    buf
181}
182
183/// Encode SetTunnelId as Nicira reg_load2 action (24 bytes).
184pub(crate) fn encode_set_tunnel_id(tun_id: u64) -> Vec<u8> {
185    // Use NXM reg_load2 (subtype 33) for setting tunnel ID
186    // Format: NX header (10) + OXM header (4) + value (8) + pad (2) = 24 bytes
187    let mut buf = encode_nx_header(NxActionSubtype::RegLoad2, 24);
188
189    // OXM header for tun_id: NXM_NX_TUN_ID (class=1, field=16, len=8)
190    let oxm_header: u32 = (1 << 16) | (16 << 9) | 8;
191    buf.extend(oxm_header.to_be_bytes());
192    buf.extend(tun_id.to_be_bytes());
193    buf.extend([0u8; 2]); // padding to 24 bytes
194    buf
195}
196
197/// Encode NxResubmit action (16 bytes for extended resubmit).
198pub(crate) fn encode_nx_resubmit(in_port: Option<u16>, table: Option<u8>) -> Vec<u8> {
199    // Use extended resubmit (subtype 14) which supports table
200    let mut buf = encode_nx_header(NxActionSubtype::ResubmitTable, 16);
201    buf.extend(in_port.unwrap_or(0xfff8).to_be_bytes()); // OFPP_IN_PORT = 0xfff8 (16-bit)
202    buf.push(table.unwrap_or(255)); // 255 = current table
203    buf.extend([0u8; 3]); // padding
204    buf
205}
206
207/// Encode NxCt (connection tracking) action.
208pub(crate) fn encode_nx_ct(flags: u16, zone: u16, table: Option<u8>) -> Vec<u8> {
209    // CT action format (24 bytes minimum):
210    // NX header (10) + flags (2) + zone_src (4) + zone (2) + recirc_table (1) + pad (3) + alg (2)
211    let mut buf = encode_nx_header(NxActionSubtype::Ct, 24);
212    buf.extend(flags.to_be_bytes());
213    buf.extend(0u32.to_be_bytes()); // zone_src (0 = use zone_imm field)
214    buf.extend(zone.to_be_bytes()); // zone_imm
215    buf.push(table.unwrap_or(255)); // recirc_table (255 = no recirculation)
216    buf.extend([0u8; 3]); // pad (3 bytes, not 1)
217    buf.extend(0u16.to_be_bytes()); // alg (0 = no ALG)
218    // No nested actions for now
219    buf
220}
221
222/// Encode NxCt with NAT (connection tracking with NAT) action.
223pub(crate) fn encode_nx_ct_nat(
224    flags: u16,
225    zone: u16,
226    table: Option<u8>,
227    nat: &super::NatConfig,
228) -> Vec<u8> {
229    // First encode the nested NAT action
230    let nat_action = encode_nx_nat(nat);
231
232    // CT action format with nested actions:
233    // NX header (10) + flags (2) + zone_src (4) + zone (2) + recirc_table (1) + pad (1) + alg (2) + nested_actions
234    // Note: The length includes the nested actions
235    let ct_header_len = 24; // Base CT action size
236    let total_len = ct_header_len + nat_action.len();
237    // Round up to 8-byte boundary
238    let padded_len = (total_len + 7) & !7;
239
240    let mut buf = Vec::with_capacity(padded_len);
241
242    // Action header
243    buf.extend((ActionType::Experimenter as u16).to_be_bytes());
244    buf.extend((padded_len as u16).to_be_bytes());
245    buf.extend(NICIRA_VENDOR_ID.to_be_bytes());
246    buf.extend((NxActionSubtype::Ct as u16).to_be_bytes());
247
248    // CT fields
249    buf.extend(flags.to_be_bytes());
250    buf.extend(0u32.to_be_bytes()); // zone_src (0 = use zone_imm field)
251    buf.extend(zone.to_be_bytes()); // zone_imm
252    buf.push(table.unwrap_or(255)); // recirc_table (255 = no recirculation)
253    buf.extend([0u8; 3]); // pad
254    buf.extend(0u16.to_be_bytes()); // alg (0 = no ALG)
255
256    // Nested NAT action
257    buf.extend(nat_action);
258
259    // Pad to 8-byte boundary
260    buf.resize(padded_len, 0);
261    buf
262}
263
264/// Encode NxNat action (used as nested action in ct).
265///
266/// NAT action format:
267/// NX header (10) + pad (2) + flags (2) + range_present (2) + [optional fields]
268fn encode_nx_nat(nat: &super::NatConfig) -> Vec<u8> {
269    let range_present = nat.range_present();
270
271    // Calculate the size of optional fields
272    let mut optional_len = 0;
273    if nat.ipv4_min.is_some() {
274        optional_len += 4;
275    }
276    if nat.ipv4_max.is_some() {
277        optional_len += 4;
278    }
279    if nat.ipv6_min.is_some() {
280        optional_len += 16;
281    }
282    if nat.ipv6_max.is_some() {
283        optional_len += 16;
284    }
285    if nat.port_min.is_some() {
286        optional_len += 2;
287    }
288    if nat.port_max.is_some() {
289        optional_len += 2;
290    }
291
292    // NAT header: 10 (NX header) + 2 (pad) + 2 (flags) + 2 (range_present) = 16
293    let header_len = 16;
294    let total_len = header_len + optional_len;
295    // Round up to 8-byte boundary
296    let padded_len = (total_len + 7) & !7;
297
298    let mut buf = encode_nx_header(NxActionSubtype::Nat, padded_len as u16);
299    buf.extend([0u8; 2]); // pad
300    buf.extend(nat.flags.to_be_bytes());
301    buf.extend(range_present.to_be_bytes());
302
303    // Optional fields in order: ipv4_min, ipv4_max, ipv6_min, ipv6_max, port_min, port_max
304    if let Some(addr) = nat.ipv4_min {
305        buf.extend(addr.octets());
306    }
307    if let Some(addr) = nat.ipv4_max {
308        buf.extend(addr.octets());
309    }
310    if let Some(addr) = nat.ipv6_min {
311        buf.extend(addr.octets());
312    }
313    if let Some(addr) = nat.ipv6_max {
314        buf.extend(addr.octets());
315    }
316    if let Some(port) = nat.port_min {
317        buf.extend(port.to_be_bytes());
318    }
319    if let Some(port) = nat.port_max {
320        buf.extend(port.to_be_bytes());
321    }
322
323    // Pad to 8-byte boundary
324    buf.resize(padded_len, 0);
325    buf
326}
327
328/// Encode NxRegLoad action for loading immediate value into register.
329///
330/// Format: `load:value->NXM_NX_REGn[start..end]`
331#[allow(dead_code)]
332pub fn encode_nx_reg_load(reg_num: u8, value: u32, start_bit: u8, n_bits: u8) -> Vec<u8> {
333    // reg_load uses subtype 7
334    // Format: NX header (10) + ofs_nbits (2) + dst (4) + value (8)
335    let mut buf = encode_nx_header(NxActionSubtype::RegLoad, 24);
336
337    // ofs_nbits: (start_bit << 6) | (n_bits - 1)
338    let ofs_nbits = ((start_bit as u16) << 6) | ((n_bits - 1) as u16);
339    buf.extend(ofs_nbits.to_be_bytes());
340
341    // dst: NXM header for register (class=1, field=reg_num, len=4)
342    let dst_header: u32 = (1 << 16) | ((reg_num as u32) << 9) | 4;
343    buf.extend(dst_header.to_be_bytes());
344
345    // value: 64-bit value (upper bits zero)
346    buf.extend((value as u64).to_be_bytes());
347    buf
348}
349
350/// Encode NxRegLoad action with NXM header for loading immediate value into any field.
351///
352/// This is the more general form that accepts any NXM field header.
353/// Format: `load:value->NXM_field[ofs..ofs+n_bits]`
354pub(crate) fn encode_nx_reg_load_nxm(dst_field: u32, dst_ofs: u16, n_bits: u16, value: u64) -> Vec<u8> {
355    // reg_load uses subtype 7
356    // Format: NX header (10) + ofs_nbits (2) + dst (4) + value (8)
357    let mut buf = encode_nx_header(NxActionSubtype::RegLoad, 24);
358
359    // ofs_nbits: (offset << 6) | (n_bits - 1)
360    let ofs_nbits = (dst_ofs << 6) | (n_bits - 1);
361    buf.extend(ofs_nbits.to_be_bytes());
362
363    // dst: NXM header for destination field
364    buf.extend(dst_field.to_be_bytes());
365
366    // value: 64-bit value
367    buf.extend(value.to_be_bytes());
368    buf
369}
370
371/// Encode NxMove action for copying bits between fields.
372///
373/// Format: `move:src[start..end]->dst[start..end]`
374pub(crate) fn encode_nx_move(
375    src_field: u32,
376    dst_field: u32,
377    n_bits: u16,
378    src_ofs: u16,
379    dst_ofs: u16,
380) -> Vec<u8> {
381    // move uses subtype 6
382    // Format: NX header (10) + n_bits (2) + src_ofs (2) + dst_ofs (2) + src (4) + dst (4)
383    let mut buf = encode_nx_header(NxActionSubtype::Move, 24);
384    buf.extend(n_bits.to_be_bytes());
385    buf.extend(src_ofs.to_be_bytes());
386    buf.extend(dst_ofs.to_be_bytes());
387    buf.extend(src_field.to_be_bytes());
388    buf.extend(dst_field.to_be_bytes());
389    buf
390}
391
392/// Encode NxLearn action for creating flows dynamically.
393///
394/// Wire format (variable length):
395/// ```text
396/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
397/// |         type (0xffff)       |         length                  |
398/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
399/// |                      vendor (0x00002320)                      |
400/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
401/// |       subtype (16)          |         idle_timeout            |
402/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
403/// |       hard_timeout          |          priority               |
404/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
405/// |                            cookie                             |
406/// |                                                               |
407/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
408/// |           flags             |  table_id   |       pad         |
409/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
410/// |      fin_idle_timeout       |       fin_hard_timeout          |
411/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
412/// |                   flow_mod_specs (variable)                   |
413/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
414/// ```
415pub(crate) fn encode_nx_learn(learn: &NxLearn) -> Vec<u8> {
416    // Calculate specs size
417    let specs_bytes = encode_learn_specs(&learn.specs);
418
419    // Total length: NX header (10) + fields (22) + specs + padding
420    let header_and_fields = 32; // 10 (header) + 22 (fixed fields)
421    let total_len = header_and_fields + specs_bytes.len();
422    // Pad to 8-byte boundary
423    let padded_len = (total_len + 7) & !7;
424
425    // Build action
426    let mut buf = Vec::with_capacity(padded_len);
427
428    // Action header
429    buf.extend((ActionType::Experimenter as u16).to_be_bytes());
430    buf.extend((padded_len as u16).to_be_bytes());
431    buf.extend(NICIRA_VENDOR_ID.to_be_bytes());
432    buf.extend((NxActionSubtype::Learn as u16).to_be_bytes());
433
434    // Learn fields
435    buf.extend(learn.idle_timeout.to_be_bytes());
436    buf.extend(learn.hard_timeout.to_be_bytes());
437    buf.extend(learn.priority.to_be_bytes());
438    buf.extend(learn.cookie.to_be_bytes());
439    buf.extend(learn.flags.to_be_bytes());
440    buf.push(learn.table_id);
441    buf.push(0); // pad
442    buf.extend(learn.fin_idle_timeout.to_be_bytes());
443    buf.extend(learn.fin_hard_timeout.to_be_bytes());
444
445    // Specs
446    buf.extend(specs_bytes);
447
448    // Padding
449    buf.resize(padded_len, 0);
450    buf
451}
452
453/// Learn spec header bits.
454mod learn_spec_header {
455    /// Match from field (src = packet field, dst = match field)
456    pub const SRC_FIELD: u16 = 0 << 13;
457    /// Match from immediate value
458    pub const SRC_IMMEDIATE: u16 = 1 << 13;
459    /// Load from field to action field
460    pub const DST_MATCH: u16 = 0 << 11;
461    /// Load to output action
462    pub const DST_LOAD: u16 = 1 << 11;
463    /// Output to port
464    pub const DST_OUTPUT: u16 = 2 << 11;
465}
466
467/// Encode a learn subfield to the wire format.
468///
469/// OVS learn subfield format (6 bytes):
470/// - 4 bytes: NXM/OXM header (full header including length)
471/// - 2 bytes: bit offset within the field
472fn encode_learn_subfield(buf: &mut Vec<u8>, nxm_header: u32, ofs: u16) {
473    buf.extend(nxm_header.to_be_bytes());
474    buf.extend(ofs.to_be_bytes());
475}
476
477/// Encode learn specs to wire format.
478fn encode_learn_specs(specs: &[LearnSpec]) -> Vec<u8> {
479    let mut buf = Vec::new();
480
481    for spec in specs {
482        match spec {
483            LearnSpec::MatchField { src_field, dst_field, n_bits } => {
484                // Header: src=field, dst=match (bits 0-10 = n_bits - 1)
485                let header = learn_spec_header::SRC_FIELD
486                    | learn_spec_header::DST_MATCH
487                    | (n_bits - 1);
488                buf.extend(header.to_be_bytes());
489                // Encode src and dst as learn subfields (6 bytes each: 4 header + 2 offset)
490                encode_learn_subfield(&mut buf, *src_field, 0);
491                encode_learn_subfield(&mut buf, *dst_field, 0);
492            }
493            LearnSpec::MatchImmediate { dst_field, value, n_bits } => {
494                // Header: src=immediate, dst=match (bits 0-10 = n_bits - 1)
495                let header = learn_spec_header::SRC_IMMEDIATE
496                    | learn_spec_header::DST_MATCH
497                    | (n_bits - 1);
498                buf.extend(header.to_be_bytes());
499                // Immediate value (padded to 2-byte chunks)
500                let value_len = (*n_bits as usize).div_ceil(16) * 2;
501                let mut padded_value = vec![0u8; value_len];
502                let copy_len = value.len().min(value_len);
503                padded_value[value_len - copy_len..].copy_from_slice(&value[..copy_len]);
504                buf.extend(padded_value);
505                // Encode dst as learn subfield (6 bytes: 4 header + 2 offset)
506                encode_learn_subfield(&mut buf, *dst_field, 0);
507            }
508            LearnSpec::LoadField { src_field, dst_field, n_bits } => {
509                // Header: src=field, dst=load (bits 0-10 = n_bits - 1)
510                let header = learn_spec_header::SRC_FIELD
511                    | learn_spec_header::DST_LOAD
512                    | (n_bits - 1);
513                buf.extend(header.to_be_bytes());
514                // Encode src and dst as learn subfields (6 bytes each: 4 header + 2 offset)
515                encode_learn_subfield(&mut buf, *src_field, 0);
516                encode_learn_subfield(&mut buf, *dst_field, 0);
517            }
518            LearnSpec::LoadImmediate { dst_field, value, n_bits } => {
519                // Header: src=immediate, dst=load (bits 0-10 = n_bits - 1)
520                let header = learn_spec_header::SRC_IMMEDIATE
521                    | learn_spec_header::DST_LOAD
522                    | (n_bits - 1);
523                buf.extend(header.to_be_bytes());
524                // Immediate value
525                let value_len = (*n_bits as usize).div_ceil(16) * 2;
526                let mut padded_value = vec![0u8; value_len];
527                let copy_len = value.len().min(value_len);
528                padded_value[value_len - copy_len..].copy_from_slice(&value[..copy_len]);
529                buf.extend(padded_value);
530                // Encode dst as learn subfield (6 bytes: 4 header + 2 offset)
531                encode_learn_subfield(&mut buf, *dst_field, 0);
532            }
533            LearnSpec::OutputField { src_field, n_bits } => {
534                // Header: src=field, dst=output (bits 0-10 = n_bits - 1)
535                let header = learn_spec_header::SRC_FIELD
536                    | learn_spec_header::DST_OUTPUT
537                    | (n_bits - 1);
538                buf.extend(header.to_be_bytes());
539                // Encode src as learn subfield (6 bytes: 4 header + 2 offset)
540                encode_learn_subfield(&mut buf, *src_field, 0);
541            }
542        }
543    }
544
545    buf
546}
547
548// ============================================================================
549// Nicira Action Decoding
550// ============================================================================
551
552use super::Action;
553use crate::oxm::OxmClass;
554
555/// Decode Nicira experimenter action.
556///
557/// The vendor ID has already been consumed. Data starts at subtype.
558pub(crate) fn decode_nicira_action(data: &[u8]) -> crate::Result<Action> {
559    if data.len() < 2 {
560        return Err(crate::Error::Parse("nicira action too short".into()));
561    }
562
563    let subtype = u16::from_be_bytes([data[0], data[1]]);
564
565    match subtype {
566        s if s == NxActionSubtype::ResubmitTable as u16 => {
567            // Resubmit: subtype (2) + in_port (2) + table (1) + pad (3)
568            if data.len() < 6 {
569                return Err(crate::Error::Parse("resubmit action too short".into()));
570            }
571            let in_port = u16::from_be_bytes([data[2], data[3]]);
572            let table = data[4];
573            let port = if in_port == 0xfff8 { None } else { Some(in_port) };
574            let table = if table == 255 { None } else { Some(table) };
575            Ok(Action::NxResubmit { port, table })
576        }
577        s if s == NxActionSubtype::Resubmit as u16 => {
578            // Simple resubmit: subtype (2) + in_port (2)
579            if data.len() < 4 {
580                return Err(crate::Error::Parse("resubmit action too short".into()));
581            }
582            let in_port = u16::from_be_bytes([data[2], data[3]]);
583            let port = if in_port == 0xfff8 { None } else { Some(in_port) };
584            Ok(Action::NxResubmit { port, table: None })
585        }
586        s if s == NxActionSubtype::Ct as u16 => {
587            // CT: subtype (2) + flags (2) + zone_src (4) + zone (2) + recirc_table (1) + ...
588            if data.len() < 10 {
589                return Err(crate::Error::Parse("ct action too short".into()));
590            }
591            let flags = u16::from_be_bytes([data[2], data[3]]);
592            // zone_src at data[4..8]
593            let zone = u16::from_be_bytes([data[8], data[9]]);
594            let recirc_table = if data.len() > 10 { data[10] } else { 255 };
595            let table = if recirc_table == 255 { None } else { Some(recirc_table) };
596            Ok(Action::NxCt { flags, zone, table })
597        }
598        s if s == NxActionSubtype::RegLoad2 as u16 => {
599            // RegLoad2: subtype (2) + OXM header (4) + value
600            if data.len() < 6 {
601                return Err(crate::Error::Parse("reg_load2 action too short".into()));
602            }
603            let oxm_header = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
604            let oxm_class = (oxm_header >> 16) as u16;
605            let field = ((oxm_header >> 9) & 0x7f) as u8;
606            let length = (oxm_header & 0xff) as usize;
607
608            if data.len() < 6 + length {
609                return Err(crate::Error::Parse("reg_load2 value truncated".into()));
610            }
611
612            let value = &data[6..6 + length];
613
614            // NXM1 class, field 16 = tunnel ID
615            if oxm_class == OxmClass::Nxm1 as u16 && field == 16 && length >= 8 {
616                let tun_id = u64::from_be_bytes([
617                    value[0], value[1], value[2], value[3],
618                    value[4], value[5], value[6], value[7],
619                ]);
620                Ok(Action::SetTunnelId(tun_id))
621            } else {
622                Ok(Action::Drop)
623            }
624        }
625        s if s == NxActionSubtype::Learn as u16 => {
626            // Learn: subtype (2) + idle_timeout (2) + hard_timeout (2) + priority (2)
627            //        + cookie (8) + flags (2) + table_id (1) + pad (1)
628            //        + fin_idle_timeout (2) + fin_hard_timeout (2) + specs (variable)
629            if data.len() < 22 {
630                return Err(crate::Error::Parse("learn action too short".into()));
631            }
632            let idle_timeout = u16::from_be_bytes([data[2], data[3]]);
633            let hard_timeout = u16::from_be_bytes([data[4], data[5]]);
634            let priority = u16::from_be_bytes([data[6], data[7]]);
635            let cookie = u64::from_be_bytes([
636                data[8], data[9], data[10], data[11],
637                data[12], data[13], data[14], data[15],
638            ]);
639            let flags = u16::from_be_bytes([data[16], data[17]]);
640            let table_id = data[18];
641            // data[19] is padding
642            let fin_idle_timeout = u16::from_be_bytes([data[20], data[21]]);
643            let fin_hard_timeout = if data.len() > 23 {
644                u16::from_be_bytes([data[22], data[23]])
645            } else {
646                0
647            };
648
649            // Decode specs (simplified - full decoding would parse the spec headers)
650            let specs = if data.len() > 24 {
651                decode_learn_specs(&data[24..])
652            } else {
653                Vec::new()
654            };
655
656            Ok(Action::NxLearn(NxLearn {
657                idle_timeout,
658                hard_timeout,
659                priority,
660                cookie,
661                flags,
662                table_id,
663                fin_idle_timeout,
664                fin_hard_timeout,
665                specs,
666            }))
667        }
668        _ => {
669            // Unknown Nicira subtype
670            Ok(Action::Drop)
671        }
672    }
673}
674
675/// Decode learn specs from wire format.
676pub(crate) fn decode_learn_specs(data: &[u8]) -> Vec<LearnSpec> {
677    let mut specs = Vec::new();
678    let mut offset = 0;
679
680    while offset + 2 <= data.len() {
681        let header = u16::from_be_bytes([data[offset], data[offset + 1]]);
682        if header == 0 {
683            break; // End of specs
684        }
685        offset += 2;
686
687        let n_bits = (header & 0x07ff) + 1; // Lower 11 bits store n_bits - 1
688        let src_type = (header >> 13) & 0x01; // Bit 13: 0=field, 1=immediate
689        let dst_type = (header >> 11) & 0x03; // Bits 11-12: 0=match, 1=load, 2=output
690
691        match (src_type, dst_type) {
692            (0, 0) => {
693                // MatchField: src_subfield (6) + dst_subfield (6)
694                // Subfield format: 4-byte NXM header + 2-byte offset
695                if offset + 12 > data.len() {
696                    break;
697                }
698                let src_field = u32::from_be_bytes([
699                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
700                ]);
701                // Skip 2-byte offset (currently unused in our API)
702                let dst_field = u32::from_be_bytes([
703                    data[offset + 6], data[offset + 7], data[offset + 8], data[offset + 9],
704                ]);
705                // Skip 2-byte offset
706                offset += 12;
707                specs.push(LearnSpec::MatchField { src_field, dst_field, n_bits });
708            }
709            (1, 0) => {
710                // MatchImmediate: value (variable) + dst_subfield (6)
711                let value_len = (n_bits as usize).div_ceil(16) * 2;
712                if offset + value_len + 6 > data.len() {
713                    break;
714                }
715                let value = data[offset..offset + value_len].to_vec();
716                offset += value_len;
717                let dst_field = u32::from_be_bytes([
718                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
719                ]);
720                offset += 6; // 4-byte header + 2-byte offset
721                specs.push(LearnSpec::MatchImmediate { dst_field, value, n_bits });
722            }
723            (0, 1) => {
724                // LoadField: src_subfield (6) + dst_subfield (6)
725                if offset + 12 > data.len() {
726                    break;
727                }
728                let src_field = u32::from_be_bytes([
729                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
730                ]);
731                let dst_field = u32::from_be_bytes([
732                    data[offset + 6], data[offset + 7], data[offset + 8], data[offset + 9],
733                ]);
734                offset += 12;
735                specs.push(LearnSpec::LoadField { src_field, dst_field, n_bits });
736            }
737            (1, 1) => {
738                // LoadImmediate: value (variable) + dst_subfield (6)
739                let value_len = (n_bits as usize).div_ceil(16) * 2;
740                if offset + value_len + 6 > data.len() {
741                    break;
742                }
743                let value = data[offset..offset + value_len].to_vec();
744                offset += value_len;
745                let dst_field = u32::from_be_bytes([
746                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
747                ]);
748                offset += 6; // 4-byte header + 2-byte offset
749                specs.push(LearnSpec::LoadImmediate { dst_field, value, n_bits });
750            }
751            (0, 2) => {
752                // OutputField: src_subfield (6)
753                if offset + 6 > data.len() {
754                    break;
755                }
756                let src_field = u32::from_be_bytes([
757                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
758                ]);
759                offset += 6; // 4-byte header + 2-byte offset
760                specs.push(LearnSpec::OutputField { src_field, n_bits });
761            }
762            _ => {
763                // Unknown spec type, skip
764                break;
765            }
766        }
767    }
768
769    specs
770}