From 61dbec7403a351f75c4709f2a3ba848301c3411b Mon Sep 17 00:00:00 2001 From: Shivang K Raghuvanshi Date: Fri, 3 Oct 2025 16:15:47 +0530 Subject: [PATCH 1/3] feat: conntrack get netlink netfilter message types Implemented the following attributes required to successfully construct a conntrack get request: * iptuple * protoinfo * protoinfotcp * prototuple * tcp_flags * tuple Signed-off-by: Shivang K Raghuvanshi --- src/buffer.rs | 7 +- src/conntrack/attributes/attribute.rs | 96 +++++++++++++ src/conntrack/attributes/iptuple.rs | 91 ++++++++++++ src/conntrack/attributes/mod.rs | 17 +++ src/conntrack/attributes/protoinfo.rs | 69 +++++++++ src/conntrack/attributes/protoinfotcp.rs | 95 +++++++++++++ src/conntrack/attributes/prototuple.rs | 130 +++++++++++++++++ src/conntrack/attributes/tcp_flags.rs | 40 ++++++ src/conntrack/attributes/tuple.rs | 88 ++++++++++++ src/conntrack/message.rs | 76 ++++++++++ src/conntrack/mod.rs | 9 ++ src/constants.rs | 2 + src/lib.rs | 3 + src/message.rs | 31 ++++- src/tests.rs | 170 +++++++++++++++++++++++ 15 files changed, 916 insertions(+), 8 deletions(-) create mode 100644 src/conntrack/attributes/attribute.rs create mode 100644 src/conntrack/attributes/iptuple.rs create mode 100644 src/conntrack/attributes/mod.rs create mode 100644 src/conntrack/attributes/protoinfo.rs create mode 100644 src/conntrack/attributes/protoinfotcp.rs create mode 100644 src/conntrack/attributes/prototuple.rs create mode 100644 src/conntrack/attributes/tcp_flags.rs create mode 100644 src/conntrack/attributes/tuple.rs create mode 100644 src/conntrack/message.rs create mode 100644 src/conntrack/mod.rs create mode 100644 src/tests.rs diff --git a/src/buffer.rs b/src/buffer.rs index a9d4917..f2b5073 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT use crate::{ + conntrack::ConntrackMessage, message::{ NetfilterHeader, NetfilterMessage, NetfilterMessageInner, NETFILTER_HEADER_LEN, @@ -57,10 +58,14 @@ impl<'a, T: AsRef<[u8]> + ?Sized> NfLogMessage::parse_with_param(buf, message_type) .context("failed to parse nflog payload")?, ), + ConntrackMessage::SUBSYS => NetfilterMessageInner::Conntrack( + ConntrackMessage::parse_with_param(buf, message_type) + .context("failed to parse conntrack payload")?, + ), _ => NetfilterMessageInner::Other { subsys, 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..221ad17 --- /dev/null +++ b/src/conntrack/message.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +use crate::{ + buffer::NetfilterBuffer, + conntrack::attributes::ConntrackNla, + constants::{IPCTNL_MSG_CT_GET, NFNL_SUBSYS_CTNETLINK}, +}; +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, + }, +} + +impl ConntrackMessage { + pub(crate) const SUBSYS: u8 = NFNL_SUBSYS_CTNETLINK; + + pub fn message_type(&self) -> u8 { + match self { + ConntrackMessage::Get(_) => IPCTNL_MSG_CT_GET, + ConntrackMessage::Other { message_type, .. } => *message_type, + } + } +} + +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 message_type { + IPCTNL_MSG_CT_GET => { + let attributes = buf + .parse_all_nlas(|nla_buf| ConntrackNla::parse(&nla_buf))?; + ConntrackMessage::Get(attributes) + } + _ => 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..cef6f26 --- /dev/null +++ b/src/conntrack/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +mod message; +pub use message::ConntrackMessage; +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..4d2d229 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -86,3 +86,5 @@ 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; + +pub(crate) const IPCTNL_MSG_CT_GET: u8 = 1; diff --git a/src/lib.rs b/src/lib.rs index aca295b..e9cf671 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,7 @@ pub(crate) mod buffer; pub mod constants; mod message; pub use message::{NetfilterHeader, NetfilterMessage, NetfilterMessageInner}; +pub mod conntrack; pub mod nflog; +#[cfg(test)] +mod tests; diff --git a/src/message.rs b/src/message.rs index 585b534..a609ae0 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, @@ -58,12 +61,14 @@ impl> Parseable> for NetfilterHeader { } #[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] pub enum NetfilterMessageInner { NfLog(NfLogMessage), + Conntrack(ConntrackMessage), Other { subsys: u8, message_type: u8, - nlas: Vec, + attributes: Vec, }, } @@ -72,13 +77,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 +97,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, @@ -113,6 +126,7 @@ impl NetfilterMessage { pub fn subsys(&self) -> u8 { match self.inner { NetfilterMessageInner::NfLog(_) => NfLogMessage::SUBSYS, + NetfilterMessageInner::Conntrack(_) => ConntrackMessage::SUBSYS, NetfilterMessageInner::Other { subsys, .. } => subsys, } } @@ -120,6 +134,9 @@ impl NetfilterMessage { pub fn message_type(&self) -> u8 { match self.inner { NetfilterMessageInner::NfLog(ref message) => message.message_type(), + NetfilterMessageInner::Conntrack(ref message) => { + message.message_type() + } NetfilterMessageInner::Other { message_type, .. } => message_type, } } diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..6eaaa01 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +use std::net::IpAddr; + +use libc::NFNL_SUBSYS_CTNETLINK; +use netlink_packet_core::{Emitable, ParseableParametrized}; + +use crate::{ + buffer::NetfilterBuffer, + conntrack::{ + ConntrackMessage, ConntrackNla, IPTuple, ProtoInfo, ProtoInfoTCP, + ProtoTuple, Protocol, TCPFlags, Tuple, + }, + constants::{AF_INET, AF_INET6, AF_UNSPEC, IPCTNL_MSG_CT_GET}, + 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 = + ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_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 = + ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_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 = + ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_GET as u16); + // Check if the deserialization was correct + assert_eq!( + NetfilterMessage::parse_with_param( + &NetfilterBuffer::new(&raw), + message_type + ) + .unwrap(), + expected + ); +} From c9d7dc371467796ef7a6dc95c58b9d489cc80378 Mon Sep 17 00:00:00 2001 From: Shivang K Raghuvanshi Date: Tue, 28 Oct 2025 13:20:37 +0530 Subject: [PATCH 2/3] refactor: use enums for subsystem and message types This refactors the crate to use type-safe enums for netfilter subsystems and message types, for a safer and more idiomatic API. - Introduces a `Subsystem` enum to replace raw `u8` identifiers for `NfLog` and `Conntrack` subsystems. - Introduces `NfLogMessageType` and `ConntrackMessageType` enums to provide type safety for messages within each subsystem. - Makes the top-level `NetfilterMessage::message_type()` function private to guide users towards the safer pattern of matching on `NetfilterMessageInner`. - Updates the internal parsing logic in `buffer.rs` to use the new `Subsystem` enum. Signed-off-by: Shivang K Raghuvanshi --- src/buffer.rs | 12 ++++----- src/conntrack/message.rs | 57 +++++++++++++++++++++++++++++----------- src/conntrack/mod.rs | 2 +- src/constants.rs | 7 ----- src/lib.rs | 4 ++- src/message.rs | 49 ++++++++++++++++++++++++++++------ src/nflog/message.rs | 50 +++++++++++++++++++++++++++-------- src/nflog/mod.rs | 2 +- src/tests.rs | 20 +++++++------- 9 files changed, 142 insertions(+), 61 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index f2b5073..b31084d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3,7 +3,7 @@ use crate::{ conntrack::ConntrackMessage, message::{ - NetfilterHeader, NetfilterMessage, NetfilterMessageInner, + NetfilterHeader, NetfilterMessage, NetfilterMessageInner, Subsystem, NETFILTER_HEADER_LEN, }, nflog::NfLogMessage, @@ -53,17 +53,17 @@ 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")?, ), - ConntrackMessage::SUBSYS => NetfilterMessageInner::Conntrack( + Subsystem::Conntrack => NetfilterMessageInner::Conntrack( ConntrackMessage::parse_with_param(buf, message_type) .context("failed to parse conntrack payload")?, ), - _ => NetfilterMessageInner::Other { - subsys, + subsys_enum @ Subsystem::Other(_) => NetfilterMessageInner::Other { + subsys: subsys_enum, message_type, attributes: buf.default_nlas()?, }, diff --git a/src/conntrack/message.rs b/src/conntrack/message.rs index 221ad17..de6bee5 100644 --- a/src/conntrack/message.rs +++ b/src/conntrack/message.rs @@ -1,10 +1,6 @@ // SPDX-License-Identifier: MIT -use crate::{ - buffer::NetfilterBuffer, - conntrack::attributes::ConntrackNla, - constants::{IPCTNL_MSG_CT_GET, NFNL_SUBSYS_CTNETLINK}, -}; +use crate::{buffer::NetfilterBuffer, conntrack::attributes::ConntrackNla}; use netlink_packet_core::{ DecodeError, DefaultNla, Emitable, Parseable, ParseableParametrized, }; @@ -19,13 +15,40 @@ pub enum ConntrackMessage { }, } -impl ConntrackMessage { - pub(crate) const SUBSYS: u8 = NFNL_SUBSYS_CTNETLINK; +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, + } + } +} - pub fn message_type(&self) -> u8 { +impl ConntrackMessage { + pub fn message_type(&self) -> ConntrackMessageType { match self { - ConntrackMessage::Get(_) => IPCTNL_MSG_CT_GET, - ConntrackMessage::Other { message_type, .. } => *message_type, + ConntrackMessage::Get(_) => ConntrackMessageType::Get, + ConntrackMessage::Other { message_type, .. } => { + (*message_type).into() + } } } } @@ -61,16 +84,18 @@ impl<'a, T: AsRef<[u8]> + ?Sized> buf: &NetfilterBuffer<&'a T>, message_type: u8, ) -> Result { - Ok(match message_type { - IPCTNL_MSG_CT_GET => { + Ok(match ConntrackMessageType::from(message_type) { + ConntrackMessageType::Get => { let attributes = buf .parse_all_nlas(|nla_buf| ConntrackNla::parse(&nla_buf))?; ConntrackMessage::Get(attributes) } - _ => ConntrackMessage::Other { - message_type, - attributes: buf.default_nlas()?, - }, + ConntrackMessageType::Other(message_type) => { + ConntrackMessage::Other { + message_type, + attributes: buf.default_nlas()?, + } + } }) } } diff --git a/src/conntrack/mod.rs b/src/conntrack/mod.rs index cef6f26..5d19102 100644 --- a/src/conntrack/mod.rs +++ b/src/conntrack/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT mod message; -pub use message::ConntrackMessage; +pub use message::{ConntrackMessage, ConntrackMessageType}; mod attributes; pub use attributes::{ ConntrackNla, IPTuple, ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, diff --git a/src/constants.rs b/src/constants.rs index 4d2d229..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,8 +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; - -pub(crate) const IPCTNL_MSG_CT_GET: u8 = 1; diff --git a/src/lib.rs b/src/lib.rs index e9cf671..11f508b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,9 @@ 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)] diff --git a/src/message.rs b/src/message.rs index a609ae0..d4e5b0e 100644 --- a/src/message.rs +++ b/src/message.rs @@ -60,13 +60,44 @@ 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, attributes: Vec, }, @@ -123,19 +154,21 @@ impl NetfilterMessage { } } - pub fn subsys(&self) -> u8 { + pub fn subsys(&self) -> Subsystem { match self.inner { - NetfilterMessageInner::NfLog(_) => NfLogMessage::SUBSYS, - NetfilterMessageInner::Conntrack(_) => ConntrackMessage::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() + message.message_type().into() } NetfilterMessageInner::Other { message_type, .. } => message_type, } @@ -155,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 index 6eaaa01..9f93cb4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,16 +2,16 @@ use std::net::IpAddr; -use libc::NFNL_SUBSYS_CTNETLINK; use netlink_packet_core::{Emitable, ParseableParametrized}; use crate::{ buffer::NetfilterBuffer, conntrack::{ - ConntrackMessage, ConntrackNla, IPTuple, ProtoInfo, ProtoInfoTCP, - ProtoTuple, Protocol, TCPFlags, Tuple, + ConntrackMessage, ConntrackMessageType, ConntrackNla, IPTuple, + ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, TCPFlags, Tuple, }, - constants::{AF_INET, AF_INET6, AF_UNSPEC, IPCTNL_MSG_CT_GET}, + constants::{AF_INET, AF_INET6, AF_UNSPEC}, + message::Subsystem, NetfilterHeader, NetfilterMessage, }; @@ -32,8 +32,8 @@ fn test_dump_conntrack() { // Check if the serialization was correct assert_eq!(buffer, raw); - let message_type = - ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_GET as u16); + 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( @@ -100,8 +100,8 @@ fn test_get_conntrack_tcp_ipv4() { // Check if the serialization was correct assert_eq!(buffer, raw); - let message_type = - ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_GET as u16); + 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( @@ -156,8 +156,8 @@ fn test_get_conntrack_udp_ipv6() { // Check if the serialization was correct assert_eq!(buffer, raw); - let message_type = - ((NFNL_SUBSYS_CTNETLINK as u16) << 8) | (IPCTNL_MSG_CT_GET as u16); + 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( From 0235257516f5dc2d844a8e6b14f7ee6c6f11d7ab Mon Sep 17 00:00:00 2001 From: Shivang K Raghuvanshi Date: Thu, 30 Oct 2025 19:41:10 +0530 Subject: [PATCH 3/3] feat: conntrack delete message type and attributes Implemented the `Delete`/`IPCTNL_MSG_CT_DELETE` conntrack message type and the following attributes, which are required to describe a conntrack entry for deletion: * mark * status * timeout * tuple_reply Added tests for deleting TCP/IPv4 and UDP/IPv6 conntrack entries. Added ProtoFamily, an enum that represents a protocol family in the Netfilter header. The bitflags dependency was also bumped to v2.10.0. Signed-off-by: Shivang K Raghuvanshi --- Cargo.toml | 2 +- examples/nflog.rs | 13 +- src/conntrack/attributes/attribute.rs | 63 +++++++- src/conntrack/attributes/mod.rs | 2 + src/conntrack/attributes/status.rs | 48 ++++++ src/conntrack/message.rs | 17 +++ src/conntrack/mod.rs | 2 +- src/lib.rs | 3 +- src/message.rs | 64 +++++++- src/nflog/mod.rs | 6 +- src/tests.rs | 211 +++++++++++++++++++++++++- 11 files changed, 406 insertions(+), 25 deletions(-) create mode 100644 src/conntrack/attributes/status.rs diff --git a/Cargo.toml b/Cargo.toml index c4b1504..9325e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ description = "netlink packet types for the netfilter subprotocol" [dependencies] netlink-packet-core = { version = "0.8.1" } -bitflags = "2.3" +bitflags = "2.10.0" libc = "0.2.77" derive_more = "0.99.16" diff --git a/examples/nflog.rs b/examples/nflog.rs index c1bf775..aaeabbf 100644 --- a/examples/nflog.rs +++ b/examples/nflog.rs @@ -6,11 +6,10 @@ // 2) build the example: cargo build --example nflog // 3) run it as root: sudo ../target/debug/examples/nflog -use std::{net::Ipv4Addr, time::Duration}; +use std::time::Duration; use netlink_packet_core::{parse_ip, NetlinkMessage, NetlinkPayload}; use netlink_packet_netfilter::{ - constants::*, nflog::{ config_request, nlas::{ @@ -19,7 +18,7 @@ use netlink_packet_netfilter::{ }, NfLogMessage, }, - NetfilterMessage, NetfilterMessageInner, + NetfilterMessage, NetfilterMessageInner, ProtoFamily, }; use netlink_sys::{constants::NETLINK_NETFILTER, Socket}; @@ -43,7 +42,11 @@ fn main() { socket.bind_auto().unwrap(); // Then we issue the PfBind command - let packet = config_request(AF_INET, 0, vec![ConfigCmd::PfBind.into()]); + let packet = config_request( + ProtoFamily::ProtoIPv4, + 0, + vec![ConfigCmd::PfBind.into()], + ); let mut buf = vec![0; packet.header.length as usize]; packet.serialize(&mut buf[..]); println!(">>> {:?}", packet); @@ -64,7 +67,7 @@ fn main() { // also set various parameters at the same time let timeout: Timeout = Duration::from_millis(100).into(); let packet = config_request( - AF_INET, + ProtoFamily::ProtoIPv4, 1, vec![ ConfigCmd::Bind.into(), diff --git a/src/conntrack/attributes/attribute.rs b/src/conntrack/attributes/attribute.rs index 272a903..5200055 100644 --- a/src/conntrack/attributes/attribute.rs +++ b/src/conntrack/attributes/attribute.rs @@ -1,20 +1,30 @@ // SPDX-License-Identifier: MIT use netlink_packet_core::{ - DecodeError, DefaultNla, Emitable, ErrorContext, Nla, NlaBuffer, - NlasIterator, Parseable, + emit_u32_be, parse_u32_be, DecodeError, DefaultNla, Emitable, ErrorContext, + Nla, NlaBuffer, NlasIterator, Parseable, }; -use crate::conntrack::attributes::{protoinfo::ProtoInfo, tuple::Tuple}; +use crate::conntrack::attributes::{ + protoinfo::ProtoInfo, status::Status, tuple::Tuple, +}; const CTA_TUPLE_ORIG: u16 = 1; +const CTA_TUPLE_REPLY: u16 = 2; const CTA_PROTOINFO: u16 = 4; +const CTA_STATUS: u16 = 3; +const CTA_TIMEOUT: u16 = 7; +const CTA_MARK: u16 = 8; #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ConntrackNla { CtaTupleOrig(Vec), + CtaTupleReply(Vec), CtaProtoInfo(Vec), + CtaStatus(Status), + CtaTimeout(u32), + CtaMark(u32), Other(DefaultNla), } @@ -24,9 +34,15 @@ impl Nla for ConntrackNla { ConntrackNla::CtaTupleOrig(attr) => { attr.iter().map(|op| op.buffer_len()).sum() } + ConntrackNla::CtaTupleReply(attr) => { + attr.iter().map(|op| op.buffer_len()).sum() + } ConntrackNla::CtaProtoInfo(attr) => { attr.iter().map(|op| op.buffer_len()).sum() } + ConntrackNla::CtaStatus(_) => size_of::(), + ConntrackNla::CtaTimeout(attr) => size_of_val(attr), + ConntrackNla::CtaMark(attr) => size_of_val(attr), ConntrackNla::Other(attr) => attr.value_len(), } } @@ -34,7 +50,11 @@ impl Nla for ConntrackNla { fn kind(&self) -> u16 { match self { ConntrackNla::CtaTupleOrig(_) => CTA_TUPLE_ORIG, + ConntrackNla::CtaTupleReply(_) => CTA_TUPLE_REPLY, ConntrackNla::CtaProtoInfo(_) => CTA_PROTOINFO, + ConntrackNla::CtaStatus(_) => CTA_STATUS, + ConntrackNla::CtaTimeout(_) => CTA_TIMEOUT, + ConntrackNla::CtaMark(_) => CTA_MARK, ConntrackNla::Other(attr) => attr.kind(), } } @@ -48,6 +68,13 @@ impl Nla for ConntrackNla { len += op.buffer_len(); } } + ConntrackNla::CtaTupleReply(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 { @@ -55,13 +82,22 @@ impl Nla for ConntrackNla { len += op.buffer_len(); } } + ConntrackNla::CtaStatus(attr) => { + emit_u32_be(buffer, (*attr).bits()).unwrap() + } + ConntrackNla::CtaTimeout(attr) => { + emit_u32_be(buffer, *attr).unwrap() + } + ConntrackNla::CtaMark(attr) => emit_u32_be(buffer, *attr).unwrap(), ConntrackNla::Other(attr) => attr.emit_value(buffer), } } fn is_nested(&self) -> bool { matches!( self, - ConntrackNla::CtaTupleOrig(_) | ConntrackNla::CtaProtoInfo(_) + ConntrackNla::CtaTupleOrig(_) + | ConntrackNla::CtaTupleReply(_) + | ConntrackNla::CtaProtoInfo(_) ) } } @@ -81,6 +117,16 @@ impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> } ConntrackNla::CtaTupleOrig(tuples) } + CTA_TUPLE_REPLY => { + let mut tuples = Vec::new(); + for nlas in NlasIterator::new(payload) { + let nlas = + &nlas.context("invalid CTA_TUPLE_REPLY value")?; + + tuples.push(Tuple::parse(nlas)?); + } + ConntrackNla::CtaTupleReply(tuples) + } CTA_PROTOINFO => { let mut proto_infos = Vec::new(); for nlas in NlasIterator::new(payload) { @@ -89,6 +135,15 @@ impl<'buffer, T: AsRef<[u8]> + ?Sized> Parseable> } ConntrackNla::CtaProtoInfo(proto_infos) } + CTA_STATUS => ConntrackNla::CtaStatus(Status::from_bits_retain( + parse_u32_be(payload).context("invalid CTA_STATUS value")?, + )), + CTA_TIMEOUT => ConntrackNla::CtaTimeout( + parse_u32_be(payload).context("invalid CTA_TIMEOUT value")?, + ), + CTA_MARK => ConntrackNla::CtaMark( + parse_u32_be(payload).context("invalid CTA_MARK value")?, + ), _ => ConntrackNla::Other(DefaultNla::parse(buf)?), }; Ok(nla) diff --git a/src/conntrack/attributes/mod.rs b/src/conntrack/attributes/mod.rs index 07dfe35..dddb1da 100644 --- a/src/conntrack/attributes/mod.rs +++ b/src/conntrack/attributes/mod.rs @@ -5,6 +5,7 @@ mod iptuple; mod protoinfo; mod protoinfotcp; mod prototuple; +mod status; mod tcp_flags; mod tuple; @@ -13,5 +14,6 @@ pub use iptuple::IPTuple; pub use protoinfo::ProtoInfo; pub use protoinfotcp::ProtoInfoTCP; pub use prototuple::{ProtoTuple, Protocol}; +pub use status::Status; pub use tcp_flags::TCPFlags; pub use tuple::Tuple; diff --git a/src/conntrack/attributes/status.rs b/src/conntrack/attributes/status.rs new file mode 100644 index 0000000..3ab5272 --- /dev/null +++ b/src/conntrack/attributes/status.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +use bitflags::bitflags; + +// Conntrack status flags from uapi/linux/netfilter/nf_conntrack_common.h +const IPS_EXPECTED: u32 = 1; +const IPS_SEEN_REPLY: u32 = 1 << 1; +const IPS_ASSURED: u32 = 1 << 2; +const IPS_CONFIRMED: u32 = 1 << 3; +const IPS_SRC_NAT: u32 = 1 << 4; +const IPS_DST_NAT: u32 = 1 << 5; +const IPS_SEQ_ADJUST: u32 = 1 << 6; +const IPS_SRC_NAT_DONE: u32 = 1 << 7; +const IPS_DST_NAT_DONE: u32 = 1 << 8; +const IPS_DYING: u32 = 1 << 9; +const IPS_FIXED_TIMEOUT: u32 = 1 << 10; +const IPS_TEMPLATE: u32 = 1 << 11; +const IPS_UNTRACKED: u32 = 1 << 12; +const IPS_HELPER: u32 = 1 << 13; +const IPS_OFFLOAD: u32 = 1 << 14; + +const IPS_NAT_MASK: u32 = IPS_SRC_NAT | IPS_DST_NAT; +const IPS_NAT_DONE_MASK: u32 = IPS_SRC_NAT_DONE | IPS_DST_NAT_DONE; + +bitflags! { + #[non_exhaustive] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Status: u32 { + const Expected = IPS_EXPECTED; + const SeenReply = IPS_SEEN_REPLY; + const Assured = IPS_ASSURED; + const Confirmed = IPS_CONFIRMED; + const SrcNat = IPS_SRC_NAT; + const DstNat = IPS_DST_NAT; + const SeqAdjust = IPS_SEQ_ADJUST; + const SrcNatDone = IPS_SRC_NAT_DONE; + const DstNatDone = IPS_DST_NAT_DONE; + const Dying = IPS_DYING; + const FixedTimeout = IPS_FIXED_TIMEOUT; + const Template = IPS_TEMPLATE; + const Untracked = IPS_UNTRACKED; + const Helper = IPS_HELPER; + const Offload = IPS_OFFLOAD; + const NatMask = IPS_NAT_MASK; + const NatDoneMask = IPS_NAT_DONE_MASK; + const _ = !0; + } +} diff --git a/src/conntrack/message.rs b/src/conntrack/message.rs index de6bee5..f8680f8 100644 --- a/src/conntrack/message.rs +++ b/src/conntrack/message.rs @@ -9,6 +9,7 @@ use netlink_packet_core::{ #[non_exhaustive] pub enum ConntrackMessage { Get(Vec), + Delete(Vec), Other { message_type: u8, attributes: Vec, @@ -16,11 +17,13 @@ pub enum ConntrackMessage { } const IPCTNL_MSG_CT_GET: u8 = 1; +const IPCTNL_MSG_CT_DELETE: u8 = 2; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum ConntrackMessageType { Get, + Delete, Other(u8), } @@ -28,6 +31,7 @@ impl From for ConntrackMessageType { fn from(value: u8) -> Self { match value { IPCTNL_MSG_CT_GET => Self::Get, + IPCTNL_MSG_CT_DELETE => Self::Delete, v => Self::Other(v), } } @@ -37,6 +41,7 @@ impl From for u8 { fn from(value: ConntrackMessageType) -> Self { match value { ConntrackMessageType::Get => IPCTNL_MSG_CT_GET, + ConntrackMessageType::Delete => IPCTNL_MSG_CT_DELETE, ConntrackMessageType::Other(v) => v, } } @@ -46,6 +51,7 @@ impl ConntrackMessage { pub fn message_type(&self) -> ConntrackMessageType { match self { ConntrackMessage::Get(_) => ConntrackMessageType::Get, + ConntrackMessage::Delete(_) => ConntrackMessageType::Delete, ConntrackMessage::Other { message_type, .. } => { (*message_type).into() } @@ -59,6 +65,9 @@ impl Emitable for ConntrackMessage { ConntrackMessage::Get(attributes) => { attributes.as_slice().buffer_len() } + ConntrackMessage::Delete(attributes) => { + attributes.as_slice().buffer_len() + } ConntrackMessage::Other { attributes, .. } => { attributes.as_slice().buffer_len() } @@ -70,6 +79,9 @@ impl Emitable for ConntrackMessage { ConntrackMessage::Get(attributes) => { attributes.as_slice().emit(buffer) } + ConntrackMessage::Delete(attributes) => { + attributes.as_slice().emit(buffer) + } ConntrackMessage::Other { attributes, .. } => { attributes.as_slice().emit(buffer) } @@ -90,6 +102,11 @@ impl<'a, T: AsRef<[u8]> + ?Sized> .parse_all_nlas(|nla_buf| ConntrackNla::parse(&nla_buf))?; ConntrackMessage::Get(attributes) } + ConntrackMessageType::Delete => { + let nlas = buf + .parse_all_nlas(|nla_buf| ConntrackNla::parse(&nla_buf))?; + ConntrackMessage::Delete(nlas) + } ConntrackMessageType::Other(message_type) => { ConntrackMessage::Other { message_type, diff --git a/src/conntrack/mod.rs b/src/conntrack/mod.rs index 5d19102..049d942 100644 --- a/src/conntrack/mod.rs +++ b/src/conntrack/mod.rs @@ -5,5 +5,5 @@ pub use message::{ConntrackMessage, ConntrackMessageType}; mod attributes; pub use attributes::{ ConntrackNla, IPTuple, ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, - TCPFlags, Tuple, + Status, TCPFlags, Tuple, }; diff --git a/src/lib.rs b/src/lib.rs index 11f508b..d28ff16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,8 @@ pub(crate) mod buffer; pub mod constants; mod message; pub use message::{ - NetfilterHeader, NetfilterMessage, NetfilterMessageInner, Subsystem, + NetfilterHeader, NetfilterMessage, NetfilterMessageInner, ProtoFamily, + Subsystem, }; pub mod conntrack; pub mod nflog; diff --git a/src/message.rs b/src/message.rs index d4e5b0e..e1bff8f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -10,6 +10,62 @@ use crate::{ buffer::NetfilterBuffer, conntrack::ConntrackMessage, nflog::NfLogMessage, }; +// ProtoFamily represents a protocol family in the Netfilter header (nfgenmsg). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ProtoFamily { + ProtoUnspec, + ProtoInet, + ProtoIPv4, + ProtoARP, + ProtoNetDev, + ProtoBridge, + ProtoIPv6, + ProtoDECNet, + Other(u8), +} + +const NFPROTO_UNSPEC: u8 = 0; +const NFPROTO_INET: u8 = 1; +const NFPROTO_IPV4: u8 = 2; +const NFPROTO_ARP: u8 = 3; +const NFPROTO_NETDEV: u8 = 5; +const NFPROTO_BRIDGE: u8 = 7; +const NFPROTO_IPV6: u8 = 10; +const NFPROTO_DECNET: u8 = 12; + +impl From for u8 { + fn from(proto_family: ProtoFamily) -> Self { + match proto_family { + ProtoFamily::ProtoUnspec => NFPROTO_UNSPEC, + ProtoFamily::ProtoInet => NFPROTO_INET, + ProtoFamily::ProtoIPv4 => NFPROTO_IPV4, + ProtoFamily::ProtoARP => NFPROTO_ARP, + ProtoFamily::ProtoNetDev => NFPROTO_NETDEV, + ProtoFamily::ProtoBridge => NFPROTO_BRIDGE, + ProtoFamily::ProtoIPv6 => NFPROTO_IPV6, + ProtoFamily::ProtoDECNet => NFPROTO_DECNET, + ProtoFamily::Other(p) => p, + } + } +} + +impl From for ProtoFamily { + fn from(proto_family_num: u8) -> Self { + match proto_family_num { + NFPROTO_UNSPEC => ProtoFamily::ProtoUnspec, + NFPROTO_INET => ProtoFamily::ProtoInet, + NFPROTO_IPV4 => ProtoFamily::ProtoIPv4, + NFPROTO_ARP => ProtoFamily::ProtoARP, + NFPROTO_NETDEV => ProtoFamily::ProtoNetDev, + NFPROTO_BRIDGE => ProtoFamily::ProtoBridge, + NFPROTO_IPV6 => ProtoFamily::ProtoIPv6, + NFPROTO_DECNET => ProtoFamily::ProtoDECNet, + _ => ProtoFamily::Other(proto_family_num), + } + } +} + pub(crate) const NETFILTER_HEADER_LEN: usize = 4; buffer!(NetfilterHeaderBuffer(NETFILTER_HEADER_LEN) { @@ -21,13 +77,13 @@ buffer!(NetfilterHeaderBuffer(NETFILTER_HEADER_LEN) { #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct NetfilterHeader { - pub family: u8, + pub family: ProtoFamily, pub version: u8, pub res_id: u16, } impl NetfilterHeader { - pub fn new(family: u8, version: u8, res_id: u16) -> Self { + pub fn new(family: ProtoFamily, version: u8, res_id: u16) -> Self { Self { family, version, @@ -43,7 +99,7 @@ impl Emitable for NetfilterHeader { fn emit(&self, buf: &mut [u8]) { let mut buf = NetfilterHeaderBuffer::new(buf); - buf.set_family(self.family); + buf.set_family(self.family.into()); buf.set_version(self.version); buf.set_res_id(self.res_id.to_be()); } @@ -53,7 +109,7 @@ impl> Parseable> for NetfilterHeader { fn parse(buf: &NetfilterHeaderBuffer) -> Result { buf.check_buffer_length()?; Ok(NetfilterHeader { - family: buf.family(), + family: buf.family().into(), version: buf.version(), res_id: u16::from_be(buf.res_id()), }) diff --git a/src/nflog/mod.rs b/src/nflog/mod.rs index 5965029..5f50145 100644 --- a/src/nflog/mod.rs +++ b/src/nflog/mod.rs @@ -9,12 +9,12 @@ use netlink_packet_core::{ }; use crate::{ - constants::NFNETLINK_V0, nflog::nlas::config::ConfigNla, NetfilterHeader, - NetfilterMessage, + constants::NFNETLINK_V0, message::ProtoFamily, + nflog::nlas::config::ConfigNla, NetfilterHeader, NetfilterMessage, }; pub fn config_request( - family: u8, + family: ProtoFamily, group_num: u16, nlas: Vec, ) -> NetlinkMessage { diff --git a/src/tests.rs b/src/tests.rs index 9f93cb4..4305835 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -8,10 +8,9 @@ use crate::{ buffer::NetfilterBuffer, conntrack::{ ConntrackMessage, ConntrackMessageType, ConntrackNla, IPTuple, - ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, TCPFlags, Tuple, + ProtoInfo, ProtoInfoTCP, ProtoTuple, Protocol, Status, TCPFlags, Tuple, }, - constants::{AF_INET, AF_INET6, AF_UNSPEC}, - message::Subsystem, + message::{ProtoFamily, Subsystem}, NetfilterHeader, NetfilterMessage, }; @@ -22,7 +21,7 @@ fn test_dump_conntrack() { let raw: Vec = vec![0x00, 0x00, 0x00, 0x00]; let expected: NetfilterMessage = NetfilterMessage::new( - NetfilterHeader::new(AF_UNSPEC, 0, 0), + NetfilterHeader::new(ProtoFamily::ProtoUnspec, 0, 0), ConntrackMessage::Get(vec![]), ); @@ -90,7 +89,7 @@ fn test_get_conntrack_tcp_ipv4() { ]; let expected: NetfilterMessage = NetfilterMessage::new( - NetfilterHeader::new(AF_INET, 0, 0), + NetfilterHeader::new(ProtoFamily::ProtoIPv4, 0, 0), ConntrackMessage::Get(attributes), ); @@ -146,7 +145,7 @@ fn test_get_conntrack_udp_ipv6() { vec![ConntrackNla::CtaTupleOrig(vec![ip_tuple, proto_tuple])]; let expected: NetfilterMessage = NetfilterMessage::new( - NetfilterHeader::new(AF_INET6, 0, 0), + NetfilterHeader::new(ProtoFamily::ProtoIPv6, 0, 0), ConntrackMessage::Get(attributes), ); @@ -168,3 +167,203 @@ fn test_get_conntrack_udp_ipv6() { expected ); } + +// wireshark capture of nlmon against command (netlink message header removed): +// conntrack -D -f ipv4 -p tcp --src 10.255.160.124 --sport 39640 --dst +// 140.82.113.26 --dport 443 NOTE: For filtered deletions, conntrack-tools +// issues a dump request to retrieve all conntrack entries, filters them in +// userspace, and then sends delete requests for the matching entries with the +// required attributes. +#[test] +fn test_delete_conntrack_tcp_ipv4() { + let raw: Vec = vec![ + 0x02, 0x00, 0x00, 0x00, 0x34, 0x00, 0x01, 0x80, 0x14, 0x00, 0x01, 0x80, + 0x08, 0x00, 0x01, 0x00, 0x0a, 0xff, 0xa0, 0x7c, 0x08, 0x00, 0x02, 0x00, + 0x8c, 0x52, 0x71, 0x1a, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x9a, 0xd8, 0x00, 0x00, + 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x34, 0x00, 0x02, 0x80, + 0x14, 0x00, 0x01, 0x80, 0x08, 0x00, 0x01, 0x00, 0x8c, 0x52, 0x71, 0x1a, + 0x08, 0x00, 0x02, 0x00, 0x0a, 0xff, 0xa0, 0x7c, 0x1c, 0x00, 0x02, 0x80, + 0x05, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, + 0x01, 0xbb, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0x9a, 0xd8, 0x00, 0x00, + 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x8e, 0x08, 0x00, 0x07, 0x00, + 0x00, 0x06, 0x97, 0x77, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x04, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x05, 0x00, 0x23, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x00, 0x00, + ]; + + let orig_src_addr = + IPTuple::SourceAddress(IpAddr::V4("10.255.160.124".parse().unwrap())); + let orig_dst_addr = IPTuple::DestinationAddress(IpAddr::V4( + "140.82.113.26".parse().unwrap(), + )); + + let orig_proto_num = ProtoTuple::Protocol(Protocol::Tcp); + let orig_src_port = ProtoTuple::SourcePort(39640); + let orig_dst_port = ProtoTuple::DestinationPort(443); + + let orig_ip_tuple = Tuple::Ip(vec![orig_src_addr, orig_dst_addr]); + let orig_proto_tuple = + Tuple::Proto(vec![orig_proto_num, orig_src_port, orig_dst_port]); + + let reply_src_addr = + IPTuple::SourceAddress(IpAddr::V4("140.82.113.26".parse().unwrap())); + let reply_dst_addr = IPTuple::DestinationAddress(IpAddr::V4( + "10.255.160.124".parse().unwrap(), + )); + + let reply_proto_num = ProtoTuple::Protocol(Protocol::Tcp); + let reply_src_port = ProtoTuple::SourcePort(443); + let reply_dst_port = ProtoTuple::DestinationPort(39640); + + let reply_ip_tuple = Tuple::Ip(vec![reply_src_addr, reply_dst_addr]); + let reply_proto_tuple = + Tuple::Proto(vec![reply_proto_num, reply_src_port, reply_dst_port]); + + let status = Status::DstNatDone + | Status::SrcNatDone + | Status::Confirmed + | Status::Assured + | Status::SeenReply; + + let timeout = 431991; + let mark = 0; + + let proto_info = vec![ProtoInfo::TCP(vec![ + ProtoInfoTCP::State(3), + ProtoInfoTCP::OriginalFlags(TCPFlags { flags: 35, mask: 0 }), + ProtoInfoTCP::ReplyFlags(TCPFlags { flags: 35, mask: 0 }), + ProtoInfoTCP::OriginalWindowScale(10), + ProtoInfoTCP::ReplyWindowScale(10), + ])]; + + let attributes = vec![ + ConntrackNla::CtaTupleOrig(vec![orig_ip_tuple, orig_proto_tuple]), + ConntrackNla::CtaTupleReply(vec![reply_ip_tuple, reply_proto_tuple]), + ConntrackNla::CtaStatus(status), + ConntrackNla::CtaTimeout(timeout), + ConntrackNla::CtaMark(mark), + ConntrackNla::CtaProtoInfo(proto_info), + ]; + + let expected: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(ProtoFamily::ProtoIPv4, 0, 0), + ConntrackMessage::Delete(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::Delete) 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 -D -f ipv6 -p udp --src 2409:40c4:2c:433d:8a2b:74e7:61f9:d824 +// --sport 36289 --dst 2404:6800:4007:839::200a --dport 443 NOTE: For filtered +// deletions, conntrack-tools issues a dump request to retrieve all conntrack +// entries, filters them in userspace, and then sends delete requests for the +// matching entries with the required attributes. +#[test] +fn test_delete_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, 0x2c, 0x43, 0x3d, + 0x8a, 0x2b, 0x74, 0xe7, 0x61, 0xf9, 0xd8, 0x24, 0x14, 0x00, 0x04, 0x00, + 0x24, 0x04, 0x68, 0x00, 0x40, 0x07, 0x08, 0x39, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x0a, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, + 0x11, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x8d, 0xc1, 0x00, 0x00, + 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x4c, 0x00, 0x02, 0x80, + 0x2c, 0x00, 0x01, 0x80, 0x14, 0x00, 0x03, 0x00, 0x24, 0x04, 0x68, 0x00, + 0x40, 0x07, 0x08, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a, + 0x14, 0x00, 0x04, 0x00, 0x24, 0x09, 0x40, 0xc4, 0x00, 0x2c, 0x43, 0x3d, + 0x8a, 0x2b, 0x74, 0xe7, 0x61, 0xf9, 0xd8, 0x24, 0x1c, 0x00, 0x02, 0x80, + 0x05, 0x00, 0x01, 0x00, 0x11, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, + 0x01, 0xbb, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0x8d, 0xc1, 0x00, 0x00, + 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x8e, 0x08, 0x00, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x73, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + let orig_src_addr = IPTuple::SourceAddress(IpAddr::V6( + "2409:40c4:2c:433d:8a2b:74e7:61f9:d824".parse().unwrap(), + )); + let orig_dst_addr = IPTuple::DestinationAddress(IpAddr::V6( + "2404:6800:4007:839::200a".parse().unwrap(), + )); + + let orig_proto_num = ProtoTuple::Protocol(Protocol::Udp); + let orig_src_port = ProtoTuple::SourcePort(36289); + let orig_dst_port = ProtoTuple::DestinationPort(443); + + let orig_ip_tuple = Tuple::Ip(vec![orig_src_addr, orig_dst_addr]); + let orig_proto_tuple = + Tuple::Proto(vec![orig_proto_num, orig_src_port, orig_dst_port]); + + let reply_src_addr = IPTuple::SourceAddress(IpAddr::V6( + "2404:6800:4007:839::200a".parse().unwrap(), + )); + let reply_dst_addr = IPTuple::DestinationAddress(IpAddr::V6( + "2409:40c4:2c:433d:8a2b:74e7:61f9:d824".parse().unwrap(), + )); + + let reply_proto_num = ProtoTuple::Protocol(Protocol::Udp); + let reply_src_port = ProtoTuple::SourcePort(443); + let reply_dst_port = ProtoTuple::DestinationPort(36289); + + let reply_ip_tuple = Tuple::Ip(vec![reply_src_addr, reply_dst_addr]); + let reply_proto_tuple = + Tuple::Proto(vec![reply_proto_num, reply_src_port, reply_dst_port]); + + let status = Status::DstNatDone + | Status::SrcNatDone + | Status::Confirmed + | Status::Assured + | Status::SeenReply; + + let timeout = 115; + let mark = 0; + + let attributes = vec![ + ConntrackNla::CtaTupleOrig(vec![orig_ip_tuple, orig_proto_tuple]), + ConntrackNla::CtaTupleReply(vec![reply_ip_tuple, reply_proto_tuple]), + ConntrackNla::CtaStatus(status), + ConntrackNla::CtaTimeout(timeout), + ConntrackNla::CtaMark(mark), + ]; + + let expected: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(ProtoFamily::ProtoIPv6, 0, 0), + ConntrackMessage::Delete(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::Delete) as u16); + // Check if the deserialization was correct + assert_eq!( + NetfilterMessage::parse_with_param( + &NetfilterBuffer::new(&raw), + message_type + ) + .unwrap(), + expected + ); +}