From f2b2be54c822e0f9f84cd698e4d0a5513b6ec9cf Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 16:58:44 -0400 Subject: [PATCH] Implement metrics in interproto --- Cargo.toml | 4 +- libs/interproto/Cargo.toml | 7 +- libs/interproto/src/protocols/icmp/mod.rs | 199 +++++++++++-------- libs/interproto/src/protocols/ip.rs | 222 +++++++++++++--------- libs/interproto/src/protocols/tcp.rs | 98 ++++++---- libs/interproto/src/protocols/udp.rs | 98 ++++++---- libs/protomask-metrics/Cargo.toml | 18 ++ libs/protomask-metrics/README.md | 1 + libs/protomask-metrics/src/lib.rs | 10 + libs/protomask-metrics/src/macros.rs | 10 + libs/protomask-metrics/src/metrics.rs | 30 +++ src/protomask.rs | 113 ++++++++++- 12 files changed, 556 insertions(+), 254 deletions(-) create mode 100644 libs/protomask-metrics/Cargo.toml create mode 100644 libs/protomask-metrics/README.md create mode 100644 libs/protomask-metrics/src/lib.rs create mode 100644 libs/protomask-metrics/src/macros.rs create mode 100644 libs/protomask-metrics/src/metrics.rs diff --git a/Cargo.toml b/Cargo.toml index c83dea3..819b552 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "libs/interproto", "libs/rfc6052", "libs/rtnl", + "libs/protomask-metrics", ] [[bin]] @@ -65,9 +66,10 @@ path = "src/protomask-6over4.rs" # Internal dependencies easy-tun = { path = "libs/easy-tun" } fast-nat = { path = "libs/fast-nat" } -interproto = { path = "libs/interproto" } +interproto = { path = "libs/interproto", features = ["metrics"] } rfc6052 = { path = "libs/rfc6052" } rtnl = { path = "libs/rtnl", features = ["tokio"] } +protomask-metrics = { path = "libs/protomask-metrics" } # External Dependencies tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/libs/interproto/Cargo.toml b/libs/interproto/Cargo.toml index 3e1405c..357e1b0 100644 --- a/libs/interproto/Cargo.toml +++ b/libs/interproto/Cargo.toml @@ -12,7 +12,12 @@ license = "GPL-3.0" keywords = [] categories = [] +[features] +default = [] +metrics = ["protomask-metrics"] + [dependencies] +protomask-metrics = { path = "../protomask-metrics", optional = true } log = "^0.4" pnet = "0.34.0" -thiserror = "^1.0.44" \ No newline at end of file +thiserror = "^1.0.44" diff --git a/libs/interproto/src/protocols/icmp/mod.rs b/libs/interproto/src/protocols/icmp/mod.rs index a529d6c..748bef2 100644 --- a/libs/interproto/src/protocols/icmp/mod.rs +++ b/libs/interproto/src/protocols/icmp/mod.rs @@ -20,59 +20,74 @@ pub fn translate_icmp_to_icmpv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Access the ICMP packet data in a safe way - let icmp_packet = IcmpPacket::new(icmp_packet).ok_or(Error::PacketTooShort { - expected: IcmpPacket::minimum_packet_size(), - actual: icmp_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the ICMP packet data in a safe way + let icmp_packet = IcmpPacket::new(icmp_packet).ok_or(Error::PacketTooShort { + expected: IcmpPacket::minimum_packet_size(), + actual: icmp_packet.len(), + })?; - // Translate the ICMP type and code to their ICMPv6 equivalents - let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( - icmp_packet.get_icmp_type(), - icmp_packet.get_icmp_code(), - )?; + // Translate the ICMP type and code to their ICMPv6 equivalents + let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( + icmp_packet.get_icmp_type(), + icmp_packet.get_icmp_code(), + )?; - // Some ICMP types require special payload edits - let payload = match icmpv6_type { - Icmpv6Types::TimeExceeded => { - // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) - // We need to translate the IPv4 header and the payload, but keep the padding - let mut output = vec![0u8; 4]; - output.copy_from_slice(&icmp_packet.payload()[..4]); - output.extend_from_slice(&translate_ipv4_to_ipv6( - &icmp_packet.payload()[4..], - new_source, - new_destination, - )?); - output - } - _ => icmp_packet.payload().to_vec(), - }; + // Some ICMP types require special payload edits + let payload = match icmpv6_type { + Icmpv6Types::TimeExceeded => { + // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv4 header and the payload, but keep the padding + let mut output = vec![0u8; 4]; + output.copy_from_slice(&icmp_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv4_to_ipv6( + &icmp_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmp_packet.payload().to_vec(), + }; - // Build a buffer to store the new ICMPv6 packet - let mut output_buffer = vec![0u8; IcmpPacket::minimum_packet_size() + payload.len()]; + // Build a buffer to store the new ICMPv6 packet + let mut output_buffer = vec![0u8; IcmpPacket::minimum_packet_size() + payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut icmpv6_packet = - unsafe { MutableIcmpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmpv6_packet = + unsafe { MutableIcmpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - icmpv6_packet.set_icmpv6_type(icmpv6_type); - icmpv6_packet.set_icmpv6_code(icmpv6_code); - icmpv6_packet.set_checksum(0); + // Set the header fields + icmpv6_packet.set_icmpv6_type(icmpv6_type); + icmpv6_packet.set_icmpv6_code(icmpv6_code); + icmpv6_packet.set_checksum(0); - // Copy the payload - icmpv6_packet.set_payload(&payload); + // Copy the payload + icmpv6_packet.set_payload(&payload); - // Calculate the checksum - icmpv6_packet.set_checksum(icmpv6::checksum( - &icmpv6_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Calculate the checksum + icmpv6_packet.set_checksum(icmpv6::checksum( + &icmpv6_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Translate an ICMPv6 packet to ICMP. This will make a best guess at the ICMP type and code since there is no 1:1 mapping. @@ -81,51 +96,67 @@ pub fn translate_icmpv6_to_icmp( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Access the ICMPv6 packet data in a safe way - let icmpv6_packet = Icmpv6Packet::new(icmpv6_packet).ok_or(Error::PacketTooShort { - expected: Icmpv6Packet::minimum_packet_size(), - actual: icmpv6_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the ICMPv6 packet data in a safe way + let icmpv6_packet = Icmpv6Packet::new(icmpv6_packet).ok_or(Error::PacketTooShort { + expected: Icmpv6Packet::minimum_packet_size(), + actual: icmpv6_packet.len(), + })?; - // Translate the ICMPv6 type and code to their ICMP equivalents - let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( - icmpv6_packet.get_icmpv6_type(), - icmpv6_packet.get_icmpv6_code(), - )?; + // Translate the ICMPv6 type and code to their ICMP equivalents + let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( + icmpv6_packet.get_icmpv6_type(), + icmpv6_packet.get_icmpv6_code(), + )?; - // Some ICMP types require special payload edits - let payload = match icmp_type { - IcmpTypes::TimeExceeded => { - // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) - // We need to translate the IPv6 header and the payload, but keep the padding - let mut output = vec![0u8; 4]; - output.copy_from_slice(&icmpv6_packet.payload()[..4]); - output.extend_from_slice(&translate_ipv6_to_ipv4( - &icmpv6_packet.payload()[4..], - new_source, - new_destination, - )?); - output - } - _ => icmpv6_packet.payload().to_vec(), - }; + // Some ICMP types require special payload edits + let payload = match icmp_type { + IcmpTypes::TimeExceeded => { + // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv6 header and the payload, but keep the padding + let mut output = vec![0u8; 4]; + output.copy_from_slice(&icmpv6_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv6_to_ipv4( + &icmpv6_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmpv6_packet.payload().to_vec(), + }; - // Build a buffer to store the new ICMP packet - let mut output_buffer = vec![0u8; Icmpv6Packet::minimum_packet_size() + payload.len()]; + // Build a buffer to store the new ICMP packet + let mut output_buffer = vec![0u8; Icmpv6Packet::minimum_packet_size() + payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut icmp_packet = unsafe { MutableIcmpPacket::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmp_packet = + unsafe { MutableIcmpPacket::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - icmp_packet.set_icmp_type(icmp_type); - icmp_packet.set_icmp_code(icmp_code); + // Set the header fields + icmp_packet.set_icmp_type(icmp_type); + icmp_packet.set_icmp_code(icmp_code); - // Copy the payload - icmp_packet.set_payload(&payload); + // Copy the payload + icmp_packet.set_payload(&payload); - // Calculate the checksum - icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); + // Calculate the checksum + icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); - // Return the translated packet - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMPV6, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMPV6, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } diff --git a/libs/interproto/src/protocols/ip.rs b/libs/interproto/src/protocols/ip.rs index 2ad9b16..daec44e 100644 --- a/libs/interproto/src/protocols/ip.rs +++ b/libs/interproto/src/protocols/ip.rs @@ -20,59 +20,75 @@ pub fn translate_ipv4_to_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Access the IPv4 packet data in a safe way - let ipv4_packet = Ipv4Packet::new(ipv4_packet).ok_or(Error::PacketTooShort { - expected: Ipv4Packet::minimum_packet_size(), - actual: ipv4_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the IPv4 packet data in a safe way + let ipv4_packet = Ipv4Packet::new(ipv4_packet).ok_or(Error::PacketTooShort { + expected: Ipv4Packet::minimum_packet_size(), + actual: ipv4_packet.len(), + })?; - // Perform recursive translation to determine the new payload - let new_payload = match ipv4_packet.get_next_level_protocol() { - // Pass ICMP packets to the icmp-to-icmpv6 translator - IpNextHeaderProtocols::Icmp => { - translate_icmp_to_icmpv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Perform recursive translation to determine the new payload + let new_payload = match ipv4_packet.get_next_level_protocol() { + // Pass ICMP packets to the icmp-to-icmpv6 translator + IpNextHeaderProtocols::Icmp => { + translate_icmp_to_icmpv6(ipv4_packet.payload(), new_source, new_destination)? + } - // Pass TCP packets to the tcp translator - IpNextHeaderProtocols::Tcp => { - recalculate_tcp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } - // Pass UDP packets to the udp translator - IpNextHeaderProtocols::Udp => { - recalculate_udp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } - // If the next level protocol is not something we know how to translate, - // just assume the payload can be passed through as-is - protocol => { - log::warn!("Unsupported next level protocol: {:?}", protocol); - ipv4_packet.payload().to_vec() - } - }; + // If the next level protocol is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next level protocol: {:?}", protocol); + ipv4_packet.payload().to_vec() + } + }; - // Build a buffer to store the new IPv6 packet - let mut output_buffer = vec![0u8; Ipv6Packet::minimum_packet_size() + new_payload.len()]; + // Build a buffer to store the new IPv6 packet + let mut output_buffer = vec![0u8; Ipv6Packet::minimum_packet_size() + new_payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut ipv6_packet = unsafe { MutableIpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv6_packet = + unsafe { MutableIpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - ipv6_packet.set_version(6); - ipv6_packet.set_next_header(match ipv4_packet.get_next_level_protocol() { - IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, - proto => proto, - }); - ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); - ipv6_packet.set_source(new_source); - ipv6_packet.set_destination(new_destination); - ipv6_packet.set_payload_length(new_payload.len().try_into().unwrap()); + // Set the header fields + ipv6_packet.set_version(6); + ipv6_packet.set_next_header(match ipv4_packet.get_next_level_protocol() { + IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, + proto => proto, + }); + ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); + ipv6_packet.set_source(new_source); + ipv6_packet.set_destination(new_destination); + ipv6_packet.set_payload_length(new_payload.len().try_into().unwrap()); - // Copy the payload to the buffer - ipv6_packet.set_payload(&new_payload); + // Copy the payload to the buffer + ipv6_packet.set_payload(&new_payload); - // Return the buffer - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_TRANSLATED).inc(); + + // Return the buffer + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Translates an IPv6 packet into an IPv4 packet. The packet payload will be translated recursively as needed. @@ -81,65 +97,81 @@ pub fn translate_ipv6_to_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Access the IPv6 packet data in a safe way - let ipv6_packet = Ipv6Packet::new(ipv6_packet).ok_or(Error::PacketTooShort { - expected: Ipv6Packet::minimum_packet_size(), - actual: ipv6_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the IPv6 packet data in a safe way + let ipv6_packet = Ipv6Packet::new(ipv6_packet).ok_or(Error::PacketTooShort { + expected: Ipv6Packet::minimum_packet_size(), + actual: ipv6_packet.len(), + })?; - // Perform recursive translation to determine the new payload - let new_payload = match ipv6_packet.get_next_header() { - // Pass ICMP packets to the icmpv6-to-icmp translator - IpNextHeaderProtocols::Icmpv6 => { - translate_icmpv6_to_icmp(ipv6_packet.payload(), new_source, new_destination)? - } + // Perform recursive translation to determine the new payload + let new_payload = match ipv6_packet.get_next_header() { + // Pass ICMP packets to the icmpv6-to-icmp translator + IpNextHeaderProtocols::Icmpv6 => { + translate_icmpv6_to_icmp(ipv6_packet.payload(), new_source, new_destination)? + } - // Pass TCP packets to the tcp translator - IpNextHeaderProtocols::Tcp => { - recalculate_tcp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? - } + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } - // Pass UDP packets to the udp translator - IpNextHeaderProtocols::Udp => { - recalculate_udp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? - } + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } - // If the next header is not something we know how to translate, - // just assume the payload can be passed through as-is - protocol => { - log::warn!("Unsupported next header: {:?}", protocol); - ipv6_packet.payload().to_vec() - } - }; + // If the next header is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next header: {:?}", protocol); + ipv6_packet.payload().to_vec() + } + }; - // Build a buffer to store the new IPv4 packet - let mut output_buffer = vec![0u8; Ipv4Packet::minimum_packet_size() + new_payload.len()]; + // Build a buffer to store the new IPv4 packet + let mut output_buffer = vec![0u8; Ipv4Packet::minimum_packet_size() + new_payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut ipv4_packet = unsafe { MutableIpv4Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv4_packet = + unsafe { MutableIpv4Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - ipv4_packet.set_version(4); - ipv4_packet.set_header_length(5); - ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); - ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { - IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, - proto => proto, - }); - ipv4_packet.set_source(new_source); - ipv4_packet.set_destination(new_destination); - ipv4_packet.set_total_length( - (Ipv4Packet::minimum_packet_size() + new_payload.len()) - .try_into() - .unwrap(), - ); + // Set the header fields + ipv4_packet.set_version(4); + ipv4_packet.set_header_length(5); + ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); + ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { + IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, + proto => proto, + }); + ipv4_packet.set_source(new_source); + ipv4_packet.set_destination(new_destination); + ipv4_packet.set_total_length( + (Ipv4Packet::minimum_packet_size() + new_payload.len()) + .try_into() + .unwrap(), + ); - // Copy the payload to the buffer - ipv4_packet.set_payload(&new_payload); + // Copy the payload to the buffer + ipv4_packet.set_payload(&new_payload); - // Calculate the checksum - ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); + // Calculate the checksum + ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); - // Return the buffer - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV6, STATUS_TRANSLATED).inc(); + + // Return the buffer + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV6, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } diff --git a/libs/interproto/src/protocols/tcp.rs b/libs/interproto/src/protocols/tcp.rs index 4885b67..e33bc23 100644 --- a/libs/interproto/src/protocols/tcp.rs +++ b/libs/interproto/src/protocols/tcp.rs @@ -10,26 +10,41 @@ pub fn recalculate_tcp_checksum_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut tcp_packet_buffer = tcp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); - // Get safe mutable access to the packet - let mut tcp_packet = - MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { - expected: TcpPacket::minimum_packet_size(), - actual: tcp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; - // Edit the packet's checksum - tcp_packet.set_checksum(0); - tcp_packet.set_checksum(tcp::ipv6_checksum( - &tcp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv6_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(tcp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(tcp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Re-calculates a TCP packet's checksum with a new IPv4 pseudo-header. @@ -38,26 +53,41 @@ pub fn recalculate_tcp_checksum_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut tcp_packet_buffer = tcp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); - // Get safe mutable access to the packet - let mut tcp_packet = - MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { - expected: TcpPacket::minimum_packet_size(), - actual: tcp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; - // Edit the packet's checksum - tcp_packet.set_checksum(0); - tcp_packet.set_checksum(tcp::ipv4_checksum( - &tcp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv4_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(tcp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(tcp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } #[cfg(test)] diff --git a/libs/interproto/src/protocols/udp.rs b/libs/interproto/src/protocols/udp.rs index cef9a23..099ef7c 100644 --- a/libs/interproto/src/protocols/udp.rs +++ b/libs/interproto/src/protocols/udp.rs @@ -10,26 +10,41 @@ pub fn recalculate_udp_checksum_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut udp_packet_buffer = udp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); - // Get safe mutable access to the packet - let mut udp_packet = - MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { - expected: UdpPacket::minimum_packet_size(), - actual: udp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; - // Edit the packet's checksum - udp_packet.set_checksum(0); - udp_packet.set_checksum(udp::ipv6_checksum( - &udp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv6_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(udp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(udp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Re-calculates a UDP packet's checksum with a new IPv4 pseudo-header. @@ -38,26 +53,41 @@ pub fn recalculate_udp_checksum_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut udp_packet_buffer = udp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); - // Get safe mutable access to the packet - let mut udp_packet = - MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { - expected: UdpPacket::minimum_packet_size(), - actual: udp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; - // Edit the packet's checksum - udp_packet.set_checksum(0); - udp_packet.set_checksum(udp::ipv4_checksum( - &udp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv4_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(udp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(udp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } #[cfg(test)] diff --git a/libs/protomask-metrics/Cargo.toml b/libs/protomask-metrics/Cargo.toml new file mode 100644 index 0000000..7a27df5 --- /dev/null +++ b/libs/protomask-metrics/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "protomask-metrics" +version = "0.1.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "Internal metrics library used by protomask" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/protomask-metrics" +documentation = "https://docs.rs/protomask-metrics" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +log = "^0.4" +prometheus = "0.13.3" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/libs/protomask-metrics/README.md b/libs/protomask-metrics/README.md new file mode 100644 index 0000000..cf6f4e4 --- /dev/null +++ b/libs/protomask-metrics/README.md @@ -0,0 +1 @@ +**`protomask-metrics` is exclusively for use in `protomask` and is not intended to be used on its own.** diff --git a/libs/protomask-metrics/src/lib.rs b/libs/protomask-metrics/src/lib.rs new file mode 100644 index 0000000..08ca4d4 --- /dev/null +++ b/libs/protomask-metrics/src/lib.rs @@ -0,0 +1,10 @@ +#![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] + +pub mod metrics; + +#[macro_use] +pub mod macros; \ No newline at end of file diff --git a/libs/protomask-metrics/src/macros.rs b/libs/protomask-metrics/src/macros.rs new file mode 100644 index 0000000..4e98602 --- /dev/null +++ b/libs/protomask-metrics/src/macros.rs @@ -0,0 +1,10 @@ + +/// A short-hand way to access one of the metrics in `protomask_metrics::metrics` +#[macro_export] +macro_rules! metric { + // Accept and name and multiple labels + ($metric_name: ident, $($label_name: ident),+) => { + protomask_metrics::metrics::$metric_name.with_label_values(&[$(protomask_metrics::metrics::label_values::$label_name),+]) + }; + +} diff --git a/libs/protomask-metrics/src/metrics.rs b/libs/protomask-metrics/src/metrics.rs new file mode 100644 index 0000000..e7fd8c9 --- /dev/null +++ b/libs/protomask-metrics/src/metrics.rs @@ -0,0 +1,30 @@ +use lazy_static::lazy_static; + +pub mod label_values { + /// IPv4 protocol + pub const PROTOCOL_IPV4: &str = "ipv4"; + /// IPv6 protocol + pub const PROTOCOL_IPV6: &str = "ipv6"; + /// ICMP protocol + pub const PROTOCOL_ICMP: &str = "icmp"; + /// ICMPv6 protocol + pub const PROTOCOL_ICMPV6: &str = "icmpv6"; + /// TCP protocol + pub const PROTOCOL_TCP: &str = "tcp"; + /// UDP protocol + pub const PROTOCOL_UDP: &str = "udp"; + + /// Dropped status + pub const STATUS_DROPPED: &str = "dropped"; + /// Translated status + pub const STATUS_TRANSLATED: &str = "translated"; +} + +lazy_static! { + /// Counter for the number of packets processed + pub static ref PACKET_COUNTER: prometheus::IntCounterVec = prometheus::register_int_counter_vec!( + "protomask_packets", + "Number of packets processed", + &["protocol", "status"] + ).unwrap(); +} diff --git a/src/protomask.rs b/src/protomask.rs index cb42757..8d34c30 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,9 +1,15 @@ -use std::path::PathBuf; - use clap::Parser; use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; -use ipnet::{Ipv4Net, Ipv6Net}; +use easy_tun::Tun; +use fast_nat::CrossProtocolNetworkAddressTable; +use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use nix::unistd::Uid; +use std::{ + io::{BufRead, Read, Write}, + net::{Ipv4Addr, Ipv6Addr}, + path::PathBuf, +}; mod common; @@ -34,6 +40,15 @@ struct Args { verbose: bool, } +impl Args { + pub fn get_static_reservations( + &self, + ) -> Result, Box> { + log::warn!("Static reservations are not yet implemented"); + Ok(Vec::new()) + } +} + #[derive(clap::Args)] #[group(required = true, multiple = false)] struct PoolArgs { @@ -47,8 +62,22 @@ struct PoolArgs { } impl PoolArgs { - pub fn prefixes(&self) -> Result, std::io::Error> { - todo!() + /// Read all pool prefixes from the chosen source + pub fn prefixes(&self) -> Result, Box> { + match self.pool_prefixes.len() > 0 { + true => Ok(self.pool_prefixes.clone()), + false => { + let mut prefixes = Vec::new(); + let file = std::fs::File::open(self.pool_file.as_ref().unwrap())?; + let reader = std::io::BufReader::new(file); + for line in reader.lines() { + let line = line?; + let prefix = line.parse::()?; + prefixes.push(prefix); + } + Ok(prefixes) + } + } } } @@ -65,4 +94,78 @@ pub async fn main() { log::error!("This program must be run as root"); std::process::exit(1); } + + // Bring up a TUN interface + log::debug!("Creating new TUN interface"); + let mut tun = Tun::new(&args.interface).unwrap(); + log::debug!("Created TUN interface: {}", tun.name()); + + // Get the interface index + let rt_handle = rtnl::new_handle().unwrap(); + let tun_link_idx = rtnl::link::get_link_index(&rt_handle, tun.name()) + .await + .unwrap() + .unwrap(); + + // Bring the interface up + rtnl::link::link_up(&rt_handle, tun_link_idx).await.unwrap(); + + // Add a route for the translation prefix + log::debug!( + "Adding route for {} to {}", + args.translation_prefix, + tun.name() + ); + rtnl::route::route_add(IpNet::V6(args.translation_prefix), &rt_handle, tun_link_idx) + .await + .unwrap(); + + // Add a route for each NAT pool prefix + for pool_prefix in args.pool.prefixes().unwrap() { + log::debug!("Adding route for {} to {}", pool_prefix, tun.name()); + rtnl::route::route_add(IpNet::V4(pool_prefix), &rt_handle, tun_link_idx) + .await + .unwrap(); + } + + // Set up the address table + let mut addr_table = CrossProtocolNetworkAddressTable::default(); + for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() { + addr_table.insert_indefinite(v4_addr, v6_addr); + } + + // Translate all incoming packets + log::info!("Translating packets on {}", tun.name()); + let mut buffer = vec![0u8; 1500]; + // loop { + // // Read a packet + // let len = tun.read(&mut buffer).unwrap(); + + // // Translate it based on the Layer 3 protocol number + // if let Some(output) = handle_packet( + // &buffer[..len], + // // IPv4 -> IPv6 + // |packet, source, dest| { + // // translate_ipv4_to_ipv6( + // // packet, + // // unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, + // // unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, + // // ) + // todo!() + // }, + // // IPv6 -> IPv4 + // |packet, source, dest| { + + // // translate_ipv6_to_ipv4( + // // packet, + // // unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, + // // unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, + // // ) + // todo!() + // }, + // ) { + // // Write the packet if we get one back from the handler functions + // tun.write_all(&output).unwrap(); + // } + // } }