diff --git a/src/buffer.rs b/src/buffer.rs index a9d4917..b31084d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT use crate::{ + conntrack::ConntrackMessage, message::{ - NetfilterHeader, NetfilterMessage, NetfilterMessageInner, + NetfilterHeader, NetfilterMessage, NetfilterMessageInner, Subsystem, NETFILTER_HEADER_LEN, }, nflog::NfLogMessage, @@ -52,15 +53,19 @@ impl<'a, T: AsRef<[u8]> + ?Sized> .context("failed to parse netfilter header")?; let subsys = (message_type >> 8) as u8; let message_type = message_type as u8; - let inner = match subsys { - NfLogMessage::SUBSYS => NetfilterMessageInner::NfLog( + let inner = match Subsystem::from(subsys) { + Subsystem::NfLog => NetfilterMessageInner::NfLog( NfLogMessage::parse_with_param(buf, message_type) .context("failed to parse nflog payload")?, ), - _ => NetfilterMessageInner::Other { - subsys, + Subsystem::Conntrack => NetfilterMessageInner::Conntrack( + ConntrackMessage::parse_with_param(buf, message_type) + .context("failed to parse conntrack payload")?, + ), + subsys_enum @ Subsystem::Other(_) => NetfilterMessageInner::Other { + subsys: subsys_enum, message_type, - nlas: buf.default_nlas()?, + attributes: buf.default_nlas()?, }, }; Ok(NetfilterMessage::new(header, inner)) diff --git a/src/conntrack/attributes/attribute.rs b/src/conntrack/attributes/attribute.rs new file mode 100644 index 0000000..272a903 --- /dev/null +++ b/src/conntrack/attributes/attribute.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, + NlasIterator, Parseable, +}; + +use crate::conntrack::attributes::{protoinfo::ProtoInfo, tuple::Tuple}; + +const CTA_TUPLE_ORIG: u16 = 1; +const CTA_PROTOINFO: u16 = 4; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConntrackNla { + CtaTupleOrig(Vec), + CtaProtoInfo(Vec), + Other(DefaultNla), +} + +impl Nla for ConntrackNla { + fn value_len(&self) -> usize { + match self { + ConntrackNla::CtaTupleOrig(attr) => { + attr.iter().map(|op| op.buffer_len()).sum() + } + ConntrackNla::CtaProtoInfo(attr) => { + attr.iter().map(|op| op.buffer_len()).sum() + } + ConntrackNla::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + ConntrackNla::CtaTupleOrig(_) => CTA_TUPLE_ORIG, + ConntrackNla::CtaProtoInfo(_) => CTA_PROTOINFO, + ConntrackNla::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + ConntrackNla::CtaTupleOrig(attr) => { + let mut len = 0; + for op in attr { + op.emit(&mut buffer[len..]); + len += op.buffer_len(); + } + } + ConntrackNla::CtaProtoInfo(attr) => { + let mut len = 0; + for op in attr { + op.emit(&mut buffer[len..]); + len += op.buffer_len(); + } + } + ConntrackNla::Other(attr) => attr.emit_value(buffer), + } + } + fn is_nested(&self) -> bool { + matches!( + self, + ConntrackNla::CtaTupleOrig(_) | ConntrackNla::CtaProtoInfo(_) + ) + } +} + +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for ConntrackNla +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_TUPLE_ORIG => { + let mut tuples = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = &nlas.context("invalid CTA_TUPLE_ORIG value")?; + tuples.push(Tuple::parse(nlas)?); + } + ConntrackNla::CtaTupleOrig(tuples) + } + CTA_PROTOINFO => { + let mut proto_infos = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = &nlas.context("invalid CTA_PROTOINFO value")?; + proto_infos.push(ProtoInfo::parse(nlas)?); + } + ConntrackNla::CtaProtoInfo(proto_infos) + } + _ => ConntrackNla::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/attributes/iptuple.rs b/src/conntrack/attributes/iptuple.rs new file mode 100644 index 0000000..7dcd770 --- /dev/null +++ b/src/conntrack/attributes/iptuple.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + parse_ip, DecodeError, DefaultNla, ErrorContext, Nla, NlaBuffer, Parseable, +}; +use std::net::IpAddr; + +const CTA_IP_V4_SRC: u16 = 1; +const CTA_IP_V6_SRC: u16 = 3; +const CTA_IP_V4_DST: u16 = 2; +const CTA_IP_V6_DST: u16 = 4; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum IPTuple { + SourceAddress(IpAddr), + DestinationAddress(IpAddr), + Other(DefaultNla), +} + +const IPV4_LEN: usize = 4; +const IPV6_LEN: usize = 16; + +// Helper function needed for implementing the Nla trait +pub fn emit_ip(addr: &IpAddr, buf: &mut [u8]) { + match addr { + IpAddr::V4(ip) => { + buf[..IPV4_LEN].copy_from_slice(ip.octets().as_slice()); + } + IpAddr::V6(ip) => { + buf[..IPV6_LEN].copy_from_slice(ip.octets().as_slice()); + } + } +} + +impl Nla for IPTuple { + fn value_len(&self) -> usize { + match self { + IPTuple::SourceAddress(attr) => match *attr { + IpAddr::V4(_) => IPV4_LEN, + IpAddr::V6(_) => IPV6_LEN, + }, + IPTuple::DestinationAddress(attr) => match *attr { + IpAddr::V4(_) => IPV4_LEN, + IpAddr::V6(_) => IPV6_LEN, + }, + IPTuple::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + IPTuple::SourceAddress(attr) => match *attr { + IpAddr::V4(_) => CTA_IP_V4_SRC, + IpAddr::V6(_) => CTA_IP_V6_SRC, + }, + IPTuple::DestinationAddress(attr) => match *attr { + IpAddr::V4(_) => CTA_IP_V4_DST, + IpAddr::V6(_) => CTA_IP_V6_DST, + }, + IPTuple::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + IPTuple::SourceAddress(attr) => emit_ip(attr, buffer), + IPTuple::DestinationAddress(attr) => emit_ip(attr, buffer), + IPTuple::Other(attr) => attr.emit_value(buffer), + } + } +} +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for IPTuple +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_IP_V4_SRC | CTA_IP_V6_SRC => Self::SourceAddress( + parse_ip(payload).context("invalid SourceAddress value")?, + ), + CTA_IP_V4_DST | CTA_IP_V6_DST => Self::DestinationAddress( + parse_ip(payload) + .context("invalid DestinationAddress value")?, + ), + _ => IPTuple::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/attributes/mod.rs b/src/conntrack/attributes/mod.rs new file mode 100644 index 0000000..07dfe35 --- /dev/null +++ b/src/conntrack/attributes/mod.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +mod attribute; +mod iptuple; +mod protoinfo; +mod protoinfotcp; +mod prototuple; +mod tcp_flags; +mod tuple; + +pub use attribute::ConntrackNla; +pub use iptuple::IPTuple; +pub use protoinfo::ProtoInfo; +pub use protoinfotcp::ProtoInfoTCP; +pub use prototuple::{ProtoTuple, Protocol}; +pub use tcp_flags::TCPFlags; +pub use tuple::Tuple; diff --git a/src/conntrack/attributes/protoinfo.rs b/src/conntrack/attributes/protoinfo.rs new file mode 100644 index 0000000..8719cb2 --- /dev/null +++ b/src/conntrack/attributes/protoinfo.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, + NlasIterator, Parseable, +}; + +use crate::conntrack::attributes::protoinfotcp::ProtoInfoTCP; + +const CTA_PROTOINFO_TCP: u16 = 1; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ProtoInfo { + TCP(Vec), + Other(DefaultNla), +} +impl Nla for ProtoInfo { + fn value_len(&self) -> usize { + match self { + ProtoInfo::TCP(nlas) => nlas.iter().map(|op| op.buffer_len()).sum(), + ProtoInfo::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + ProtoInfo::TCP(_) => CTA_PROTOINFO_TCP, + ProtoInfo::Other(attr) => attr.kind(), + } + } + fn emit_value(&self, buffer: &mut [u8]) { + match self { + ProtoInfo::TCP(nlas) => { + let mut len = 0; + for op in nlas { + op.emit(&mut buffer[len..]); + len += op.buffer_len(); + } + } + ProtoInfo::Other(attr) => attr.emit_value(buffer), + } + } + fn is_nested(&self) -> bool { + matches!(self, ProtoInfo::TCP(_)) + } +} + +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for ProtoInfo +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_PROTOINFO_TCP => { + let mut proto_info_tcps = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = + &nlas.context("invailid CTA_PROTOINFO_TCP value")?; + proto_info_tcps.push(ProtoInfoTCP::parse(nlas)?); + } + ProtoInfo::TCP(proto_info_tcps) + } + _ => ProtoInfo::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/attributes/protoinfotcp.rs b/src/conntrack/attributes/protoinfotcp.rs new file mode 100644 index 0000000..cf3dc6c --- /dev/null +++ b/src/conntrack/attributes/protoinfotcp.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + parse_u8, DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, + Parseable, +}; + +use crate::conntrack::attributes::tcp_flags::{TCPFlags, TCPFlagsBuffer}; + +const CTA_PROTOINFO_TCP_STATE: u16 = 1; +const CTA_PROTOINFO_TCP_WSCALE_ORIGINAL: u16 = 2; +const CTA_PROTOINFO_TCP_WSCALE_REPLY: u16 = 3; +const CTA_PROTOINFO_TCP_FLAGS_ORIGINAL: u16 = 4; +const CTA_PROTOINFO_TCP_FLAGS_REPLY: u16 = 5; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ProtoInfoTCP { + State(u8), + OriginalWindowScale(u8), + ReplyWindowScale(u8), + OriginalFlags(TCPFlags), + ReplyFlags(TCPFlags), + Other(DefaultNla), +} +impl Nla for ProtoInfoTCP { + fn value_len(&self) -> usize { + match self { + ProtoInfoTCP::State(attr) => size_of_val(attr), + ProtoInfoTCP::OriginalWindowScale(attr) => size_of_val(attr), + ProtoInfoTCP::ReplyWindowScale(attr) => size_of_val(attr), + ProtoInfoTCP::OriginalFlags(attr) => attr.buffer_len(), + ProtoInfoTCP::ReplyFlags(attr) => attr.buffer_len(), + ProtoInfoTCP::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + ProtoInfoTCP::State(_) => CTA_PROTOINFO_TCP_STATE, + ProtoInfoTCP::OriginalWindowScale(_) => { + CTA_PROTOINFO_TCP_WSCALE_ORIGINAL + } + ProtoInfoTCP::ReplyWindowScale(_) => CTA_PROTOINFO_TCP_WSCALE_REPLY, + ProtoInfoTCP::OriginalFlags(_) => CTA_PROTOINFO_TCP_FLAGS_ORIGINAL, + ProtoInfoTCP::ReplyFlags(_) => CTA_PROTOINFO_TCP_FLAGS_REPLY, + ProtoInfoTCP::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + ProtoInfoTCP::State(attr) => buffer[0] = *attr, + ProtoInfoTCP::OriginalWindowScale(attr) => buffer[0] = *attr, + ProtoInfoTCP::ReplyWindowScale(attr) => buffer[0] = *attr, + ProtoInfoTCP::OriginalFlags(attr) => attr.emit(buffer), + ProtoInfoTCP::ReplyFlags(attr) => attr.emit(buffer), + ProtoInfoTCP::Other(attr) => attr.emit_value(buffer), + } + } +} +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for ProtoInfoTCP +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_PROTOINFO_TCP_STATE => ProtoInfoTCP::State( + parse_u8(payload) + .context("invalid CTA_PROTOINFO_TCP_STATE value")?, + ), + CTA_PROTOINFO_TCP_WSCALE_ORIGINAL => { + ProtoInfoTCP::OriginalWindowScale(parse_u8(payload).context( + "invalid CTA_PROTOINFO_TCP_WSCALE_ORIGINAL value", + )?) + } + CTA_PROTOINFO_TCP_WSCALE_REPLY => ProtoInfoTCP::ReplyWindowScale( + parse_u8(payload) + .context("invalid CTA_PROTOINFO_TCP_WSCALE_REPLY value")?, + ), + CTA_PROTOINFO_TCP_FLAGS_ORIGINAL => ProtoInfoTCP::OriginalFlags( + TCPFlags::parse(&TCPFlagsBuffer::new(payload)).context( + "invalid CTA_PROTOINFO_TCP_FLAGS_ORIGINAL value", + )?, + ), + CTA_PROTOINFO_TCP_FLAGS_REPLY => ProtoInfoTCP::ReplyFlags( + TCPFlags::parse(&TCPFlagsBuffer::new(payload)) + .context("invalid CTA_PROTOINFO_TCP_FLAGS_REPLY value")?, + ), + _ => ProtoInfoTCP::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/attributes/prototuple.rs b/src/conntrack/attributes/prototuple.rs new file mode 100644 index 0000000..4486019 --- /dev/null +++ b/src/conntrack/attributes/prototuple.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + emit_u16_be, parse_u16_be, parse_u8, DecodeError, DefaultNla, ErrorContext, + Nla, NlaBuffer, Parseable, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Protocol { + Icmp, + Igmp, + IpIp, + Tcp, + Udp, + Ipv6Icmp, + Gre, + UdpLite, + Sctp, + Dccp, + Other(u8), +} + +impl From for u8 { + fn from(protocol: Protocol) -> Self { + use libc::*; + match protocol { + Protocol::Icmp => IPPROTO_ICMP as u8, + Protocol::Igmp => IPPROTO_IGMP as u8, + Protocol::IpIp => IPPROTO_IPIP as u8, + Protocol::Tcp => IPPROTO_TCP as u8, + Protocol::Udp => IPPROTO_UDP as u8, + Protocol::Ipv6Icmp => IPPROTO_ICMPV6 as u8, + Protocol::Gre => IPPROTO_GRE as u8, + Protocol::UdpLite => IPPROTO_UDPLITE as u8, + Protocol::Sctp => IPPROTO_SCTP as u8, + Protocol::Dccp => IPPROTO_DCCP as u8, + Protocol::Other(p) => p, + } + } +} + +impl From for Protocol { + fn from(protocol_num: u8) -> Self { + use libc::*; + match protocol_num as i32 { + IPPROTO_ICMP => Protocol::Icmp, + IPPROTO_IGMP => Protocol::Igmp, + IPPROTO_IPIP => Protocol::IpIp, + IPPROTO_TCP => Protocol::Tcp, + IPPROTO_UDP => Protocol::Udp, + IPPROTO_ICMPV6 => Protocol::Ipv6Icmp, + IPPROTO_GRE => Protocol::Gre, + IPPROTO_UDPLITE => Protocol::UdpLite, + IPPROTO_SCTP => Protocol::Sctp, + IPPROTO_DCCP => Protocol::Dccp, + _ => Protocol::Other(protocol_num), + } + } +} + +const CTA_PROTO_NUM: u16 = 1; +const CTA_PROTO_SRC_PORT: u16 = 2; +const CTA_PROTO_DST_PORT: u16 = 3; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ProtoTuple { + Protocol(Protocol), + SourcePort(u16), + DestinationPort(u16), + Other(DefaultNla), +} + +impl Nla for ProtoTuple { + fn value_len(&self) -> usize { + match self { + ProtoTuple::Protocol(_) => size_of::(), + ProtoTuple::SourcePort(attr) => size_of_val(attr), + ProtoTuple::DestinationPort(attr) => size_of_val(attr), + ProtoTuple::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + ProtoTuple::Protocol(_) => CTA_PROTO_NUM, + ProtoTuple::SourcePort(_) => CTA_PROTO_SRC_PORT, + ProtoTuple::DestinationPort(_) => CTA_PROTO_DST_PORT, + ProtoTuple::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + ProtoTuple::Protocol(attr) => buffer[0] = (*attr).into(), + ProtoTuple::SourcePort(attr) => emit_u16_be(buffer, *attr).unwrap(), + ProtoTuple::DestinationPort(attr) => { + emit_u16_be(buffer, *attr).unwrap() + } + ProtoTuple::Other(attr) => attr.emit_value(buffer), + } + } +} + +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for ProtoTuple +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_PROTO_NUM => ProtoTuple::Protocol( + parse_u8(payload) + .context("invalid CTA_PROTO_NUM value")? + .into(), + ), + CTA_PROTO_SRC_PORT => ProtoTuple::SourcePort( + parse_u16_be(payload) + .context("invalid CTA_PROTO_SRC_PORT value")?, + ), + CTA_PROTO_DST_PORT => ProtoTuple::DestinationPort( + parse_u16_be(payload) + .context("invalid CTA_PROTO_DST_PORT value")?, + ), + _ => ProtoTuple::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/attributes/tcp_flags.rs b/src/conntrack/attributes/tcp_flags.rs new file mode 100644 index 0000000..6b10831 --- /dev/null +++ b/src/conntrack/attributes/tcp_flags.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + buffer, fields, getter, setter, DecodeError, Emitable, Parseable, +}; + +const TCP_FLAGS_LEN: usize = 2; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct TCPFlags { + pub flags: u8, + pub mask: u8, +} + +buffer!(TCPFlagsBuffer(TCP_FLAGS_LEN) { + flags: (u8, 0), + mask: (u8, 1), +}); + +impl> Parseable> for TCPFlags { + fn parse(buf: &TCPFlagsBuffer) -> Result { + Ok(TCPFlags { + flags: buf.flags(), + mask: buf.mask(), + }) + } +} + +impl Emitable for TCPFlags { + fn buffer_len(&self) -> usize { + TCP_FLAGS_LEN + } + + fn emit(&self, buffer: &mut [u8]) { + let mut buffer = TCPFlagsBuffer::new(buffer); + buffer.set_flags(self.flags); + buffer.set_mask(self.mask); + } +} diff --git a/src/conntrack/attributes/tuple.rs b/src/conntrack/attributes/tuple.rs new file mode 100644 index 0000000..169e29c --- /dev/null +++ b/src/conntrack/attributes/tuple.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, + NlasIterator, Parseable, +}; + +use crate::conntrack::attributes::{iptuple::IPTuple, prototuple::ProtoTuple}; + +const CTA_TUPLE_IP: u16 = 1; +const CTA_TUPLE_PROTO: u16 = 2; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum Tuple { + Ip(Vec), + Proto(Vec), + Other(DefaultNla), +} + +impl Nla for Tuple { + fn value_len(&self) -> usize { + match self { + Tuple::Ip(nlas) => nlas.iter().map(|op| op.buffer_len()).sum(), + Tuple::Proto(nlas) => nlas.iter().map(|op| op.buffer_len()).sum(), + Tuple::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Tuple::Ip(_) => CTA_TUPLE_IP, + Tuple::Proto(_) => CTA_TUPLE_PROTO, + Tuple::Other(attr) => attr.kind(), + } + } + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Tuple::Ip(nlas) => { + let mut len = 0; + for op in nlas { + op.emit(&mut buffer[len..]); + len += op.buffer_len(); + } + } + Tuple::Proto(nlas) => { + let mut len = 0; + for op in nlas { + op.emit(&mut buffer[len..]); + len += op.buffer_len(); + } + } + Tuple::Other(attr) => attr.emit_value(buffer), + } + } + fn is_nested(&self) -> bool { + matches!(self, Tuple::Ip(_) | Tuple::Proto(_)) + } +} +impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> + for Tuple +{ + fn parse(buf: &NlaBuffer<&'buffer T>) -> Result { + let kind = buf.kind(); + let payload = buf.value(); + let nla = match kind { + CTA_TUPLE_IP => { + let mut ip_tuples = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = &nlas.context("invalid CTA_TUPLE_IP value")?; + ip_tuples.push(IPTuple::parse(nlas)?); + } + Tuple::Ip(ip_tuples) + } + CTA_TUPLE_PROTO => { + let mut proto_tuples = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = + &nlas.context("invalid CTA_TUPLE_PROTO value")?; + proto_tuples.push(ProtoTuple::parse(nlas)?); + } + Tuple::Proto(proto_tuples) + } + _ => Tuple::Other(DefaultNla::parse(buf)?), + }; + Ok(nla) + } +} diff --git a/src/conntrack/message.rs b/src/conntrack/message.rs new file mode 100644 index 0000000..de6bee5 --- /dev/null +++ b/src/conntrack/message.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +use crate::{buffer::NetfilterBuffer, conntrack::attributes::ConntrackNla}; +use netlink_packet_core::{ + DecodeError, DefaultNla, Emitable, Parseable, ParseableParametrized, +}; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum ConntrackMessage { + Get(Vec), + Other { + message_type: u8, + attributes: Vec, + }, +} + +const IPCTNL_MSG_CT_GET: u8 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConntrackMessageType { + Get, + Other(u8), +} + +impl From for ConntrackMessageType { + fn from(value: u8) -> Self { + match value { + IPCTNL_MSG_CT_GET => Self::Get, + v => Self::Other(v), + } + } +} + +impl From for u8 { + fn from(value: ConntrackMessageType) -> Self { + match value { + ConntrackMessageType::Get => IPCTNL_MSG_CT_GET, + ConntrackMessageType::Other(v) => v, + } + } +} + +impl ConntrackMessage { + pub fn message_type(&self) -> ConntrackMessageType { + match self { + ConntrackMessage::Get(_) => ConntrackMessageType::Get, + ConntrackMessage::Other { message_type, .. } => { + (*message_type).into() + } + } + } +} + +impl Emitable for ConntrackMessage { + fn buffer_len(&self) -> usize { + match self { + ConntrackMessage::Get(attributes) => { + attributes.as_slice().buffer_len() + } + ConntrackMessage::Other { attributes, .. } => { + attributes.as_slice().buffer_len() + } + } + } + + fn emit(&self, buffer: &mut [u8]) { + match self { + ConntrackMessage::Get(attributes) => { + attributes.as_slice().emit(buffer) + } + ConntrackMessage::Other { attributes, .. } => { + attributes.as_slice().emit(buffer) + } + }; + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> + ParseableParametrized, u8> for ConntrackMessage +{ + fn parse_with_param( + buf: &NetfilterBuffer<&'a T>, + message_type: u8, + ) -> Result { + Ok(match ConntrackMessageType::from(message_type) { + ConntrackMessageType::Get => { + let attributes = buf + .parse_all_nlas(|nla_buf| ConntrackNla::parse(&nla_buf))?; + ConntrackMessage::Get(attributes) + } + ConntrackMessageType::Other(message_type) => { + ConntrackMessage::Other { + message_type, + attributes: buf.default_nlas()?, + } + } + }) + } +} diff --git a/src/conntrack/mod.rs b/src/conntrack/mod.rs new file mode 100644 index 0000000..5d19102 --- /dev/null +++ b/src/conntrack/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +mod message; +pub use message::{ConntrackMessage, ConntrackMessageType}; +mod attributes; +pub use attributes::{ + ConntrackNla, IPTuple, ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, + TCPFlags, Tuple, +}; diff --git a/src/constants.rs b/src/constants.rs index 7eabf9c..15b3a8e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -43,10 +43,8 @@ pub const AF_ALG: u8 = libc::AF_ALG as u8; pub const NFNETLINK_V0: u8 = libc::NFNETLINK_V0 as u8; pub const NFNL_SUBSYS_NONE: u8 = libc::NFNL_SUBSYS_NONE as u8; -pub const NFNL_SUBSYS_CTNETLINK: u8 = libc::NFNL_SUBSYS_CTNETLINK as u8; pub const NFNL_SUBSYS_CTNETLINK_EXP: u8 = libc::NFNL_SUBSYS_CTNETLINK_EXP as u8; pub const NFNL_SUBSYS_QUEUE: u8 = libc::NFNL_SUBSYS_QUEUE as u8; -pub const NFNL_SUBSYS_ULOG: u8 = libc::NFNL_SUBSYS_ULOG as u8; pub const NFNL_SUBSYS_OSF: u8 = libc::NFNL_SUBSYS_OSF as u8; pub const NFNL_SUBSYS_IPSET: u8 = libc::NFNL_SUBSYS_IPSET as u8; pub const NFNL_SUBSYS_ACCT: u8 = libc::NFNL_SUBSYS_ACCT as u8; @@ -83,6 +81,3 @@ pub const NFULA_HWHEADER: u16 = libc::NFULA_HWHEADER as u16; pub const NFULA_HWLEN: u16 = libc::NFULA_HWLEN as u16; pub const NFULA_CT: u16 = libc::NFULA_CT as u16; pub const NFULA_CT_INFO: u16 = libc::NFULA_CT_INFO as u16; - -pub const NFULNL_MSG_CONFIG: u8 = libc::NFULNL_MSG_CONFIG as u8; -pub const NFULNL_MSG_PACKET: u8 = libc::NFULNL_MSG_PACKET as u8; diff --git a/src/lib.rs b/src/lib.rs index aca295b..11f508b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,10 @@ pub(crate) mod buffer; pub mod constants; mod message; -pub use message::{NetfilterHeader, NetfilterMessage, NetfilterMessageInner}; +pub use message::{ + NetfilterHeader, NetfilterMessage, NetfilterMessageInner, Subsystem, +}; +pub mod conntrack; pub mod nflog; +#[cfg(test)] +mod tests; diff --git a/src/message.rs b/src/message.rs index 585b534..d4e5b0e 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,9 +6,11 @@ use netlink_packet_core::{ Parseable, ParseableParametrized, }; -use crate::{buffer::NetfilterBuffer, nflog::NfLogMessage}; +use crate::{ + buffer::NetfilterBuffer, conntrack::ConntrackMessage, nflog::NfLogMessage, +}; -pub const NETFILTER_HEADER_LEN: usize = 4; +pub(crate) const NETFILTER_HEADER_LEN: usize = 4; buffer!(NetfilterHeaderBuffer(NETFILTER_HEADER_LEN) { family: (u8, 0), @@ -17,6 +19,7 @@ buffer!(NetfilterHeaderBuffer(NETFILTER_HEADER_LEN) { }); #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct NetfilterHeader { pub family: u8, pub version: u8, @@ -57,13 +60,46 @@ impl> Parseable> for NetfilterHeader { } } +const NFNL_SUBSYS_CTNETLINK: u8 = libc::NFNL_SUBSYS_CTNETLINK as u8; +const NFNL_SUBSYS_ULOG: u8 = libc::NFNL_SUBSYS_ULOG as u8; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum Subsystem { + NfLog, + Conntrack, + Other(u8), +} + +impl From for Subsystem { + fn from(value: u8) -> Self { + match value { + NFNL_SUBSYS_ULOG => Self::NfLog, + NFNL_SUBSYS_CTNETLINK => Self::Conntrack, + v => Self::Other(v), + } + } +} + +impl From for u8 { + fn from(value: Subsystem) -> Self { + match value { + Subsystem::NfLog => NFNL_SUBSYS_ULOG, + Subsystem::Conntrack => NFNL_SUBSYS_CTNETLINK, + Subsystem::Other(v) => v, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] pub enum NetfilterMessageInner { NfLog(NfLogMessage), + Conntrack(ConntrackMessage), Other { - subsys: u8, + subsys: Subsystem, message_type: u8, - nlas: Vec, + attributes: Vec, }, } @@ -72,13 +108,19 @@ impl From for NetfilterMessageInner { Self::NfLog(message) } } +impl From for NetfilterMessageInner { + fn from(message: ConntrackMessage) -> Self { + Self::Conntrack(message) + } +} impl Emitable for NetfilterMessageInner { fn buffer_len(&self) -> usize { match self { NetfilterMessageInner::NfLog(message) => message.buffer_len(), - NetfilterMessageInner::Other { nlas, .. } => { - nlas.as_slice().buffer_len() + NetfilterMessageInner::Conntrack(message) => message.buffer_len(), + NetfilterMessageInner::Other { attributes, .. } => { + attributes.as_slice().buffer_len() } } } @@ -86,14 +128,16 @@ impl Emitable for NetfilterMessageInner { fn emit(&self, buffer: &mut [u8]) { match self { NetfilterMessageInner::NfLog(message) => message.emit(buffer), - NetfilterMessageInner::Other { nlas, .. } => { - nlas.as_slice().emit(buffer) + NetfilterMessageInner::Conntrack(message) => message.emit(buffer), + NetfilterMessageInner::Other { attributes, .. } => { + attributes.as_slice().emit(buffer) } } } } #[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] pub struct NetfilterMessage { pub header: NetfilterHeader, pub inner: NetfilterMessageInner, @@ -110,16 +154,22 @@ impl NetfilterMessage { } } - pub fn subsys(&self) -> u8 { + pub fn subsys(&self) -> Subsystem { match self.inner { - NetfilterMessageInner::NfLog(_) => NfLogMessage::SUBSYS, + NetfilterMessageInner::NfLog(_) => Subsystem::NfLog, + NetfilterMessageInner::Conntrack(_) => Subsystem::Conntrack, NetfilterMessageInner::Other { subsys, .. } => subsys, } } - pub fn message_type(&self) -> u8 { + fn message_type(&self) -> u8 { match self.inner { - NetfilterMessageInner::NfLog(ref message) => message.message_type(), + NetfilterMessageInner::NfLog(ref message) => { + message.message_type().into() + } + NetfilterMessageInner::Conntrack(ref message) => { + message.message_type().into() + } NetfilterMessageInner::Other { message_type, .. } => message_type, } } @@ -138,7 +188,7 @@ impl Emitable for NetfilterMessage { impl NetlinkSerializable for NetfilterMessage { fn message_type(&self) -> u16 { - ((self.subsys() as u16) << 8) | self.message_type() as u16 + ((u8::from(self.subsys()) as u16) << 8) | self.message_type() as u16 } fn buffer_len(&self) -> usize { diff --git a/src/nflog/message.rs b/src/nflog/message.rs index 9b8f464..696f562 100644 --- a/src/nflog/message.rs +++ b/src/nflog/message.rs @@ -6,7 +6,6 @@ use netlink_packet_core::{ use crate::{ buffer::NetfilterBuffer, - constants::{NFNL_SUBSYS_ULOG, NFULNL_MSG_CONFIG, NFULNL_MSG_PACKET}, nflog::nlas::{config::ConfigNla, packet::PacketNla}, }; @@ -20,14 +19,43 @@ pub enum NfLogMessage { }, } -impl NfLogMessage { - pub const SUBSYS: u8 = NFNL_SUBSYS_ULOG; +const NFULNL_MSG_CONFIG: u8 = libc::NFULNL_MSG_CONFIG as u8; +const NFULNL_MSG_PACKET: u8 = libc::NFULNL_MSG_PACKET as u8; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum NfLogMessageType { + Config, + Packet, + Other(u8), +} + +impl From for NfLogMessageType { + fn from(value: u8) -> Self { + match value { + NFULNL_MSG_CONFIG => Self::Config, + NFULNL_MSG_PACKET => Self::Packet, + v => Self::Other(v), + } + } +} + +impl From for u8 { + fn from(value: NfLogMessageType) -> Self { + match value { + NfLogMessageType::Config => NFULNL_MSG_CONFIG, + NfLogMessageType::Packet => NFULNL_MSG_PACKET, + NfLogMessageType::Other(v) => v, + } + } +} - pub fn message_type(&self) -> u8 { +impl NfLogMessage { + pub fn message_type(&self) -> NfLogMessageType { match self { - NfLogMessage::Config(_) => NFULNL_MSG_CONFIG, - NfLogMessage::Packet(_) => NFULNL_MSG_PACKET, - NfLogMessage::Other { message_type, .. } => *message_type, + NfLogMessage::Config(_) => NfLogMessageType::Config, + NfLogMessage::Packet(_) => NfLogMessageType::Packet, + NfLogMessage::Other { message_type, .. } => (*message_type).into(), } } } @@ -57,18 +85,18 @@ impl<'a, T: AsRef<[u8]> + ?Sized> buf: &NetfilterBuffer<&'a T>, message_type: u8, ) -> Result { - Ok(match message_type { - NFULNL_MSG_CONFIG => { + Ok(match NfLogMessageType::from(message_type) { + NfLogMessageType::Config => { let nlas = buf.parse_all_nlas(|nla_buf| ConfigNla::parse(&nla_buf))?; NfLogMessage::Config(nlas) } - NFULNL_MSG_PACKET => { + NfLogMessageType::Packet => { let nlas = buf.parse_all_nlas(|nla_buf| PacketNla::parse(&nla_buf))?; NfLogMessage::Packet(nlas) } - _ => NfLogMessage::Other { + NfLogMessageType::Other(message_type) => NfLogMessage::Other { message_type, nlas: buf.default_nlas()?, }, diff --git a/src/nflog/mod.rs b/src/nflog/mod.rs index d3d39fc..5965029 100644 --- a/src/nflog/mod.rs +++ b/src/nflog/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT mod message; -pub use message::NfLogMessage; +pub use message::{NfLogMessage, NfLogMessageType}; pub mod nlas; use netlink_packet_core::{ diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..9f93cb4 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +use std::net::IpAddr; + +use netlink_packet_core::{Emitable, ParseableParametrized}; + +use crate::{ + buffer::NetfilterBuffer, + conntrack::{ + ConntrackMessage, ConntrackMessageType, ConntrackNla, IPTuple, + ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, TCPFlags, Tuple, + }, + constants::{AF_INET, AF_INET6, AF_UNSPEC}, + message::Subsystem, + NetfilterHeader, NetfilterMessage, +}; + +// wireshark capture of nlmon against command (netlink message header removed): +// conntrack -L +#[test] +fn test_dump_conntrack() { + let raw: Vec = vec![0x00, 0x00, 0x00, 0x00]; + + let expected: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(AF_UNSPEC, 0, 0), + ConntrackMessage::Get(vec![]), + ); + + let mut buffer = vec![0; expected.buffer_len()]; + expected.emit(&mut buffer); + + // Check if the serialization was correct + assert_eq!(buffer, raw); + + let message_type = ((u8::from(Subsystem::Conntrack) as u16) << 8) + | (u8::from(ConntrackMessageType::Get) as u16); + // Check if the deserialization was correct + assert_eq!( + NetfilterMessage::parse_with_param( + &NetfilterBuffer::new(&raw), + message_type + ) + .unwrap(), + expected + ); +} + +// wireshark capture of nlmon against command (netlink message header removed): +// conntrack -G -p tcp -s 10.57.97.124 -d 148.113.20.105 --sport 39600 --dport +// 443 +#[test] +fn test_get_conntrack_tcp_ipv4() { + let raw: Vec = vec![ + 0x02, 0x00, 0x00, 0x00, 0x34, 0x00, 0x01, 0x80, 0x14, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0a, 0x39, 0x61, 0x7c, 0x08, 0x00, 0x02, 0x00, + 0x94, 0x71, 0x14, 0x69, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x9a, 0xb0, 0x00, 0x00, + 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x18, 0x00, 0x04, 0x80, + 0x14, 0x00, 0x01, 0x80, 0x06, 0x00, 0x04, 0x00, 0x0a, 0x0a, 0x00, 0x00, + 0x06, 0x00, 0x05, 0x00, 0x0a, 0x0a, 0x00, 0x00, + ]; + + let src_addr = + IPTuple::SourceAddress(IpAddr::V4("10.57.97.124".parse().unwrap())); + let dst_addr = IPTuple::DestinationAddress(IpAddr::V4( + "148.113.20.105".parse().unwrap(), + )); + + let proto_num = ProtoTuple::Protocol(Protocol::Tcp); + let src_port = ProtoTuple::SourcePort(39600); + let dst_port = ProtoTuple::DestinationPort(443); + + let ip_tuple = Tuple::Ip(vec![src_addr, dst_addr]); + let proto_tuple = Tuple::Proto(vec![proto_num, src_port, dst_port]); + + let proto_info = ProtoInfo::TCP(vec![ + ProtoInfoTCP::OriginalFlags(TCPFlags { + flags: 10, + mask: 10, + }), + ProtoInfoTCP::ReplyFlags(TCPFlags { + flags: 10, + mask: 10, + }), + ]); + + let attributes = vec![ + ConntrackNla::CtaTupleOrig(vec![ip_tuple, proto_tuple]), + ConntrackNla::CtaProtoInfo(vec![proto_info]), + ]; + + let expected: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(AF_INET, 0, 0), + ConntrackMessage::Get(attributes), + ); + + let mut buffer = vec![0; expected.buffer_len()]; + expected.emit(&mut buffer); + + // Check if the serialization was correct + assert_eq!(buffer, raw); + + let message_type = ((u8::from(Subsystem::Conntrack) as u16) << 8) + | (u8::from(ConntrackMessageType::Get) as u16); + // Check if the deserialization was correct + assert_eq!( + NetfilterMessage::parse_with_param( + &NetfilterBuffer::new(&raw), + message_type + ) + .unwrap(), + expected + ); +} + +// wireshark capture of nlmon against command (netlink message header removed): +// conntrack -G -p udp -s 2409:40c4:e8:6bc3:d1d8:1087:4fa2:68a3 --sport 58456 -d +// 2404:6800:4009:81d::200e --dport 443 +#[test] +fn test_get_conntrack_udp_ipv6() { + let raw: Vec = vec![ + 0x0a, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x01, 0x80, 0x2c, 0x00, 0x01, 0x80, + 0x14, 0x00, 0x03, 0x00, 0x24, 0x09, 0x40, 0xc4, 0x00, 0xe8, 0x6b, 0xc3, + 0xd1, 0xd8, 0x10, 0x87, 0x4f, 0xa2, 0x68, 0xa3, 0x14, 0x00, 0x04, 0x00, + 0x24, 0x04, 0x68, 0x00, 0x40, 0x09, 0x08, 0x1d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x0e, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x11, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0xe4, 0x58, 0x00, 0x00, + 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, + ]; + + let src_addr = IPTuple::SourceAddress(IpAddr::V6( + "2409:40c4:e8:6bc3:d1d8:1087:4fa2:68a3".parse().unwrap(), + )); + let dst_addr = IPTuple::DestinationAddress(IpAddr::V6( + "2404:6800:4009:81d::200e".parse().unwrap(), + )); + + let proto_num = ProtoTuple::Protocol(Protocol::Udp); + let src_port = ProtoTuple::SourcePort(58456); + let dst_port = ProtoTuple::DestinationPort(443); + + let ip_tuple = Tuple::Ip(vec![src_addr, dst_addr]); + let proto_tuple = Tuple::Proto(vec![proto_num, src_port, dst_port]); + + let attributes = + vec![ConntrackNla::CtaTupleOrig(vec![ip_tuple, proto_tuple])]; + + let expected: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(AF_INET6, 0, 0), + ConntrackMessage::Get(attributes), + ); + + let mut buffer = vec![0; expected.buffer_len()]; + expected.emit(&mut buffer); + + // Check if the serialization was correct + assert_eq!(buffer, raw); + + let message_type = ((u8::from(Subsystem::Conntrack) as u16) << 8) + | (u8::from(ConntrackMessageType::Get) as u16); + // Check if the deserialization was correct + assert_eq!( + NetfilterMessage::parse_with_param( + &NetfilterBuffer::new(&raw), + message_type + ) + .unwrap(), + expected + ); +}