letmein_fwproto/
lib.rs

1// -*- coding: utf-8 -*-
2//
3// Copyright (C) 2024 - 2026 Michael Büsch <m@bues.ch>
4//
5// Licensed under the Apache License version 2.0
6// or the MIT license, at your option.
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9//! This crate implements the firewall socket protocol
10//! for communication between the `letmeind` and `letmeinfwd` daemons.
11//!
12//! Serializing messages to a raw byte stream and
13//! deserializing raw byte stream to a message is implemented here.
14
15#![forbid(unsafe_code)]
16
17use anyhow::{self as ah, format_err as err, Context as _};
18use letmein_conf::ConfigChecksum;
19use letmein_proto::{ResourceId, UserId};
20use std::{
21    future::Future,
22    net::{IpAddr, Ipv4Addr},
23};
24use tokio::io::ErrorKind;
25
26#[cfg(any(target_os = "linux", target_os = "android"))]
27use tokio::net::UnixStream;
28
29#[cfg(target_os = "windows")]
30use tokio::net::windows::named_pipe::{NamedPipeClient, NamedPipeServer};
31
32/// Firewall daemon Unix socket file name.
33pub const SOCK_FILE: &str = "letmeinfwd.sock";
34
35/// The operation to perform on the firewall.
36#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
37#[repr(u16)]
38pub enum FirewallOperation {
39    /// Not-Acknowledge message.
40    #[default]
41    Nack,
42    /// Acknowledge message.
43    Ack,
44    /// Add port-open or jump rules.
45    Install,
46    /// Revoke port-open or jump rules.
47    Revoke,
48}
49
50impl TryFrom<u16> for FirewallOperation {
51    type Error = ah::Error;
52
53    fn try_from(value: u16) -> Result<Self, Self::Error> {
54        const OPERATION_NACK: u16 = FirewallOperation::Nack as u16;
55        const OPERATION_ACK: u16 = FirewallOperation::Ack as u16;
56        const OPERATION_INSTALL: u16 = FirewallOperation::Install as u16;
57        const OPERATION_REVOKE: u16 = FirewallOperation::Revoke as u16;
58        match value {
59            OPERATION_NACK => Ok(Self::Nack),
60            OPERATION_ACK => Ok(Self::Ack),
61            OPERATION_INSTALL => Ok(Self::Install),
62            OPERATION_REVOKE => Ok(Self::Revoke),
63            _ => Err(err!("Invalid FirewallMessage/Operation value")),
64        }
65    }
66}
67
68impl From<FirewallOperation> for u16 {
69    fn from(operation: FirewallOperation) -> u16 {
70        operation as _
71    }
72}
73
74/// The type of address to open in the firewall.
75#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
76#[repr(u16)]
77pub enum AddrType {
78    /// IPv6 address.
79    #[default]
80    Ipv6,
81    /// IPv4 address.
82    Ipv4,
83}
84
85impl TryFrom<u16> for AddrType {
86    type Error = ah::Error;
87
88    fn try_from(value: u16) -> Result<Self, Self::Error> {
89        const ADDRTYPE_IPV6: u16 = AddrType::Ipv6 as u16;
90        const ADDRTYPE_IPV4: u16 = AddrType::Ipv4 as u16;
91        match value {
92            ADDRTYPE_IPV6 => Ok(Self::Ipv6),
93            ADDRTYPE_IPV4 => Ok(Self::Ipv4),
94            _ => Err(err!("Invalid FirewallMessage/AddrType value")),
95        }
96    }
97}
98
99impl From<AddrType> for u16 {
100    fn from(addr_type: AddrType) -> u16 {
101        addr_type as _
102    }
103}
104
105/// Size of the `addr` field in the message.
106const ADDR_SIZE: usize = 16;
107
108/// Size of the `conf_cs` field in the message.
109const CONF_CS_SIZE: usize = ConfigChecksum::SIZE;
110
111/// Size of the firewall control message.
112const FWMSG_SIZE: usize = 2 + 4 + 4 + 2 + ADDR_SIZE + CONF_CS_SIZE;
113
114/// Byte offset of the `operation` field in the firewall control message.
115const FWMSG_OFFS_OPERATION: usize = 0;
116
117/// Byte offset of the `user` field in the firewall control message.
118const FWMSG_OFFS_USER: usize = 2;
119
120/// Byte offset of the `resource` field in the firewall control message.
121const FWMSG_OFFS_RESOURCE: usize = 6;
122
123/// Byte offset of the `addr_type` field in the firewall control message.
124const FWMSG_OFFS_ADDR_TYPE: usize = 10;
125
126/// Byte offset of the `addr` field in the firewall control message.
127const FWMSG_OFFS_ADDR: usize = 12;
128
129/// Byte offset of the `conf_cs` field in the firewall control message.
130const FWMSG_OFFS_CONF_CS: usize = 28;
131
132/// A message to control the firewall.
133#[derive(PartialEq, Eq, Debug, Default)]
134pub struct FirewallMessage {
135    operation: FirewallOperation,
136    user: UserId,
137    resource: ResourceId,
138    addr_type: AddrType,
139    addr: [u8; ADDR_SIZE],
140    conf_cs: ConfigChecksum,
141}
142
143/// Convert an `IpAddr` to the `operation` and `addr` fields of a firewall control message.
144fn addr_to_octets(addr: IpAddr) -> (AddrType, [u8; ADDR_SIZE]) {
145    match addr {
146        IpAddr::V4(addr) => {
147            let o = addr.octets();
148            (
149                AddrType::Ipv4,
150                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, o[0], o[1], o[2], o[3]],
151            )
152        }
153        IpAddr::V6(addr) => (AddrType::Ipv6, addr.octets()),
154    }
155}
156
157/// Convert a firewall control message `operation` and `addr` fields to an `IpAddr`.
158fn octets_to_addr(addr_type: AddrType, addr: &[u8; ADDR_SIZE]) -> IpAddr {
159    match addr_type {
160        AddrType::Ipv4 => Ipv4Addr::new(addr[12], addr[13], addr[14], addr[15]).into(),
161        AddrType::Ipv6 => (*addr).into(),
162    }
163}
164
165impl FirewallMessage {
166    /// Construct a new message to install firewall rules for a resource.
167    pub fn new_install(
168        user: UserId,
169        resource: ResourceId,
170        addr: IpAddr,
171        conf_cs: &ConfigChecksum,
172    ) -> Self {
173        let (addr_type, addr) = addr_to_octets(addr);
174        Self {
175            operation: FirewallOperation::Install,
176            user,
177            resource,
178            addr_type,
179            addr,
180            conf_cs: conf_cs.clone(),
181        }
182    }
183
184    /// Construct a new message to revoke firewall rule for a resource.
185    pub fn new_revoke(
186        user: UserId,
187        resource: ResourceId,
188        addr: IpAddr,
189        conf_cs: &ConfigChecksum,
190    ) -> Self {
191        let (addr_type, addr) = addr_to_octets(addr);
192        Self {
193            operation: FirewallOperation::Revoke,
194            user,
195            resource,
196            addr_type,
197            addr,
198            conf_cs: conf_cs.clone(),
199        }
200    }
201
202    /// Construct a new acknowledge message.
203    pub fn new_ack() -> Self {
204        Self {
205            operation: FirewallOperation::Ack,
206            ..Default::default()
207        }
208    }
209
210    /// Construct a new not-acknowledge message.
211    pub fn new_nack() -> Self {
212        Self {
213            operation: FirewallOperation::Nack,
214            ..Default::default()
215        }
216    }
217
218    /// Get the operation type from this message.
219    pub fn operation(&self) -> FirewallOperation {
220        self.operation
221    }
222
223    /// Get the user ID from this message.
224    pub fn user(&self) -> UserId {
225        self.user
226    }
227
228    /// Get the resource ID from this message.
229    pub fn resource(&self) -> ResourceId {
230        self.resource
231    }
232
233    /// Get the `IpAddr` from this message.
234    pub fn addr(&self) -> Option<IpAddr> {
235        match self.operation {
236            FirewallOperation::Install | FirewallOperation::Revoke => {
237                Some(octets_to_addr(self.addr_type, &self.addr))
238            }
239            FirewallOperation::Ack | FirewallOperation::Nack => None,
240        }
241    }
242
243    /// Get the configuration checksum from this message.
244    pub fn conf_checksum(&self) -> Option<&ConfigChecksum> {
245        match self.operation {
246            FirewallOperation::Install | FirewallOperation::Revoke => Some(&self.conf_cs),
247            FirewallOperation::Ack | FirewallOperation::Nack => None,
248        }
249    }
250
251    /// Serialize this message into a byte stream.
252    pub fn msg_serialize(&self) -> ah::Result<[u8; FWMSG_SIZE]> {
253        // The serialization is simple enough to do manually.
254        // Therefore, we don't use the `serde` crate here.
255
256        #[inline]
257        fn serialize_u16(buf: &mut [u8], value: u16) {
258            buf[0..2].copy_from_slice(&value.to_be_bytes());
259        }
260
261        #[inline]
262        fn serialize_u32(buf: &mut [u8], value: u32) {
263            buf[0..4].copy_from_slice(&value.to_be_bytes());
264        }
265
266        let mut buf = [0; FWMSG_SIZE];
267        serialize_u16(&mut buf[FWMSG_OFFS_OPERATION..], self.operation.into());
268        serialize_u32(&mut buf[FWMSG_OFFS_USER..], self.user.into());
269        serialize_u32(&mut buf[FWMSG_OFFS_RESOURCE..], self.resource.into());
270        serialize_u16(&mut buf[FWMSG_OFFS_ADDR_TYPE..], self.addr_type.into());
271        buf[FWMSG_OFFS_ADDR..FWMSG_OFFS_ADDR + ADDR_SIZE].copy_from_slice(&self.addr);
272        buf[FWMSG_OFFS_CONF_CS..FWMSG_OFFS_CONF_CS + CONF_CS_SIZE]
273            .copy_from_slice(self.conf_cs.as_bytes());
274
275        Ok(buf)
276    }
277
278    /// Try to deserialize a byte stream into a message.
279    pub fn try_msg_deserialize(buf: &[u8]) -> ah::Result<Self> {
280        if buf.len() != FWMSG_SIZE {
281            return Err(err!("Deserialize: Raw message size mismatch."));
282        }
283
284        // The deserialization is simple enough to do manually.
285        // Therefore, we don't use the `serde` crate here.
286
287        #[inline]
288        fn deserialize_u16(buf: &[u8]) -> ah::Result<u16> {
289            Ok(u16::from_be_bytes(buf[0..2].try_into()?))
290        }
291
292        #[inline]
293        fn deserialize_u32(buf: &[u8]) -> ah::Result<u32> {
294            Ok(u32::from_be_bytes(buf[0..4].try_into()?))
295        }
296
297        let operation = deserialize_u16(&buf[FWMSG_OFFS_OPERATION..])?;
298        let user = deserialize_u32(&buf[FWMSG_OFFS_USER..])?;
299        let resource = deserialize_u32(&buf[FWMSG_OFFS_RESOURCE..])?;
300        let addr_type = deserialize_u16(&buf[FWMSG_OFFS_ADDR_TYPE..])?;
301        let addr = &buf[FWMSG_OFFS_ADDR..FWMSG_OFFS_ADDR + ADDR_SIZE];
302        let conf_cs = &buf[FWMSG_OFFS_CONF_CS..FWMSG_OFFS_CONF_CS + CONF_CS_SIZE];
303
304        Ok(Self {
305            operation: operation.try_into()?,
306            resource: resource.into(),
307            user: user.into(),
308            addr_type: addr_type.try_into()?,
309            addr: addr.try_into()?,
310            conf_cs: conf_cs.try_into()?,
311        })
312    }
313
314    /// Send this message over a [Stream].
315    pub async fn send(&self, stream: &mut impl Stream) -> ah::Result<()> {
316        let txbuf = self.msg_serialize()?;
317        let mut txcount = 0;
318        loop {
319            stream.writable().await.context("Socket polling (tx)")?;
320            match stream.try_write(&txbuf[txcount..]) {
321                Ok(n) => {
322                    txcount += n;
323                    assert!(txcount <= txbuf.len());
324                    if txcount == txbuf.len() {
325                        return Ok(());
326                    }
327                }
328                Err(e) if e.kind() == ErrorKind::WouldBlock => (),
329                Err(e) => {
330                    return Err(err!("Socket write: {e}"));
331                }
332            }
333        }
334    }
335
336    /// Try to receive a message from a [Stream].
337    pub async fn recv(stream: &mut impl Stream) -> ah::Result<Option<Self>> {
338        let mut rxbuf = [0; FWMSG_SIZE];
339        let mut rxcount = 0;
340        loop {
341            stream.readable().await.context("Socket polling (rx)")?;
342            match stream.try_read(&mut rxbuf[rxcount..]) {
343                Ok(n) => {
344                    if n == 0 {
345                        return Ok(None);
346                    }
347                    rxcount += n;
348                    assert!(rxcount <= FWMSG_SIZE);
349                    if rxcount == FWMSG_SIZE {
350                        return Ok(Some(Self::try_msg_deserialize(&rxbuf)?));
351                    }
352                }
353                Err(e) if e.kind() == ErrorKind::WouldBlock => (),
354                Err(e) => {
355                    return Err(err!("Socket read: {e}"));
356                }
357            }
358        }
359    }
360}
361
362/// Communication stream abstraction.
363pub trait Stream {
364    fn readable(&self) -> impl Future<Output = std::io::Result<()>> + Send;
365    fn try_read(&self, buf: &mut [u8]) -> std::io::Result<usize>;
366    fn writable(&self) -> impl Future<Output = std::io::Result<()>> + Send;
367    fn try_write(&self, buf: &[u8]) -> std::io::Result<usize>;
368}
369
370macro_rules! impl_stream_for {
371    ($ty:ty) => {
372        impl Stream for $ty {
373            fn readable(&self) -> impl Future<Output = std::io::Result<()>> + Send {
374                self.readable()
375            }
376            fn try_read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
377                self.try_read(buf)
378            }
379            fn writable(&self) -> impl Future<Output = std::io::Result<()>> + Send {
380                self.writable()
381            }
382            fn try_write(&self, buf: &[u8]) -> std::io::Result<usize> {
383                self.try_write(buf)
384            }
385        }
386    };
387}
388
389#[cfg(any(target_os = "linux", target_os = "android"))]
390impl_stream_for!(UnixStream);
391#[cfg(target_os = "windows")]
392impl_stream_for!(NamedPipeClient);
393#[cfg(target_os = "windows")]
394impl_stream_for!(NamedPipeServer);
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    fn check_ser_de(msg: &FirewallMessage) {
401        // Serialize a message and then deserialize the byte stream
402        // and check if the resulting message is the same.
403        let bytes = msg.msg_serialize().unwrap();
404        let msg_de = FirewallMessage::try_msg_deserialize(&bytes).unwrap();
405        assert_eq!(*msg, msg_de);
406    }
407
408    #[test]
409    fn test_msg_install_v6() {
410        let msg = FirewallMessage::new_install(
411            0x66773322.into(),
412            0xAABB9988.into(),
413            "::1".parse().unwrap(),
414            &ConfigChecksum::calculate(b"foo"),
415        );
416        assert_eq!(msg.operation(), FirewallOperation::Install);
417        assert_eq!(msg.addr(), Some("::1".parse().unwrap()));
418        check_ser_de(&msg);
419
420        let msg = FirewallMessage::new_install(
421            0x66773322.into(),
422            0xAABB9988.into(),
423            "0102:0304:0506:0708:090A:0B0C:0D0E:0F10".parse().unwrap(),
424            &ConfigChecksum::calculate(b"foo"),
425        );
426        let bytes = msg.msg_serialize().unwrap();
427        assert_eq!(
428            bytes,
429            [
430                0x00, 0x02, // operation
431                0x66, 0x77, 0x33, 0x22, // user
432                0xAA, 0xBB, 0x99, 0x88, // resource
433                0x00, 0x00, // addr_type
434                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // addr
435                0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // addr
436                0xA9, 0x14, 0x20, 0xEB, 0x27, 0x9B, 0xD1, 0x96, // conf_cs
437                0x15, 0x04, 0x9D, 0x00, 0xD6, 0x07, 0x07, 0x35, // conf_cs
438                0xAF, 0xF1, 0xE9, 0x28, 0xC1, 0xBF, 0x9C, 0x65, // conf_cs
439                0x3F, 0x29, 0x22, 0x33, 0x11, 0xDD, 0x4C, 0xAA, // conf_cs
440            ]
441        );
442
443        let msg = FirewallMessage::new_install(
444            0x66773322.into(),
445            0xAABB9988.into(),
446            "0102:0304:0506:0708:090A:0B0C:0D0E:0F10".parse().unwrap(),
447            &ConfigChecksum::calculate(b"foo"),
448        );
449        let bytes = msg.msg_serialize().unwrap();
450        assert_eq!(
451            bytes,
452            [
453                0x00, 0x02, // operation
454                0x66, 0x77, 0x33, 0x22, // user
455                0xAA, 0xBB, 0x99, 0x88, // resource
456                0x00, 0x00, // addr_type
457                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // addr
458                0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // addr
459                0xA9, 0x14, 0x20, 0xEB, 0x27, 0x9B, 0xD1, 0x96, // conf_cs
460                0x15, 0x04, 0x9D, 0x00, 0xD6, 0x07, 0x07, 0x35, // conf_cs
461                0xAF, 0xF1, 0xE9, 0x28, 0xC1, 0xBF, 0x9C, 0x65, // conf_cs
462                0x3F, 0x29, 0x22, 0x33, 0x11, 0xDD, 0x4C, 0xAA, // conf_cs
463            ]
464        );
465
466        let msg = FirewallMessage::new_install(
467            0x66773322.into(),
468            0xAABB9988.into(),
469            "0102:0304:0506:0708:090A:0B0C:0D0E:0F10".parse().unwrap(),
470            &ConfigChecksum::calculate(b"bar"),
471        );
472        let bytes = msg.msg_serialize().unwrap();
473        assert_eq!(
474            bytes,
475            [
476                0x00, 0x02, // operation
477                0x66, 0x77, 0x33, 0x22, // user
478                0xAA, 0xBB, 0x99, 0x88, // resource
479                0x00, 0x00, // addr_type
480                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // addr
481                0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // addr
482                0xFB, 0x58, 0xE5, 0x1D, 0x34, 0xB5, 0x6B, 0x33, // conf_cs
483                0x78, 0xF6, 0xEC, 0x2D, 0x4A, 0x54, 0x96, 0xC2, // conf_cs
484                0xAF, 0x00, 0xF2, 0x75, 0x02, 0x00, 0x0F, 0xD5, // conf_cs
485                0x98, 0xED, 0x31, 0x09, 0x73, 0x68, 0x50, 0x79, // conf_cs
486            ]
487        );
488    }
489
490    #[test]
491    fn test_msg_install_v4() {
492        let msg = FirewallMessage::new_install(
493            0x66773322.into(),
494            0xAABB9988.into(),
495            "1.2.3.4".parse().unwrap(),
496            &ConfigChecksum::calculate(b"foo"),
497        );
498        assert_eq!(msg.operation(), FirewallOperation::Install);
499        assert_eq!(msg.addr(), Some("1.2.3.4".parse().unwrap()));
500        check_ser_de(&msg);
501
502        let bytes = msg.msg_serialize().unwrap();
503        assert_eq!(
504            bytes,
505            [
506                0x00, 0x02, // operation
507                0x66, 0x77, 0x33, 0x22, // user
508                0xAA, 0xBB, 0x99, 0x88, // resource
509                0x00, 0x01, // addr_type
510                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
511                0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, // addr
512                0xA9, 0x14, 0x20, 0xEB, 0x27, 0x9B, 0xD1, 0x96, // conf_cs
513                0x15, 0x04, 0x9D, 0x00, 0xD6, 0x07, 0x07, 0x35, // conf_cs
514                0xAF, 0xF1, 0xE9, 0x28, 0xC1, 0xBF, 0x9C, 0x65, // conf_cs
515                0x3F, 0x29, 0x22, 0x33, 0x11, 0xDD, 0x4C, 0xAA, // conf_cs
516            ]
517        );
518    }
519
520    #[test]
521    fn test_msg_revoke_v6() {
522        let msg = FirewallMessage::new_revoke(
523            0x66773322.into(),
524            0xAABB9988.into(),
525            "0102:0304:0506:0708:090A:0B0C:0D0E:0F10".parse().unwrap(),
526            &ConfigChecksum::calculate(b"foo"),
527        );
528        assert_eq!(msg.operation(), FirewallOperation::Revoke);
529        assert_eq!(
530            msg.addr(),
531            Some("0102:0304:0506:0708:090A:0B0C:0D0E:0F10".parse().unwrap())
532        );
533        check_ser_de(&msg);
534
535        let bytes = msg.msg_serialize().unwrap();
536        assert_eq!(
537            bytes,
538            [
539                0x00, 0x03, // operation
540                0x66, 0x77, 0x33, 0x22, // user
541                0xAA, 0xBB, 0x99, 0x88, // resource
542                0x00, 0x00, // addr_type
543                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // addr
544                0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // addr
545                0xA9, 0x14, 0x20, 0xEB, 0x27, 0x9B, 0xD1, 0x96, // conf_cs
546                0x15, 0x04, 0x9D, 0x00, 0xD6, 0x07, 0x07, 0x35, // conf_cs
547                0xAF, 0xF1, 0xE9, 0x28, 0xC1, 0xBF, 0x9C, 0x65, // conf_cs
548                0x3F, 0x29, 0x22, 0x33, 0x11, 0xDD, 0x4C, 0xAA, // conf_cs
549            ]
550        );
551    }
552
553    #[test]
554    fn test_msg_revoke_v4() {
555        let msg = FirewallMessage::new_revoke(
556            0x66773322.into(),
557            0xAABB9988.into(),
558            "1.2.3.4".parse().unwrap(),
559            &ConfigChecksum::calculate(b"foo"),
560        );
561        assert_eq!(msg.operation(), FirewallOperation::Revoke);
562        assert_eq!(msg.addr(), Some("1.2.3.4".parse().unwrap()));
563        check_ser_de(&msg);
564
565        let bytes = msg.msg_serialize().unwrap();
566        assert_eq!(
567            bytes,
568            [
569                0x00, 0x03, // operation
570                0x66, 0x77, 0x33, 0x22, // user
571                0xAA, 0xBB, 0x99, 0x88, // resource
572                0x00, 0x01, // addr_type
573                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
574                0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, // addr
575                0xA9, 0x14, 0x20, 0xEB, 0x27, 0x9B, 0xD1, 0x96, // conf_cs
576                0x15, 0x04, 0x9D, 0x00, 0xD6, 0x07, 0x07, 0x35, // conf_cs
577                0xAF, 0xF1, 0xE9, 0x28, 0xC1, 0xBF, 0x9C, 0x65, // conf_cs
578                0x3F, 0x29, 0x22, 0x33, 0x11, 0xDD, 0x4C, 0xAA, // conf_cs
579            ]
580        );
581    }
582
583    #[test]
584    fn test_msg_ack() {
585        let msg = FirewallMessage::new_ack();
586        assert_eq!(msg.operation(), FirewallOperation::Ack);
587        assert_eq!(msg.addr(), None);
588        check_ser_de(&msg);
589
590        let bytes = msg.msg_serialize().unwrap();
591        assert_eq!(
592            bytes,
593            [
594                0x00, 0x01, // operation
595                0x00, 0x00, 0x00, 0x00, // user
596                0x00, 0x00, 0x00, 0x00, // resource
597                0x00, 0x00, // addr_type
598                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
599                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
600                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
601                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
602                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
603                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
604            ]
605        );
606    }
607
608    #[test]
609    fn test_msg_nack() {
610        let msg = FirewallMessage::new_nack();
611        assert_eq!(msg.operation(), FirewallOperation::Nack);
612        assert_eq!(msg.addr(), None);
613        check_ser_de(&msg);
614
615        let bytes = msg.msg_serialize().unwrap();
616        assert_eq!(
617            bytes,
618            [
619                0x00, 0x00, // operation
620                0x00, 0x00, 0x00, 0x00, // user
621                0x00, 0x00, 0x00, 0x00, // resource
622                0x00, 0x00, // addr_type
623                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
624                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // addr
625                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
626                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
627                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
628                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // conf_cs
629            ]
630        );
631    }
632}
633
634// vim: ts=4 sw=4 expandtab