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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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<Vec<u8>> {
-    // 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 <ewpratten@gmail.com>"]
+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<Vec<(Ipv6Addr, Ipv4Addr)>, Box<dyn std::error::Error>> {
+        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<Vec<Ipv4Net>, std::io::Error> {
-        todo!()
+    /// Read all pool prefixes from the chosen source
+    pub fn prefixes(&self) -> Result<Vec<Ipv4Net>, Box<dyn std::error::Error>> {
+        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::<Ipv4Net>()?;
+                    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();
+    //     }
+    // }
 }