Repository: samcrow/canadensis (language: rust)

Issue:
CAN loopback frames
The library works great, however, I would like to use time synchronisation over canbus. The time synchronisation master requires loopback frames to know the precise transmission timestamp of its messages, but they don't seem to be implemented in canadensis. I would gladly make a PR, but I'm not sure what would be the best way to approach this.

The simlest solution without modifying canadensis internals would be to loopback everything as regular received messages and then check if source id matches current node id in `TransferHandler::handle_message`. But I'm not sure if this is the best approach, because it could cause weird bugs in other handlers if source id is not checked. It would also act weird in case of a node id conflict.

Another solution would be to add loopback flag to frames (maybe in `Transfer` and also in `canadensis_can::Frame`) and then have an additional `TransferHandler::handle_loopback(&mut self, 
node: &mut N, 
transfer: &Transfer)` method. This is similar to how [pycyphal](https://github.com/OpenCyphal/pycyphal/blob/1c4539390016eafb87b8377259b425883f19ee2e/pycyphal/transport/can/media/_frame.py#L66) does. This way loopback frames can be separated and don't interfere with regular frames.

Any thoughts?


Discussion / hints:
Adding a loopback flag sounds like a good idea.

The only problem I can think of is that some CAN controllers (bxCAN, MCP2515, maybe others) have loopback mode as a global switch and can't loopback specific frames only. The best solution might be documentation saying that some drivers may ignore the loopback flag.

Out of curiosity, what CAN controller are you using that can do loopback for specific frames?
I'm using bxCAN and it supports loopback only in debug mode (RX is disconnected). However, I'm aiming for a similar implementation as in the old [`libuavcan`](https://github.com/OpenCyphal-Garage/platform_specific_components/blob/legacy-v0/stm32/libuavcan/driver/src/uc_stm32_can.cpp#L684), where loopback is implemented in software and TX timestamp is captured in TX mailbox empty interrupt. This is not ideal, but if critical sections in software are short, you can get microsecond precision time synchronisation.
It is reasonable to enable loopback in the driver for all frames and then silently drop all loopback frames except those few that were requested by the application to be reported back. It was implemented this way in the [original libuavcan driver for SocketCAN](https://github.com/OpenCyphal-Garage/platform_specific_components/blob/7c2f4ec48f5a0c274755ee1b383cb815f8f2b88f/linux/libuavcan/include/uavcan_linux/socketcan.hpp#L324), for example.
Now that I understand the idea of using the interrupt to get a timestamp and do software loopback, this makes more sense. The driver part (canadensis_bxcan or similar) might be slightly challenging, but the rest of the changes seem simple.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd3736d..eea4f13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- `canadensis`: Added loopback publishing/requesting functions and loopback handler function
+- `canadensis_bxcan`: Added support for loopback frames, with timestamps based on the transmit time calculated by
+  software
+- `canadensis_can`: Breaking change: Added support for loopback frames and transfers
 - `canadensis_core`: Added `remove` function to `SessionTracker` (Breaking change)
+- `canadensis_core`: Added `loopback` field to transfer types (Breaking change)
 - `canadensis_codegen_rust`: Added the ability to produce documentation comments in generated code based on DSDL
   comments
 - `canadensis_codegen_rust`: Added the ability to generate enums from DSDL types marked with `#[canadensis(enum)]`
@@ -24,13 +29,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `canadensis`: Clippy fixes
 - `canadensis`: Breaking change: Updated `half` dependency to 2.2
 - `canadensis`: Updated private dependencies
+- `canadensis`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_bxcan`: Clippy fixes
 - `canadensis_bxcan`: Breaking change: Removed deprecated functions `bxcan_frame_to_uavcan` and `uavcan_frame_to_bxcan`,
   made the replacement functions private
 - `canadensis_bxcan`: Breaking change: Updated `bxcan` dependency to 0.7
+- `canadensis_bxcan`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
+- `canadensis_can`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_codegen_rust`: Clippy fixes
 - `canadensis_codegen_rust`: Updated `clap` dependency to 4.1
 - `canadensis_codegen_rust`: Breaking change: Updated dependencies of generated code: `half` to 2.2, `memoffset` to 0.8
+- `canadensis_core`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_data_types`: Changed the order of constants to match the order in the DSDL files
 - `canadensis_data_types`: Regenerated code to match revision 935973babe11755d8070e67452b3508b4b6833e2
   of <https://github.com/OpenCyphal/public_regulated_data_types/>
@@ -45,16 +54,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `canadensis_dsdl_frontend`: Breaking change: Updated `half` dependency to 2.2
 - `canadensis_encoding`: Clippy fixes
 - `canadensis_encoding`: Breaking change: Updated `half` dependency to 2.2
+- `canadensis_linux`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_macro`: Code generation can fail with an error
 - `canadensis_macro`: Updated `syn` dependency to 2.0
 - `canadensis_macro`: Breaking change: Updated dependencies of generated code: `half` to 2.2, `memoffset` to 0.8
 - `canadensis_pnp_client`: Updated `crc-any` dependency to 2.4
+- `canadensis_pnp_client`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_serial`: Breaking change: Changed header format
 - `canadensis_serial`: Updated `simplelog` dependency to 0.12
+- `canadensis_serial`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_udp`: Major rework for [new version of Cyphal/UDP](https://forum.opencyphal.org/t/cyphal-udp-architectural-issues-caused-by-the-dependency-between-the-nodes-ip-address-and-its-identity/1765/60)
 - `canadensis_udp`: Breaking change: Changed header format
 - `canadensis_udp`: Clippy fixes
 - `canadensis_udp`: Updated `simplelog` dependency to 0.12
+- `canadensis_udp`: Breaking change: Moved clock down into drivers to make frame timestamps more accurate
 - `canadensis_write_crc`: Updated `object` dependency to 0.30
 
 
diff --git a/canadensis/examples/basic_node.rs b/canadensis/examples/basic_node.rs
index 35da61b..fe3cdbf 100644
--- a/canadensis/examples/basic_node.rs
+++ b/canadensis/examples/basic_node.rs
@@ -97,7 +97,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     };
 
     const QUEUE_CAPACITY: usize = 1210;
-    type Queue = SingleQueueDriver<ArrayQueue<Microseconds64, QUEUE_CAPACITY>, LinuxCan>;
+    type Queue =
+        SingleQueueDriver<SystemClock, ArrayQueue<Microseconds64, QUEUE_CAPACITY>, LinuxCan>;
     let queue_driver: Queue = SingleQueueDriver::new(ArrayQueue::new(), can);
 
     // Create a node with capacity for 8 publishers and 8 requesters
@@ -110,8 +111,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     let core_node: CoreNode<
         SystemClock,
-        CanTransmitter<Microseconds64, Queue>,
-        CanReceiver<Microseconds64, Queue>,
+        CanTransmitter<SystemClock, Queue>,
+        CanReceiver<SystemClock, Queue>,
         TransferIdFixedMap<CanTransport, TRANSFER_IDS>,
         Queue,
         PUBLISHERS,
diff --git a/canadensis/examples/diagnostic_console.rs b/canadensis/examples/diagnostic_console.rs
index ecac625..df9e29a 100644
--- a/canadensis/examples/diagnostic_console.rs
+++ b/canadensis/examples/diagnostic_console.rs
@@ -11,15 +11,14 @@ extern crate socketcan;
 use std::env;
 use std::error::Error;
 use std::process;
-use std::time::Instant;
 
-use canadensis::core::time::{Clock, MicrosecondDuration64, Microseconds64};
+use canadensis::core::time::MicrosecondDuration64;
 use canadensis::core::transport::Receiver;
 use canadensis::encoding::{DataType, Deserialize, ReadCursor};
 use canadensis_can::{CanReceiver, Mtu};
 use canadensis_data_types::uavcan::diagnostic::record_1_1::{self, Record};
 use canadensis_data_types::uavcan::diagnostic::severity_1_0::Severity;
-use canadensis_linux::LinuxCan;
+use canadensis_linux::{LinuxCan, SystemClock};
 
 fn main() -> Result<(), Box<dyn Error>> {
     let interface = env::args().skip(1).next().unwrap_or_else(|| {
@@ -41,7 +40,7 @@ fn main() -> Result<(), Box<dyn Error>> {
         .unwrap();
 
     loop {
-        match receiver.receive(clock.now(), &mut can) {
+        match receiver.receive(&mut clock, &mut can) {
             Ok(Some(transfer)) => {
                 match Record::deserialize(&mut ReadCursor::new(&transfer.payload)) {
                     Ok(log_record) => {
@@ -78,26 +77,3 @@ fn main() -> Result<(), Box<dyn Error>> {
         }
     }
 }
-
-#[derive(Debug, Clone)]
-struct SystemClock {
-    start_time: Instant,
-}
-
-impl SystemClock {
-    pub fn new() -> Self {
-        SystemClock {
-            start_time: Instant::now(),
-        }
-    }
-}
-
-impl Clock for SystemClock {
-    type Instant = Microseconds64;
-
-    fn now(&mut self) -> Self::Instant {
-        let since_start = Instant::now().duration_since(self.start_time);
-        let microseconds = since_start.as_micros();
-        Microseconds64::new(microseconds as u64)
-    }
-}
diff --git a/canadensis/examples/minimal_node.rs b/canadensis/examples/minimal_node.rs
index b1abd6d..f14ff71 100644
--- a/canadensis/examples/minimal_node.rs
+++ b/canadensis/examples/minimal_node.rs
@@ -36,7 +36,6 @@ use std::time::Duration;
 
 use socketcan::CANSocket;
 
-use canadensis::core::time::Microseconds64;
 use canadensis::node::{CoreNode, MinimalNode};
 use canadensis::requester::TransferIdFixedMap;
 use canadensis::Node;
@@ -68,8 +67,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     const REQUESTERS: usize = 2;
     let core_node: CoreNode<
         SystemClock,
-        CanTransmitter<Microseconds64, LinuxCan>,
-        CanReceiver<Microseconds64, LinuxCan>,
+        CanTransmitter<SystemClock, LinuxCan>,
+        CanReceiver<SystemClock, LinuxCan>,
         TransferIdFixedMap<CanTransport, TRANSFER_IDS>,
         LinuxCan,
         PUBLISHERS,
diff --git a/canadensis/examples/register_client.rs b/canadensis/examples/register_client.rs
index 7e0fa63..60c714a 100644
--- a/canadensis/examples/register_client.rs
+++ b/canadensis/examples/register_client.rs
@@ -95,7 +95,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     };
 
     // Create a node with capacity for 2 publishers and 2 requesters
-    type Queue = SingleQueueDriver<ArrayQueue<Microseconds64, 64>, LinuxCan>;
+    type Queue = SingleQueueDriver<SystemClock, ArrayQueue<Microseconds64, 64>, LinuxCan>;
     // TRANSFER_IDS must be a power of two and greater than one
     const TRANSFER_IDS: usize = 2;
     const PUBLISHERS: usize = 2;
@@ -106,8 +106,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     let receiver = CanReceiver::new(node_id, Mtu::Can8);
     let core_node: CoreNode<
         SystemClock,
-        CanTransmitter<Microseconds64, Queue>,
-        CanReceiver<Microseconds64, Queue>,
+        CanTransmitter<SystemClock, Queue>,
+        CanReceiver<SystemClock, Queue>,
         TransferIdFixedMap<CanTransport, TRANSFER_IDS>,
         Queue,
         PUBLISHERS,
diff --git a/canadensis/examples/registers.rs b/canadensis/examples/registers.rs
index ee26536..ebbee1c 100644
--- a/canadensis/examples/registers.rs
+++ b/canadensis/examples/registers.rs
@@ -95,7 +95,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     };
 
     // Create a node with capacity for 82 publishers and 2 requesters
-    type Queue = SingleQueueDriver<ArrayQueue<Microseconds64, 64>, LinuxCan>;
+    type Queue = SingleQueueDriver<SystemClock, ArrayQueue<Microseconds64, 64>, LinuxCan>;
     const TRANSFER_IDS: usize = 1;
     const PUBLISHERS: usize = 2;
     const REQUESTERS: usize = 2;
@@ -105,8 +105,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     let receiver = CanReceiver::new(node_id, Mtu::Can8);
     let core_node: CoreNode<
         SystemClock,
-        CanTransmitter<Microseconds64, Queue>,
-        CanReceiver<Microseconds64, Queue>,
+        CanTransmitter<SystemClock, Queue>,
+        CanReceiver<SystemClock, Queue>,
         TransferIdFixedMap<CanTransport, TRANSFER_IDS>,
         Queue,
         PUBLISHERS,
diff --git a/canadensis/examples/tcp_serial_basic_node.rs b/canadensis/examples/tcp_serial_basic_node.rs
index 67be300..5ec82f6 100644
--- a/canadensis/examples/tcp_serial_basic_node.rs
+++ b/canadensis/examples/tcp_serial_basic_node.rs
@@ -94,7 +94,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         SystemClock,
         SerialTransmitter<SocketDriver, 256>,
         SerialReceiver<
-            Microseconds64,
+            SystemClock,
             SocketDriver,
             DynamicSubscriptionManager<Subscription<Microseconds64>>,
         >,
diff --git a/canadensis/examples/udp_basic_node.rs b/canadensis/examples/udp_basic_node.rs
index f8a02d1..8cecea3 100644
--- a/canadensis/examples/udp_basic_node.rs
+++ b/canadensis/examples/udp_basic_node.rs
@@ -84,7 +84,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         SystemClock,
         UdpTransmitter<StdUdpSocket, MTU>,
         UdpReceiver<
-            Microseconds64,
+            SystemClock,
             SessionDynamicMap<Microseconds64, UdpNodeId, UdpTransferId, UdpSessionData>,
             StdUdpSocket,
             MTU,
diff --git a/canadensis/src/anonymous.rs b/canadensis/src/anonymous.rs
index a18e30e..f5ae71e 100644
--- a/canadensis/src/anonymous.rs
+++ b/canadensis/src/anonymous.rs
@@ -17,7 +17,7 @@ use canadensis_encoding::{Message, Serialize};
 /// Anonymous nodes have some limitations:
 /// * They can only send messages, not service requests or responses
 /// * They cannot send multi-frame messages
-pub struct AnonymousPublisher<C: Clock, M, T: Transmitter<C::Instant>> {
+pub struct AnonymousPublisher<C: Clock, M, T: Transmitter<C>> {
     /// The priority of transfers from this transmitter
     priority: <T::Transport as Transport>::Priority,
     /// The subject to transmit on
@@ -34,7 +34,7 @@ impl<C, M, T> AnonymousPublisher<C, M, T>
 where
     C: Clock,
     M: Message + Serialize,
-    T: Transmitter<C::Instant>,
+    T: Transmitter<C>,
 {
     /// Creates an anonymous message publisher
     pub fn new(
@@ -61,6 +61,32 @@ where
         clock: &mut C,
         transmitter: &mut T,
         driver: &mut T::Driver,
+    ) -> nb::Result<(), AnonymousPublishError<T::Error>> {
+        self.send_inner(payload, false, clock, transmitter, driver)
+    }
+
+    /// Prepares an anonymous message, with the loopback flag set, for sending and pushes it into
+    /// the provided transmitter
+    ///
+    /// This function returns an error if the message is too long to fit into one frame, or if
+    /// memory allocation fails.
+    pub fn send_loopback(
+        &mut self,
+        payload: &M,
+        clock: &mut C,
+        transmitter: &mut T,
+        driver: &mut T::Driver,
+    ) -> nb::Result<(), AnonymousPublishError<T::Error>> {
+        self.send_inner(payload, true, clock, transmitter, driver)
+    }
+
+    fn send_inner(
+        &mut self,
+        payload: &M,
+        loopback: bool,
+        clock: &mut C,
+        transmitter: &mut T,
+        driver: &mut T::Driver,
     ) -> nb::Result<(), AnonymousPublishError<T::Error>> {
         // Check that the message fits into one frame
         // Convert to bites, rounding up
@@ -71,7 +97,14 @@ where
         // Part 1: Serialize
         let deadline = self.timeout + clock.now();
         do_serialize(payload, |payload_bytes| {
-            self.send_payload(payload_bytes, deadline, transmitter, clock, driver)
+            self.send_payload(
+                payload_bytes,
+                deadline,
+                loopback,
+                transmitter,
+                clock,
+                driver,
+            )
         })
         .map_err(|e| e.map(AnonymousPublishError::Transport))?;
         Ok(())
@@ -81,6 +114,7 @@ where
         &mut self,
         payload: &[u8],
         deadline: C::Instant,
+        loopback: bool,
         transmitter: &mut T,
         clock: &mut C,
         driver: &mut T::Driver,
@@ -94,6 +128,7 @@ where
                 subject: self.subject,
                 source: None,
             }),
+            loopback,
             payload,
         };
         self.next_transfer_id = self.next_transfer_id.clone().increment();
diff --git a/canadensis/src/lib.rs b/canadensis/src/lib.rs
index ac059b5..97926eb 100644
--- a/canadensis/src/lib.rs
+++ b/canadensis/src/lib.rs
@@ -93,6 +93,8 @@ where
 pub trait TransferHandler<I: Instant, T: Transport> {
     /// Potentially handles an incoming message transfer
     ///
+    /// This function does not handle any loopback transfers.
+    ///
     /// This function returns true if the message was handled and should not be sent on to other
     /// handlers.
     ///
@@ -107,6 +109,8 @@ pub trait TransferHandler<I: Instant, T: Transport> {
 
     /// Potentially handles an incoming service request
     ///
+    /// This function does not handle any loopback transfers.
+    ///
     /// This function returns true if the request was handled and should not be sent on to other
     /// handlers.
     ///
@@ -122,6 +126,8 @@ pub trait TransferHandler<I: Instant, T: Transport> {
 
     /// Potentially handles an incoming service response
     ///
+    /// This function does not handle any loopback transfers.
+    ///
     /// This function returns true if the response was handled and should not be sent on to other
     /// handlers.
     ///
@@ -134,6 +140,22 @@ pub trait TransferHandler<I: Instant, T: Transport> {
         false
     }
 
+    /// Potentially handles a loopback transfer sent from this node
+    ///
+    /// All loopback transfers (message, request, and response) are handled here.
+    ///
+    /// This function returns true if the response was handled and should not be sent on to other
+    /// handlers.
+    ///
+    /// The default implementation does nothing and returns false.
+    fn handle_loopback<N: Node<Instant = I, Transport = T>>(
+        &mut self,
+        _node: &mut N,
+        _transfer: &Transfer<Vec<u8>, I, T>,
+    ) -> bool {
+        false
+    }
+
     /// Chains another handler after this handler and returns the combined handler
     ///
     /// For each incoming transfer, this handler will be given the transfer before the next handler.
@@ -177,6 +199,14 @@ where
         <H as TransferHandler<I, T>>::handle_response(self, node, transfer)
     }
 
+    fn handle_loopback<N: Node<Instant = I, Transport = T>>(
+        &mut self,
+        node: &mut N,
+        transfer: &Transfer<Vec<u8>, I, T>,
+    ) -> bool {
+        <H as TransferHandler<I, T>>::handle_loopback(self, node, transfer)
+    }
+
     fn chain<H1>(self, next: H1) -> TransferHandlerChain<Self, H1>
     where
         Self: Sized,
@@ -270,6 +300,19 @@ where
             self.handler1.handle_response(node, transfer)
         }
     }
+
+    fn handle_loopback<N: Node<Instant = I, Transport = T>>(
+        &mut self,
+        node: &mut N,
+        transfer: &Transfer<Vec<u8>, I, T>,
+    ) -> bool {
+        let handled = self.handler0.handle_loopback(node, transfer);
+        if handled {
+            true
+        } else {
+            self.handler1.handle_loopback(node, transfer)
+        }
+    }
 }
 
 /// A Cyphal node
@@ -284,9 +327,9 @@ pub trait Node {
     /// The transport that this node uses
     type Transport: Transport;
     /// The transmitter that this node uses
-    type Transmitter: Transmitter<Self::Instant, Transport = Self::Transport>;
+    type Transmitter: Transmitter<Self::Clock, Transport = Self::Transport>;
     /// The receiver that this node uses
-    type Receiver: Receiver<Self::Instant, Transport = Self::Transport>;
+    type Receiver: Receiver<Self::Clock, Transport = Self::Transport>;
 
     /// Receives any available incoming frames and attempts ot reassemble them into a transfer
     ///
@@ -298,7 +341,7 @@ pub trait Node {
     fn receive<H>(
         &mut self,
         handler: &mut H,
-    ) -> Result<(), <Self::Receiver as Receiver<Self::Instant>>::Error>
+    ) -> Result<(), <Self::Receiver as Receiver<Self::Clock>>::Error>
     where
         H: TransferHandler<Self::Instant, Self::Transport>;
 
@@ -316,7 +359,7 @@ pub trait Node {
         priority: <Self::Transport as Transport>::Priority,
     ) -> Result<
         PublishToken<T>,
-        StartSendError<<Self::Transmitter as Transmitter<Self::Instant>>::Error>,
+        StartSendError<<Self::Transmitter as Transmitter<Self::Clock>>::Error>,
     >
     where
         T: Message;
@@ -333,7 +376,18 @@ pub trait Node {
         &mut self,
         token: &PublishToken<T>,
         payload: &T,
-    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Instant>>::Error>
+    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Clock>>::Error>
+    where
+        T: Message + Serialize;
+
+    /// Publishes a message with the loopback flag set to true
+    ///
+    /// A token can be created by calling [`start_publishing`](#tymethod.start_publishing).
+    fn publish_loopback<T>(
+        &mut self,
+        token: &PublishToken<T>,
+        payload: &T,
+    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Clock>>::Error>
     where
         T: Message + Serialize;
 
@@ -349,7 +403,7 @@ pub trait Node {
         receive_timeout: <<<Self as Node>::Clock as Clock>::Instant as Instant>::Duration,
         response_payload_size_max: usize,
         priority: <Self::Transport as Transport>::Priority,
-    ) -> Result<ServiceToken<T>, StartSendError<<Self::Receiver as Receiver<Self::Instant>>::Error>>
+    ) -> Result<ServiceToken<T>, StartSendError<<Self::Receiver as Receiver<Self::Clock>>::Error>>
     where
         T: Request;
 
@@ -368,7 +422,22 @@ pub trait Node {
         destination: <Self::Transport as Transport>::NodeId,
     ) -> nb::Result<
         <Self::Transport as Transport>::TransferId,
-        <Self::Transmitter as Transmitter<Self::Instant>>::Error,
+        <Self::Transmitter as Transmitter<Self::Clock>>::Error,
+    >
+    where
+        T: Request + Serialize;
+
+    /// Sends a service request to another node, with the loopback flag set to true
+    ///
+    /// On success, this function returns the transfer ID of the request.
+    fn send_request_loopback<T>(
+        &mut self,
+        token: &ServiceToken<T>,
+        payload: &T,
+        destination: <Self::Transport as Transport>::NodeId,
+    ) -> nb::Result<
+        <Self::Transport as Transport>::TransferId,
+        <Self::Transmitter as Transmitter<Self::Clock>>::Error,
     >
     where
         T: Request + Serialize;
@@ -379,7 +448,7 @@ pub trait Node {
         subject: SubjectId,
         payload_size_max: usize,
         timeout: <<<Self as Node>::Clock as Clock>::Instant as Instant>::Duration,
-    ) -> Result<(), <Self::Receiver as Receiver<Self::Instant>>::Error>;
+    ) -> Result<(), <Self::Receiver as Receiver<Self::Clock>>::Error>;
 
     /// Subscribes to requests for a service
     fn subscribe_request(
@@ -387,25 +456,26 @@ pub trait Node {
         service: ServiceId,
         payload_size_max: usize,
         timeout: <<<Self as Node>::Clock as Clock>::Instant as Instant>::Duration,
-    ) -> Result<(), <Self::Receiver as Receiver<Self::Instant>>::Error>;
+    ) -> Result<(), <Self::Receiver as Receiver<Self::Clock>>::Error>;
 
     /// Responds to a service request
     ///
     /// This function requires a response token to match this response to its corresponding
     /// request. The token is passed to a transfer handler along with a request, so that the handler
     /// can send a response.
+    ///
+    /// The response has its loopback flag set to false.
     fn send_response<T>(
         &mut self,
         token: ResponseToken<Self::Transport>,
         timeout: <<<Self as Node>::Clock as Clock>::Instant as Instant>::Duration,
         payload: &T,
-    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Instant>>::Error>
+    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Clock>>::Error>
     where
         T: Response + Serialize;
 
     /// Attempts to flush all outgoing frames
-    fn flush(&mut self)
-        -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Instant>>::Error>;
+    fn flush(&mut self) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Clock>>::Error>;
 
     // Component access
 
diff --git a/canadensis/src/node/basic.rs b/canadensis/src/node/basic.rs
index e8e2f91..362545b 100644
--- a/canadensis/src/node/basic.rs
+++ b/canadensis/src/node/basic.rs
@@ -53,8 +53,8 @@ where
     ) -> Result<
         Self,
         NodeError<
-            StartSendError<<N::Transmitter as Transmitter<N::Instant>>::Error>,
-            <N::Receiver as Receiver<N::Instant>>::Error,
+            StartSendError<<N::Transmitter as Transmitter<N::Clock>>::Error>,
+            <N::Receiver as Receiver<N::Clock>>::Error,
         >,
     > {
         // The MinimalNode takes care of heartbeats.
@@ -113,7 +113,7 @@ where
     /// This function must be called once per second to send heartbeat and port list messages
     pub fn run_per_second_tasks(
         &mut self,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error> {
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error> {
         self.node.run_per_second_tasks()?;
         if self.seconds_since_port_list_published == 10 {
             self.seconds_since_port_list_published = 1;
@@ -126,7 +126,7 @@ where
 
     fn publish_port_list(
         &mut self,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error> {
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error> {
         self.node
             .node_mut()
             .publish(&self.port_list_token, &self.port_list)
@@ -168,7 +168,7 @@ where
     fn receive<H>(
         &mut self,
         handler: &mut H,
-    ) -> Result<(), <N::Receiver as Receiver<N::Instant>>::Error>
+    ) -> Result<(), <N::Receiver as Receiver<N::Clock>>::Error>
     where
         H: TransferHandler<Self::Instant, Self::Transport>,
     {
@@ -184,7 +184,7 @@ where
         subject: SubjectId,
         timeout: <<N::Clock as Clock>::Instant as Instant>::Duration,
         priority: <Self::Transport as Transport>::Priority,
-    ) -> Result<PublishToken<T>, StartSendError<<N::Transmitter as Transmitter<N::Instant>>::Error>>
+    ) -> Result<PublishToken<T>, StartSendError<<N::Transmitter as Transmitter<N::Clock>>::Error>>
     where
         T: Message,
     {
@@ -210,20 +210,31 @@ where
         &mut self,
         token: &PublishToken<T>,
         payload: &T,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error>
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error>
     where
         T: Message + Serialize,
     {
         self.node.node_mut().publish(token, payload)
     }
 
+    fn publish_loopback<T>(
+        &mut self,
+        token: &PublishToken<T>,
+        payload: &T,
+    ) -> nb::Result<(), <Self::Transmitter as Transmitter<Self::Clock>>::Error>
+    where
+        T: Message + Serialize,
+    {
+        self.node.node_mut().publish_loopback(token, payload)
+    }
+
     fn start_sending_requests<T>(
         &mut self,
         service: ServiceId,
         receive_timeout: <<N::Clock as Clock>::Instant as Instant>::Duration,
         response_payload_size_max: usize,
         priority: <Self::Transport as Transport>::Priority,
-    ) -> Result<ServiceToken<T>, StartSendError<<N::Receiver as Receiver<N::Instant>>::Error>>
+    ) -> Result<ServiceToken<T>, StartSendError<<N::Receiver as Receiver<N::Clock>>::Error>>
     where
         T: Request,
     {
@@ -255,7 +266,7 @@ where
         destination: <Self::Transport as Transport>::NodeId,
     ) -> nb::Result<
         <Self::Transport as Transport>::TransferId,
-        <N::Transmitter as Transmitter<N::Instant>>::Error,
+        <N::Transmitter as Transmitter<N::Clock>>::Error,
     >
     where
         T: Request + Serialize,
@@ -265,12 +276,29 @@ where
             .send_request(token, payload, destination)
     }
 
+    fn send_request_loopback<T>(
+        &mut self,
+        token: &ServiceToken<T>,
+        payload: &T,
+        destination: <Self::Transport as Transport>::NodeId,
+    ) -> nb::Result<
+        <Self::Transport as Transport>::TransferId,
+        <Self::Transmitter as Transmitter<Self::Clock>>::Error,
+    >
+    where
+        T: Request + Serialize,
+    {
+        self.node
+            .node_mut()
+            .send_request_loopback(token, payload, destination)
+    }
+
     fn subscribe_message(
         &mut self,
         subject: SubjectId,
         payload_size_max: usize,
         timeout: <<N::Clock as Clock>::Instant as Instant>::Duration,
-    ) -> Result<(), <N::Receiver as Receiver<N::Instant>>::Error> {
+    ) -> Result<(), <N::Receiver as Receiver<N::Clock>>::Error> {
         self.node
             .node_mut()
             .subscribe_message(subject, payload_size_max, timeout)?;
@@ -286,7 +314,7 @@ where
         service: ServiceId,
         payload_size_max: usize,
         timeout: <<N::Clock as Clock>::Instant as Instant>::Duration,
-    ) -> Result<(), <N::Receiver as Receiver<N::Instant>>::Error> {
+    ) -> Result<(), <N::Receiver as Receiver<N::Clock>>::Error> {
         self.node
             .node_mut()
             .subscribe_request(service, payload_size_max, timeout)?;
@@ -302,7 +330,7 @@ where
         token: ResponseToken<Self::Transport>,
         timeout: <<N::Clock as Clock>::Instant as Instant>::Duration,
         payload: &T,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error>
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error>
     where
         T: Response + Serialize,
     {
@@ -311,7 +339,7 @@ where
 
     fn flush(
         &mut self,
-    ) -> canadensis_core::nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error> {
+    ) -> canadensis_core::nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error> {
         self.node.node_mut().flush()
     }
 
diff --git a/canadensis/src/node/core.rs b/canadensis/src/node/core.rs
index 7e9b61d..10d6dc0 100644
--- a/canadensis/src/node/core.rs
+++ b/canadensis/src/node/core.rs
@@ -34,24 +34,24 @@ use crate::{Node, PublishToken, ResponseToken, ServiceToken, StartSendError, Tra
 pub struct CoreNode<C, T, U, TR, D, const P: usize, const R: usize>
 where
     C: Clock,
-    U: Receiver<C::Instant>,
-    T: Transmitter<C::Instant>,
+    U: Receiver<C>,
+    T: Transmitter<C>,
 {
     clock: C,
     transmitter: T,
     receiver: U,
     driver: D,
     node_id: <T::Transport as Transport>::NodeId,
-    publishers: FnvIndexMap<SubjectId, Publisher<C::Instant, T>, P>,
-    requesters: FnvIndexMap<ServiceId, Requester<C::Instant, T, TR>, R>,
+    publishers: FnvIndexMap<SubjectId, Publisher<C, T>, P>,
+    requesters: FnvIndexMap<ServiceId, Requester<C, T, TR>, R>,
 }
 
 impl<C, T, U, N, TR, D, const P: usize, const R: usize> CoreNode<C, T, U, TR, D, P, R>
 where
     C: Clock,
     N: Transport,
-    U: Receiver<C::Instant, Transport = N, Driver = D>,
-    T: Transmitter<C::Instant, Transport = N, Driver = D>,
+    U: Receiver<C, Transport = N, Driver = D>,
+    T: Transmitter<C, Transport = N, Driver = D>,
     TR: TransferIdTracker<N>,
 {
     /// Creates a node
@@ -88,6 +88,8 @@ where
         &mut self.driver
     }
 
+    /// Categorizes a transfer as a message, request, response, or loopback,
+    /// and calls the corresponding method of the handler
     fn handle_incoming_transfer<H>(
         &mut self,
         transfer: Transfer<Vec<u8>, C::Instant, U::Transport>,
@@ -95,33 +97,40 @@ where
     ) where
         H: TransferHandler<<Self as Node>::Instant, U::Transport>,
     {
-        match transfer.header {
-            Header::Message(message_header) => {
-                let message_transfer = MessageTransfer {
-                    header: message_header,
-                    payload: transfer.payload,
-                };
-                handler.handle_message(self, &message_transfer);
-            }
-            Header::Request(service_header) => {
-                let token = ResponseToken {
-                    service: service_header.service,
-                    client: service_header.source.clone(),
-                    transfer: service_header.transfer_id.clone(),
-                    priority: service_header.priority.clone(),
-                };
-                let service_transfer = ServiceTransfer {
-                    header: service_header,
-                    payload: transfer.payload,
-                };
-                handler.handle_request(self, token, &service_transfer);
-            }
-            Header::Response(service_header) => {
-                let service_transfer = ServiceTransfer {
-                    header: service_header,
-                    payload: transfer.payload,
-                };
-                handler.handle_response(self, &service_transfer);
+        if transfer.loopback {
+            handler.handle_loopback(self, &transfer);
+        } else {
+            match transfer.header {
+                Header::Message(message_header) => {
+                    let message_transfer = MessageTransfer {
+                        header: message_header,
+                        loopback: transfer.loopback,
+                        payload: transfer.payload,
+                    };
+                    handler.handle_message(self, &message_transfer);
+                }
+                Header::Request(service_header) => {
+                    let token = ResponseToken {
+                        service: service_header.service,
+                        client: service_header.source.clone(),
+                        transfer: service_header.transfer_id.clone(),
+                        priority: service_header.priority.clone(),
+                    };
+                    let service_transfer = ServiceTransfer {
+                        header: service_header,
+                        loopback: transfer.loopback,
+                        payload: transfer.payload,
+                    };
+                    handler.handle_request(self, token, &service_transfer);
+                }
+                Header::Response(service_header) => {
+                    let service_transfer = ServiceTransfer {
+                        header: service_header,
+                        loopback: transfer.loopback,
+                        payload: transfer.payload,
+                    };
+                    handler.handle_response(self, &service_transfer);
+                }
             }
         }
     }
@@ -141,6 +150,7 @@ where
                 source: self.node_id.clone(),
                 destination: token.client,
             }),
+            loopback: false,
             payload,
         };
         self.transmitter
@@ -152,8 +162,8 @@ impl<C, T, U, N, TR, D, const P: usize, const R: usize> Node for CoreNode<C, T,
 where
     C: Clock,
     N: Transport,
-    T: Transmitter<<C as Clock>::Instant, Transport = N, Driver = D>,
-    U: Receiver<<C as Clock>::Instant, Transport = N, Driver = D>,
+    T: Transmitter<C, Transport = N, Driver = D>,
+    U: Receiver<C, Transport = N, Driver = D>,
     TR: TransferIdTracker<N>,
 {
     type Clock = C;
@@ -166,7 +176,7 @@ where
     where
         H: TransferHandler<Self::Instant, Self::Transport>,
     {
-        if let Some(transfer) = self.receiver.receive(self.clock.now(), &mut self.driver)? {
+        if let Some(transfer) = self.receiver.receive(&mut self.clock, &mut self.driver)? {
             self.handle_incoming_transfer(transfer, handler)
         }
         Ok(())
@@ -219,6 +229,27 @@ where
         )
     }
 
+    fn publish_loopback<M>(
+        &mut self,
+        token: &PublishToken<M>,
+        payload: &M,
+    ) -> nb::Result<(), T::Error>
+    where
+        M: Message + Serialize,
+    {
+        let publisher = self
+            .publishers
+            .get_mut(&token.0)
+            .expect("Bug: Token exists but no publisher");
+        publisher.publish_loopback(
+            &mut self.clock,
+            token.0,
+            payload,
+            &mut self.transmitter,
+            &mut self.driver,
+        )
+    }
+
     /// Sets up to send requests for a service
     ///
     /// This also subscribes to the corresponding responses.
@@ -294,6 +325,32 @@ where
         )
     }
 
+    fn send_request_loopback<M>(
+        &mut self,
+        token: &ServiceToken<M>,
+        payload: &M,
+        destination: <Self::Transport as Transport>::NodeId,
+    ) -> nb::Result<
+        <Self::Transport as Transport>::TransferId,
+        <Self::Transmitter as Transmitter<Self::Clock>>::Error,
+    >
+    where
+        M: Request + Serialize,
+    {
+        let requester = self
+            .requesters
+            .get_mut(&token.0)
+            .expect("Bug: No requester for token");
+        requester.send_loopback(
+            &mut self.clock,
+            token.0,
+            payload,
+            destination,
+            &mut self.transmitter,
+            &mut self.driver,
+        )
+    }
+
     fn subscribe_message(
         &mut self,
         subject: SubjectId,
diff --git a/canadensis/src/node/minimal.rs b/canadensis/src/node/minimal.rs
index d6cf386..e8d47b8 100644
--- a/canadensis/src/node/minimal.rs
+++ b/canadensis/src/node/minimal.rs
@@ -36,7 +36,7 @@ where
     /// * `node`: The underlying node (this is usually a [`CoreNode`](crate::node::CoreNode))
     pub fn new(
         mut node: N,
-    ) -> Result<Self, StartSendError<<N::Transmitter as Transmitter<N::Instant>>::Error>> {
+    ) -> Result<Self, StartSendError<<N::Transmitter as Transmitter<N::Clock>>::Error>> {
         // Default heartbeat settings
         let heartbeat = Heartbeat {
             uptime: 0,
@@ -73,14 +73,14 @@ where
     /// Either `run_periodic_tasks` or `run_per_second_tasks` should be called, but not both.
     pub fn run_per_second_tasks(
         &mut self,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error> {
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error> {
         self.send_heartbeat()
     }
 
     /// Publishes a heartbeat message
     fn send_heartbeat(
         &mut self,
-    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Instant>>::Error> {
+    ) -> nb::Result<(), <N::Transmitter as Transmitter<N::Clock>>::Error> {
         self.heartbeat.uptime = self.heartbeat.uptime.saturating_add(1);
         self.node.publish(&self.heartbeat_token, &self.heartbeat)
     }
diff --git a/canadensis/src/publisher.rs b/canadensis/src/publisher.rs
index 6646120..d39f140 100644
--- a/canadensis/src/publisher.rs
+++ b/canadensis/src/publisher.rs
@@ -8,18 +8,18 @@ use canadensis_encoding::{Message, Serialize};
 /// Assembles transfers and manages transfer IDs to send messages
 ///
 /// The subject ID is not part of this struct because it is used as a key in the map of publishers.
-pub struct Publisher<I: Instant, T: Transmitter<I>> {
+pub struct Publisher<C: Clock, T: Transmitter<C>> {
     /// The ID of the next transfer sent
     next_transfer_id: <T::Transport as Transport>::TransferId,
     /// Timeout for sending a transfer, measured from the time the payload is serialized
-    timeout: I::Duration,
+    timeout: <<C as Clock>::Instant as Instant>::Duration,
     /// Priority for transfers
     priority: <T::Transport as Transport>::Priority,
     /// ID of this node
     source: <T::Transport as Transport>::NodeId,
 }
 
-impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
+impl<C: Clock, T: Transmitter<C>> Publisher<C, T> {
     /// Creates a message transmitter
     ///
     /// node: The ID of this node
@@ -27,7 +27,7 @@ impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
     /// priority: The priority to use for messages
     pub fn new(
         node_id: <T::Transport as Transport>::NodeId,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         priority: <T::Transport as Transport>::Priority,
     ) -> Self {
         Publisher {
@@ -38,7 +38,10 @@ impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
         }
     }
 
-    pub fn publish<M, C>(
+    /// Publishes a message
+    ///
+    /// The loopback flag is set to false
+    pub fn publish<M>(
         &mut self,
         clock: &mut C,
         subject: SubjectId,
@@ -48,30 +51,60 @@ impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
     ) -> nb::Result<(), T::Error>
     where
         M: Message + Serialize,
-        I: Instant,
-        C: Clock<Instant = I>,
     {
         let deadline = self.timeout + clock.now();
         // Part 1: Serialize
         do_serialize(payload, |payload_bytes| {
             // Part 2: Split into frames and put frames in the queue
-            self.send_payload(subject, payload_bytes, deadline, transmitter, clock, driver)
+            self.send_payload(
+                subject,
+                payload_bytes,
+                deadline,
+                false,
+                transmitter,
+                clock,
+                driver,
+            )
         })
     }
-
-    pub fn send_payload<C>(
+    /// Publishes a loopback message
+    pub fn publish_loopback<M>(
         &mut self,
+        clock: &mut C,
         subject: SubjectId,
-        payload: &[u8],
-        deadline: I,
+        payload: &M,
         transmitter: &mut T,
-        clock: &mut C,
         driver: &mut T::Driver,
     ) -> nb::Result<(), T::Error>
     where
-        I: Clone,
-        C: Clock<Instant = I>,
+        M: Message + Serialize,
     {
+        let deadline = self.timeout + clock.now();
+        // Part 1: Serialize
+        do_serialize(payload, |payload_bytes| {
+            // Part 2: Split into frames and put frames in the queue
+            self.send_payload(
+                subject,
+                payload_bytes,
+                deadline,
+                true,
+                transmitter,
+                clock,
+                driver,
+            )
+        })
+    }
+
+    fn send_payload(
+        &mut self,
+        subject: SubjectId,
+        payload: &[u8],
+        deadline: C::Instant,
+        loopback: bool,
+        transmitter: &mut T,
+        clock: &mut C,
+        driver: &mut T::Driver,
+    ) -> nb::Result<(), T::Error> {
         // Assemble the transfer
         let transfer = Transfer {
             header: Header::Message(MessageHeader {
@@ -81,6 +114,7 @@ impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
                 subject,
                 source: Some(self.source.clone()),
             }),
+            loopback,
             payload,
         };
         self.next_transfer_id = self.next_transfer_id.clone().increment();
@@ -91,15 +125,15 @@ impl<I: Instant, T: Transmitter<I>> Publisher<I, T> {
 
 mod fmt_impl {
     use crate::publisher::Publisher;
-    use canadensis_core::time::Instant;
+    use canadensis_core::time::{Clock, Instant};
     use canadensis_core::transport::{Transmitter, Transport};
     use core::fmt::{Debug, Formatter, Result};
 
-    impl<I, T> Debug for Publisher<I, T>
+    impl<C, T> Debug for Publisher<C, T>
     where
-        I: Instant,
-        I::Duration: Debug,
-        T: Transmitter<I>,
+        C: Clock,
+        <<C as Clock>::Instant as Instant>::Duration: Debug,
+        T: Transmitter<C>,
         <T::Transport as Transport>::TransferId: Debug,
         <T::Transport as Transport>::Priority: Debug,
         <T::Transport as Transport>::NodeId: Debug,
diff --git a/canadensis/src/register.rs b/canadensis/src/register.rs
index ba89b5d..b2806b2 100644
--- a/canadensis/src/register.rs
+++ b/canadensis/src/register.rs
@@ -139,7 +139,7 @@ where
     /// for requests.
     pub fn subscribe_requests<N>(
         node: &mut N,
-    ) -> Result<(), <N::Receiver as Receiver<N::Instant>>::Error>
+    ) -> Result<(), <N::Receiver as Receiver<N::Clock>>::Error>
     where
         N: Node,
     {
diff --git a/canadensis/src/requester.rs b/canadensis/src/requester.rs
index e77dd06..8a4b8e2 100644
--- a/canadensis/src/requester.rs
+++ b/canadensis/src/requester.rs
@@ -11,18 +11,18 @@ use canadensis_encoding::{Request, Serialize};
 use crate::serialize::do_serialize;
 
 /// Assembles transfers and manages transfer IDs to send service requests
-pub struct Requester<I: Instant, T: Transmitter<I>, R> {
+pub struct Requester<C: Clock, T: Transmitter<C>, R> {
     /// The ID of this node
     this_node: <T::Transport as Transport>::NodeId,
     /// The priority of transfers from this transmitter
     priority: <T::Transport as Transport>::Priority,
     /// The timeout for sending transfers
-    timeout: I::Duration,
+    timeout: <<C as Clock>::Instant as Instant>::Duration,
     /// The ID of the next transfer to send, for each destination node
     transfer_ids: R,
 }
 
-impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requester<I, T, R> {
+impl<C: Clock, T: Transmitter<C>, R: TransferIdTracker<T::Transport>> Requester<C, T, R> {
     /// Creates a service request transmitter
     ///
     /// this_node: The ID of this node
@@ -32,7 +32,7 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
     /// service: The service ID to request
     pub fn new(
         this_node: <T::Transport as Transport>::NodeId,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         priority: <T::Transport as Transport>::Priority,
     ) -> Self {
         Requester {
@@ -44,7 +44,7 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
     }
 
     /// Sends a service request and returns its transfer ID
-    pub fn send<Q, C>(
+    pub fn send<Q>(
         &mut self,
         clock: &mut C,
         service: ServiceId,
@@ -55,7 +55,6 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
     ) -> nb::Result<<T::Transport as Transport>::TransferId, T::Error>
     where
         Q: Serialize + Request,
-        C: Clock<Instant = I>,
     {
         // Part 1: Serialize
         let deadline = self.timeout + clock.now();
@@ -66,6 +65,7 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
                 service,
                 destination,
                 deadline,
+                false,
                 transmitter,
                 clock,
                 driver,
@@ -73,19 +73,47 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
         })
     }
 
-    fn send_payload<C>(
+    /// Sends a loopback service request and returns its transfer ID
+    pub fn send_loopback<Q>(
         &mut self,
-        payload: &[u8],
+        clock: &mut C,
         service: ServiceId,
+        payload: &Q,
         destination: <T::Transport as Transport>::NodeId,
-        deadline: I,
         transmitter: &mut T,
-        clock: &mut C,
         driver: &mut T::Driver,
     ) -> nb::Result<<T::Transport as Transport>::TransferId, T::Error>
     where
-        C: Clock<Instant = I>,
+        Q: Serialize + Request,
     {
+        // Part 1: Serialize
+        let deadline = self.timeout + clock.now();
+        do_serialize(payload, |payload_bytes| {
+            // Part 2: Split into frames and send
+            self.send_payload(
+                payload_bytes,
+                service,
+                destination,
+                deadline,
+                true,
+                transmitter,
+                clock,
+                driver,
+            )
+        })
+    }
+
+    fn send_payload(
+        &mut self,
+        payload: &[u8],
+        service: ServiceId,
+        destination: <T::Transport as Transport>::NodeId,
+        deadline: C::Instant,
+        loopback: bool,
+        transmitter: &mut T,
+        clock: &mut C,
+        driver: &mut T::Driver,
+    ) -> nb::Result<<T::Transport as Transport>::TransferId, T::Error> {
         // Assemble the transfer
         let transfer_id = self
             .transfer_ids
@@ -100,6 +128,7 @@ impl<I: Instant, T: Transmitter<I>, R: TransferIdTracker<T::Transport>> Requeste
                 source: self.this_node.clone(),
                 destination,
             }),
+            loopback,
             payload,
         };
 
@@ -162,16 +191,16 @@ impl<T: Transport, const C: usize> TransferIdTracker<T> for TransferIdFixedMap<T
 mod fmt_impl {
     use core::fmt::{Debug, Formatter, Result};
 
-    use canadensis_core::time::Instant;
+    use canadensis_core::time::{Clock, Instant};
     use canadensis_core::transport::{Transmitter, Transport};
 
     use crate::requester::Requester;
 
-    impl<I, T, R> Debug for Requester<I, T, R>
+    impl<C, T, R> Debug for Requester<C, T, R>
     where
-        I: Instant,
-        T: Transmitter<I>,
-        I::Duration: Debug,
+        C: Clock,
+        T: Transmitter<C>,
+        <<C as Clock>::Instant as Instant>::Duration: Debug,
         <T::Transport as Transport>::TransferId: Debug,
         <T::Transport as Transport>::Priority: Debug,
         <T::Transport as Transport>::NodeId: Debug,
diff --git a/canadensis_bxcan/src/lib.rs b/canadensis_bxcan/src/lib.rs
index 089e394..16eb056 100644
--- a/canadensis_bxcan/src/lib.rs
+++ b/canadensis_bxcan/src/lib.rs
@@ -28,25 +28,32 @@ pub use bxcan::OverrunError;
 use bxcan::filter::Mask32;
 use bxcan::{Can, ExtendedId, Fifo, FilterOwner, Instance, Mailbox};
 use canadensis::core::subscription::Subscription;
-use canadensis::core::time::Instant;
+use canadensis::core::time::{Clock, Instant};
 use canadensis::core::OutOfMemoryError;
 use canadensis_can::driver::{optimize_filters, ReceiveDriver, TransmitDriver};
 use canadensis_can::{CanNodeId, Frame};
 use core::cmp::Ordering;
 use core::convert::{Infallible, TryFrom};
+use heapless::Deque;
+
+/// Maximum number of loopback frames that can be stored
+const LOOPBACK_CAPACITY: usize = 2;
 
 /// A CAN driver that wraps a bxCAN device and keeps track of deadlines for queued frames
-pub struct BxCanDriver<I, N>
+pub struct BxCanDriver<C, N>
 where
+    C: Clock,
     N: Instance,
 {
     can: Can<N>,
-    deadlines: DeadlineTracker<I>,
+    deadlines: DeadlineTracker<C::Instant>,
+    /// Copies of transmitted loopback frames that have not yet been received
+    loopback_frames: Deque<Frame<C::Instant>, LOOPBACK_CAPACITY>,
 }
 
-impl<I, N> BxCanDriver<I, N>
+impl<C, N> BxCanDriver<C, N>
 where
-    I: Clone,
+    C: Clock,
     N: Instance,
 {
     /// Creates a CAN driver
@@ -54,6 +61,7 @@ where
         BxCanDriver {
             can,
             deadlines: DeadlineTracker::new(),
+            loopback_frames: Deque::new(),
         }
     }
 
@@ -70,11 +78,82 @@ where
     pub fn can_mut(&mut self) -> &mut Can<N> {
         &mut self.can
     }
+
+    /// Returns true if at least one loopback frame is ready to receive
+    pub fn loopback_frame_waiting(&self) -> bool {
+        !self.loopback_frames.is_empty()
+    }
+
+    /// Tries to transmit a frame, and assumes that the frame's deadline has not passed
+    fn transmit_inner(
+        &mut self,
+        frame: &Frame<C::Instant>,
+        deadline: C::Instant,
+    ) -> nb::Result<Option<Frame<C::Instant>>, <Self as TransmitDriver<C>>::Error> {
+        let frame = cyphal_frame_to_bxcan(frame);
+        match self.can.transmit(&frame) {
+            Ok(status) => {
+                // Store the deadline for this frame
+                let replaced_deadline = self.deadlines.replace(status.mailbox(), deadline);
+                if let (Some(removed_frame), Some(removed_frame_deadline)) =
+                    (status.dequeued_frame(), replaced_deadline)
+                {
+                    if let Ok(removed_frame) =
+                        bxcan_frame_to_cyphal(removed_frame, removed_frame_deadline)
+                    {
+                        Ok(Some(removed_frame))
+                    } else {
+                        // Frame that was removed is not compatible with Cyphal, so ignore it
+                        Ok(None)
+                    }
+                } else {
+                    // No frame was removed
+                    Ok(None)
+                }
+            }
+            Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock),
+            Err(nb::Error::Other(infallible)) => match infallible {},
+        }
+    }
+}
+impl<C, N> BxCanDriver<C, N>
+where
+    C: Clock,
+    N: Instance + FilterOwner,
+{
+    /// Tries to receive a frame from the CAN bus
+    fn receive_from_bus(
+        &mut self,
+        clock: &mut C,
+    ) -> nb::Result<Frame<C::Instant>, <Self as ReceiveDriver<C>>::Error> {
+        loop {
+            match self.can.receive() {
+                Ok(frame) => {
+                    if let Ok(frame) = bxcan_frame_to_cyphal(&frame, clock.now()) {
+                        break Ok(frame);
+                    }
+                    // Otherwise the frame is remote or basic ID, not compatible with Cyphal.
+                    // Try to receive another frame.
+                }
+                Err(nb::Error::WouldBlock) => break Err(nb::Error::WouldBlock),
+                Err(nb::Error::Other(e)) => break Err(nb::Error::Other(e)),
+            }
+        }
+    }
+    /// Tries to receive a frame from the loopback queue
+    fn receive_loopback(
+        &mut self,
+    ) -> nb::Result<Frame<C::Instant>, <Self as ReceiveDriver<C>>::Error> {
+        match self.loopback_frames.pop_front() {
+            Some(frame) => Ok(frame),
+            None => Err(nb::Error::WouldBlock),
+        }
+    }
 }
 
-impl<I, N> TransmitDriver<I> for BxCanDriver<I, N>
+impl<C, N> TransmitDriver<C> for BxCanDriver<C, N>
 where
-    I: Instant,
+    C: Clock,
     N: Instance,
 {
     type Error = Infallible;
@@ -89,37 +168,28 @@ where
         }
     }
 
-    fn transmit(&mut self, frame: Frame<I>, now: I) -> nb::Result<Option<Frame<I>>, Self::Error> {
+    fn transmit(
+        &mut self,
+        frame: Frame<C::Instant>,
+        clock: &mut C,
+    ) -> nb::Result<Option<Frame<C::Instant>>, Self::Error> {
+        let now = clock.now();
         clean_expired_frames(&mut self.deadlines, &mut self.can, now);
         // Check that the frame's deadline has not passed
         let deadline = frame.timestamp();
         match deadline.overflow_safe_compare(&now) {
             Ordering::Greater | Ordering::Equal => {
                 // Deadline is now or in the future. Continue to transmit.
-                let frame = cyphal_frame_to_bxcan(&frame);
-                match self.can.transmit(&frame) {
-                    Ok(status) => {
-                        // Store the deadline for this frame
-                        let replaced_deadline = self.deadlines.replace(status.mailbox(), deadline);
-                        if let (Some(removed_frame), Some(removed_frame_deadline)) =
-                            (status.dequeued_frame(), replaced_deadline)
-                        {
-                            if let Ok(removed_frame) =
-                                bxcan_frame_to_cyphal(removed_frame, removed_frame_deadline)
-                            {
-                                Ok(Some(removed_frame))
-                            } else {
-                                // Frame that was removed is not compatible with Cyphal, so ignore it
-                                Ok(None)
-                            }
-                        } else {
-                            // No frame was removed
-                            Ok(None)
-                        }
-                    }
-                    Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock),
-                    Err(nb::Error::Other(infallible)) => match infallible {},
+                let transmit_status = self.transmit_inner(&frame, deadline);
+                if transmit_status.is_ok() && frame.loopback() {
+                    // Loopback frame successfully sent; store a copy with its timestamp set to
+                    // the time just before it was sent
+                    let mut loopback_frame = frame;
+                    loopback_frame.set_timestamp(now);
+                    // If the loopback queue is full, drop this frame
+                    let _ = self.loopback_frames.push_back(loopback_frame);
                 }
+                transmit_status
             }
             Ordering::Less => {
                 // Deadline passed, ignore frame
@@ -128,33 +198,31 @@ where
         }
     }
 
-    fn flush(&mut self, _now: I) -> nb::Result<(), Self::Error> {
+    fn flush(&mut self, _clock: &mut C) -> nb::Result<(), Self::Error> {
         // The hardware does this automatically
         Ok(())
     }
 }
 
-impl<I, N> ReceiveDriver<I> for BxCanDriver<I, N>
+impl<C, N> ReceiveDriver<C> for BxCanDriver<C, N>
 where
-    I: Instant,
+    C: Clock,
     N: Instance + FilterOwner,
 {
     /// This matches the error type defined in bxcan
     type Error = OverrunError;
 
-    fn receive(&mut self, now: I) -> nb::Result<Frame<I>, Self::Error> {
-        loop {
-            match self.can.receive() {
-                Ok(frame) => {
-                    if let Ok(frame) = bxcan_frame_to_cyphal(&frame, now) {
-                        break Ok(frame);
-                    }
-                    // Otherwise the frame is remote or basic ID, not compatible with Cyphal.
-                    // Try to receive another frame.
-                }
-                Err(nb::Error::WouldBlock) => break Err(nb::Error::WouldBlock),
-                Err(nb::Error::Other(e)) => break Err(nb::Error::Other(e)),
-            }
+    /// Tries to receive a frame from the frame receive queue or the loopback frame queue
+    ///
+    /// If both loopback and non-loopback frames are waiting, this function returns a non-loopback
+    /// frame.
+    ///
+    fn receive(&mut self, clock: &mut C) -> nb::Result<Frame<C::Instant>, Self::Error> {
+        match self.receive_from_bus(clock) {
+            Ok(frame) => Ok(frame),
+            Err(nb::Error::Other(e)) => Err(nb::Error::Other(e)),
+            // No frames waiting from the bus. Try the loopback queue.
+            Err(nb::Error::WouldBlock) => self.receive_loopback(),
         }
     }
 
diff --git a/canadensis_bxcan/src/pnp.rs b/canadensis_bxcan/src/pnp.rs
index cb4b69d..08df544 100644
--- a/canadensis_bxcan/src/pnp.rs
+++ b/canadensis_bxcan/src/pnp.rs
@@ -17,16 +17,10 @@ pub struct BxCanPnpClient<C: Clock, M, I: Instance + FilterOwner> {
     pub client: PnpClient<
         C,
         M,
-        CanTransmitter<
-            C::Instant,
-            SingleQueueDriver<SingleFrameQueue<C::Instant>, BxCanDriver<C::Instant, I>>,
-        >,
-        CanReceiver<
-            C::Instant,
-            SingleQueueDriver<SingleFrameQueue<C::Instant>, BxCanDriver<C::Instant, I>>,
-        >,
+        CanTransmitter<C, SingleQueueDriver<C, SingleFrameQueue<C::Instant>, BxCanDriver<C, I>>>,
+        CanReceiver<C, SingleQueueDriver<C, SingleFrameQueue<C::Instant>, BxCanDriver<C, I>>>,
     >,
-    driver: SingleQueueDriver<SingleFrameQueue<C::Instant>, BxCanDriver<C::Instant, I>>,
+    driver: SingleQueueDriver<C, SingleFrameQueue<C::Instant>, BxCanDriver<C, I>>,
 }
 
 impl<C, M, I> BxCanPnpClient<C, M, I>
@@ -57,8 +51,8 @@ where
     }
 
     /// Handles and parses incoming CAN frames, and returns a node ID if one was received
-    pub fn handle_incoming_frames(&mut self, now: C::Instant) -> Option<CanNodeId> {
-        match self.client.receive(now, &mut self.driver) {
+    pub fn handle_incoming_frames(&mut self, clock: &mut C) -> Option<CanNodeId> {
+        match self.client.receive(clock, &mut self.driver) {
             Ok(Some(id)) => Some(id),
             Ok(None) | Err(_) => None,
         }
diff --git a/canadensis_can/src/data.rs b/canadensis_can/src/data.rs
index 5d99d46..f27e469 100644
--- a/canadensis_can/src/data.rs
+++ b/canadensis_can/src/data.rs
@@ -67,6 +67,20 @@ pub const FRAME_CAPACITY: usize = 8;
 ///
 /// RTR/Error frames are not used and therefore not modeled here.
 /// CAN frames with 11-bit ID are not used by Cyphal/CAN and so they are not supported by the library.
+///
+/// # Loopback
+///
+/// Each frame has a loopback flag.
+///
+/// For an outgoing frame, if loopback is true the driver should place a copy of this frame
+/// in the received frame path (as if it had been received on the bus) with the copy's timestamp
+/// set to the time the frame was transmitted.
+///
+/// For an incoming frame, if loopback is true this frame was not actually received from
+/// another device. The frame timestamp is the time the original frame was sent.
+///
+/// This is useful for time synchronization.
+///
 #[derive(Debug, Clone, PartialEq, Default)]
 pub struct Frame<I> {
     /// For RX frames: reception timestamp.
@@ -75,6 +89,8 @@ pub struct Frame<I> {
     timestamp: I,
     /// 29-bit extended ID
     id: CanId,
+    /// See "Loopback" in the struct documentation
+    loopback: bool,
     /// The frame data
     data: heapless::Vec<u8, FRAME_CAPACITY>,
 }
@@ -82,15 +98,37 @@ pub struct Frame<I> {
 impl<I> Frame<I> {
     /// Creates a frame
     ///
+    /// The loopback flag is set to false.
+    ///
     /// # Panics
     /// This function will panic if the length of data is greater than FRAME_CAPACITY.
     pub fn new(timestamp: I, id: CanId, data: &[u8]) -> Self {
         Frame {
             timestamp,
             id,
+            loopback: false,
             data: heapless::Vec::from_slice(data).expect("Data to large for a frame"),
         }
     }
+
+    /// Sets the timestamp
+    #[inline]
+    pub fn set_timestamp(&mut self, timestamp: I) {
+        self.timestamp = timestamp;
+    }
+
+    /// Sets the loopback flag
+    #[inline]
+    pub fn set_loopback(&mut self, loopback: bool) {
+        self.loopback = loopback
+    }
+
+    /// Returns the loopback flag
+    #[inline]
+    pub fn loopback(&self) -> bool {
+        self.loopback
+    }
+
     /// Returns the ID of this frame
     #[inline]
     pub fn id(&self) -> CanId {
diff --git a/canadensis_can/src/driver.rs b/canadensis_can/src/driver.rs
index 7da5d76..c3e3352 100644
--- a/canadensis_can/src/driver.rs
+++ b/canadensis_can/src/driver.rs
@@ -4,6 +4,7 @@ use crate::data::Frame;
 use crate::types::CanNodeId;
 use alloc::vec::Vec;
 use canadensis_core::subscription::Subscription;
+use canadensis_core::time::Clock;
 use canadensis_core::{nb, OutOfMemoryError, ServiceId, SubjectId};
 use canadensis_filter_config::{optimize, Filter};
 use core::fmt::Debug;
@@ -16,7 +17,10 @@ use fallible_collections::FallibleVec;
 ///
 /// The result type is `nb::Result`, which allows the driver to indicate that it cannot send a
 /// frame.
-pub trait TransmitDriver<I> {
+pub trait TransmitDriver<C>
+where
+    C: Clock,
+{
     /// The error type
     type Error: Debug;
 
@@ -30,21 +34,28 @@ pub trait TransmitDriver<I> {
     ///
     /// If this driver contains a queue, this function may add the frame to the queue and not
     /// immediately transmit until `flush()` is called.
-    fn transmit(&mut self, frame: Frame<I>, now: I) -> nb::Result<Option<Frame<I>>, Self::Error>;
+    fn transmit(
+        &mut self,
+        frame: Frame<C::Instant>,
+        clock: &mut C,
+    ) -> nb::Result<Option<Frame<C::Instant>>, Self::Error>;
     /// Attempts to flush all frames out of any in-memory queues that may exist and transmit
     /// them
-    fn flush(&mut self, now: I) -> nb::Result<(), Self::Error>;
+    fn flush(&mut self, clock: &mut C) -> nb::Result<(), Self::Error>;
 }
 
 /// A CAN driver that can receive frames
 ///
 /// The result type is `nb::Result`, which allows the driver to indicate that no frame is available
 /// to receive.
-pub trait ReceiveDriver<I> {
+pub trait ReceiveDriver<C>
+where
+    C: Clock,
+{
     /// The error type
     type Error: Debug;
     /// Attempts to receive a frame without blocking
-    fn receive(&mut self, now: I) -> nb::Result<Frame<I>, Self::Error>;
+    fn receive(&mut self, clock: &mut C) -> nb::Result<Frame<C::Instant>, Self::Error>;
 
     /// Sets up frame reception filters to accept only frames matching the provided subscriptions
     ///
diff --git a/canadensis_can/src/queue/mod.rs b/canadensis_can/src/queue/mod.rs
index d81b665..966a7db 100644
--- a/canadensis_can/src/queue/mod.rs
+++ b/canadensis_can/src/queue/mod.rs
@@ -8,12 +8,13 @@ pub use self::array_queue::ArrayQueue;
 pub use self::queue_only_driver::QueueOnlyDriver;
 pub use self::single_frame_queue::SingleFrameQueue;
 use core::cmp::Ordering;
+use core::marker::PhantomData;
 
 use crate::driver::{ReceiveDriver, TransmitDriver};
 use crate::types::CanNodeId;
 use crate::Frame;
 use canadensis_core::subscription::Subscription;
-use canadensis_core::time::Instant;
+use canadensis_core::time::{Clock, Instant};
 use canadensis_core::{nb, OutOfMemoryError};
 
 /// A queue of outgoing frames
@@ -49,15 +50,20 @@ pub trait FrameQueue<I> {
 }
 
 /// A single transmit queue and a single driver
-pub struct SingleQueueDriver<Q, D> {
+pub struct SingleQueueDriver<C, Q, D> {
     queue: Q,
     driver: D,
+    _clock: PhantomData<C>,
 }
 
-impl<Q, D> SingleQueueDriver<Q, D> {
+impl<C, Q, D> SingleQueueDriver<C, Q, D> {
     /// Creates a queue and driver pair
     pub fn new(queue: Q, driver: D) -> Self {
-        SingleQueueDriver { queue, driver }
+        SingleQueueDriver {
+            queue,
+            driver,
+            _clock: PhantomData,
+        }
     }
 
     /// Breaks down this queue driver into its queue and driver
@@ -75,11 +81,11 @@ impl<Q, D> SingleQueueDriver<Q, D> {
     }
 }
 
-impl<I, Q, D> TransmitDriver<I> for SingleQueueDriver<Q, D>
+impl<C, Q, D> TransmitDriver<C> for SingleQueueDriver<C, Q, D>
 where
-    I: Instant,
-    Q: FrameQueue<I>,
-    D: TransmitDriver<I>,
+    C: Clock,
+    Q: FrameQueue<C::Instant>,
+    D: TransmitDriver<C>,
 {
     type Error = D::Error;
 
@@ -90,7 +96,11 @@ where
     /// Adds a frame to the back of the queue
     ///
     /// This function returns `Err(nb::Error::WouldBlock)` if the queue is full.
-    fn transmit(&mut self, frame: Frame<I>, _now: I) -> nb::Result<Option<Frame<I>>, Self::Error> {
+    fn transmit(
+        &mut self,
+        frame: Frame<C::Instant>,
+        _clock: &mut C,
+    ) -> nb::Result<Option<Frame<C::Instant>>, Self::Error> {
         self.queue
             .push_frame(frame)
             .map(|_oom| None)
@@ -98,19 +108,20 @@ where
     }
 
     /// Attempts to send all queued frames to the driver
-    fn flush(&mut self, now: I) -> nb::Result<(), Self::Error> {
-        flush_single_queue(&mut self.queue, &mut self.driver, now)
+    fn flush(&mut self, clock: &mut C) -> nb::Result<(), Self::Error> {
+        flush_single_queue(&mut self.queue, &mut self.driver, clock)
     }
 }
 
-impl<I, Q, D> ReceiveDriver<I> for SingleQueueDriver<Q, D>
+impl<C, Q, D> ReceiveDriver<C> for SingleQueueDriver<C, Q, D>
 where
-    D: ReceiveDriver<I>,
+    C: Clock,
+    D: ReceiveDriver<C>,
 {
     type Error = D::Error;
 
-    fn receive(&mut self, now: I) -> nb::Result<Frame<I>, Self::Error> {
-        self.driver.receive(now)
+    fn receive(&mut self, clock: &mut C) -> nb::Result<Frame<C::Instant>, Self::Error> {
+        self.driver.receive(clock)
     }
 
     fn apply_filters<S>(&mut self, local_node: Option<CanNodeId>, subscriptions: S)
@@ -129,24 +140,25 @@ where
 ///
 /// This function discards frames with a deadline less than the current time (`now`).
 ///
-pub fn flush_single_queue<I, Q, D>(
+pub fn flush_single_queue<C, Q, D>(
     queue: &mut Q,
     driver: &mut D,
-    now: I,
+    clock: &mut C,
 ) -> nb::Result<(), D::Error>
 where
-    I: Instant,
-    Q: FrameQueue<I>,
-    D: TransmitDriver<I>,
+    C: Clock,
+    Q: FrameQueue<C::Instant>,
+    D: TransmitDriver<C>,
 {
     while let Some(frame) = queue.pop_frame() {
+        let now = clock.now();
         if frame_is_expired(&frame, &now) {
             // Frame deadline has passed
             drop(frame);
             continue;
         }
 
-        match driver.transmit(frame.clone(), now) {
+        match driver.transmit(frame.clone(), clock) {
             Ok(None) => { /* Transmitted, keep going and try the next frame */ }
             Ok(Some(removed_frame)) => {
                 // Removed a lower-priority frame
diff --git a/canadensis_can/src/queue/queue_only_driver.rs b/canadensis_can/src/queue/queue_only_driver.rs
index 99a3d57..6b8631f 100644
--- a/canadensis_can/src/queue/queue_only_driver.rs
+++ b/canadensis_can/src/queue/queue_only_driver.rs
@@ -7,6 +7,7 @@ use crate::types::CanNodeId;
 use crate::Frame;
 use alloc::vec::Vec;
 use canadensis_core::subscription::Subscription;
+use canadensis_core::time::Clock;
 use canadensis_core::{nb, OutOfMemoryError};
 use core::convert::Infallible;
 use fallible_collections::FallibleVec;
@@ -85,8 +86,10 @@ impl<I: Default, const TC: usize, const RC: usize> Default for QueueOnlyDriver<I
     }
 }
 
-impl<I: Default + Clone, const TC: usize, const RC: usize> TransmitDriver<I>
-    for QueueOnlyDriver<I, TC, RC>
+impl<C: Clock, const TC: usize, const RC: usize> TransmitDriver<C>
+    for QueueOnlyDriver<C::Instant, TC, RC>
+where
+    C::Instant: Default,
 {
     type Error = Infallible;
 
@@ -94,25 +97,29 @@ impl<I: Default + Clone, const TC: usize, const RC: usize> TransmitDriver<I>
         self.tx_queue.try_reserve(frames)
     }
 
-    fn transmit(&mut self, frame: Frame<I>, _now: I) -> nb::Result<Option<Frame<I>>, Self::Error> {
+    fn transmit(
+        &mut self,
+        frame: Frame<C::Instant>,
+        _clock: &mut C,
+    ) -> nb::Result<Option<Frame<C::Instant>>, Self::Error> {
         self.tx_queue
             .push_frame(frame)
             .map(|_oom| None)
             .map_err(|_oom| nb::Error::WouldBlock)
     }
 
-    fn flush(&mut self, _now: I) -> nb::Result<(), Self::Error> {
+    fn flush(&mut self, _clock: &mut C) -> nb::Result<(), Self::Error> {
         // Can't do anything here. Frames have to be removed externally.
         Ok(())
     }
 }
 
-impl<I: Default + Clone, const TC: usize, const RC: usize> ReceiveDriver<I>
-    for QueueOnlyDriver<I, TC, RC>
+impl<C: Clock, const TC: usize, const RC: usize> ReceiveDriver<C>
+    for QueueOnlyDriver<C::Instant, TC, RC>
 {
     type Error = Infallible;
 
-    fn receive(&mut self, _now: I) -> nb::Result<Frame<I>, Self::Error> {
+    fn receive(&mut self, _clock: &mut C) -> nb::Result<Frame<C::Instant>, Self::Error> {
         self.rx_queue.pop_front().ok_or(nb::Error::WouldBlock)
     }
 
diff --git a/canadensis_can/src/redundant/redundant_queue.rs b/canadensis_can/src/redundant/redundant_queue.rs
index c4463c7..75c9055 100644
--- a/canadensis_can/src/redundant/redundant_queue.rs
+++ b/canadensis_can/src/redundant/redundant_queue.rs
@@ -1,5 +1,6 @@
 use crate::driver::TransmitDriver;
 use crate::Frame;
+use canadensis_core::time::Clock;
 use canadensis_core::{nb, OutOfMemoryError};
 
 /// An aggregation of two outgoing frame queues that can be used for double-redundant transports
@@ -32,11 +33,11 @@ impl<D0, D1> RedundantDriver<D0, D1> {
     }
 }
 
-impl<I, D0, D1> TransmitDriver<I> for RedundantDriver<D0, D1>
+impl<C, D0, D1> TransmitDriver<C> for RedundantDriver<D0, D1>
 where
-    I: Clone,
-    D0: TransmitDriver<I>,
-    D1: TransmitDriver<I>,
+    C: Clock,
+    D0: TransmitDriver<C>,
+    D1: TransmitDriver<C>,
 {
     type Error = RedundantError<D0::Error, D1::Error>;
     /// Tries to reserve space on both queues, returning `Ok(())` if the operation succeeded
@@ -55,16 +56,20 @@ where
     /// If a driver failed to allocate memory during the most recent call to
     /// [`try_reserve`](#method.try_reserve), this function does not attempt to push a frame onto
     /// that queue.
-    fn transmit(&mut self, frame: Frame<I>, now: I) -> nb::Result<Option<Frame<I>>, Self::Error> {
+    fn transmit(
+        &mut self,
+        frame: Frame<C::Instant>,
+        clock: &mut C,
+    ) -> nb::Result<Option<Frame<C::Instant>>, Self::Error> {
         // If a queue failed to reserve memory in the last call to try_reserve(),
         // don't try to push the frame there.
         let push_status_0 = if self.status0.is_ok() {
-            self.driver0.transmit(frame.clone(), now.clone())
+            self.driver0.transmit(frame.clone(), clock)
         } else {
             Err(nb::Error::WouldBlock)
         };
         let push_status_1 = if self.status1.is_ok() {
-            self.driver1.transmit(frame, now)
+            self.driver1.transmit(frame, clock)
         } else {
             Err(nb::Error::WouldBlock)
         };
@@ -83,9 +88,9 @@ where
         }
     }
 
-    fn flush(&mut self, now: I) -> nb::Result<(), Self::Error> {
-        let flush_status_0 = self.driver0.flush(now.clone());
-        let flush_status_1 = self.driver1.flush(now);
+    fn flush(&mut self, clock: &mut C) -> nb::Result<(), Self::Error> {
+        let flush_status_0 = self.driver0.flush(clock);
+        let flush_status_1 = self.driver1.flush(clock);
         // This is successful if at least one driver successfully flushed its frames
         match (flush_status_0, flush_status_1) {
             (Ok(()), _) => Ok(()),
diff --git a/canadensis_can/src/rx.rs b/canadensis_can/src/rx.rs
index a477da3..8c1d573 100644
--- a/canadensis_can/src/rx.rs
+++ b/canadensis_can/src/rx.rs
@@ -19,7 +19,7 @@ use crate::rx::session::SessionError;
 use crate::rx::subscription::{Subscription, SubscriptionError};
 use crate::types::{CanNodeId, CanTransferId, CanTransport, Error};
 use crate::Mtu;
-use canadensis_core::time::Instant;
+use canadensis_core::time::{Clock, Instant};
 use canadensis_core::transfer::{Header, MessageHeader, ServiceHeader, Transfer};
 use canadensis_core::transport::Receiver;
 use canadensis_core::{
@@ -28,13 +28,13 @@ use canadensis_core::{
 
 /// Handles subscriptions and assembles incoming frames into transfers
 #[derive(Debug)]
-pub struct CanReceiver<I: Instant, D> {
+pub struct CanReceiver<C: Clock, D> {
     /// Subscriptions for messages
-    subscriptions_message: Vec<Subscription<I>>,
+    subscriptions_message: Vec<Subscription<C::Instant>>,
     /// Subscriptions for service responses
-    subscriptions_response: Vec<Subscription<I>>,
+    subscriptions_response: Vec<Subscription<C::Instant>>,
     /// Subscriptions for service requests
-    subscriptions_request: Vec<Subscription<I>>,
+    subscriptions_request: Vec<Subscription<C::Instant>>,
     /// The ID of this node, or None if this node is anonymous
     id: Option<CanNodeId>,
     /// MTU of the transport
@@ -50,10 +50,10 @@ pub struct CanReceiver<I: Instant, D> {
     _driver: PhantomData<D>,
 }
 
-impl<I, D> Receiver<I> for CanReceiver<I, D>
+impl<C, D> Receiver<C> for CanReceiver<C, D>
 where
-    I: Instant,
-    D: ReceiveDriver<I>,
+    C: Clock,
+    D: ReceiveDriver<C>,
 {
     type Transport = CanTransport;
     type Driver = D;
@@ -61,15 +61,15 @@ where
 
     fn receive(
         &mut self,
-        now: I,
+        clock: &mut C,
         driver: &mut Self::Driver,
-    ) -> Result<Option<Transfer<Vec<u8>, I, Self::Transport>>, Self::Error> {
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, Self::Transport>>, Self::Error> {
         // The current time is equal to or greater than the frame timestamp. Use that timestamp
         // to clean up expired sessions.
-        self.clean_expired_sessions(now);
+        self.clean_expired_sessions(clock.now());
         // Loop until all available frames have been handled
         loop {
-            match driver.receive(now) {
+            match driver.receive(clock) {
                 Ok(frame) => {
                     match self.accept_frame(frame) {
                         Ok(Some(transfer)) => break Ok(Some(transfer)),
@@ -101,7 +101,7 @@ where
         &mut self,
         subject: SubjectId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), Self::Error> {
         self.subscribe(
@@ -142,7 +142,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         if self.id.is_some() {
@@ -188,7 +188,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         if self.id.is_some() {
@@ -213,10 +213,10 @@ where
     }
 }
 
-impl<I, D> CanReceiver<I, D>
+impl<C, D> CanReceiver<C, D>
 where
-    I: Instant,
-    D: ReceiveDriver<I>,
+    C: Clock,
+    D: ReceiveDriver<C>,
 {
     /// Creates a receiver
     ///
@@ -266,8 +266,8 @@ where
     /// not subscribed to will be silently ignored.
     fn accept_frame(
         &mut self,
-        frame: Frame<I>,
-    ) -> Result<Option<Transfer<Vec<u8>, I, CanTransport>>, OutOfMemoryError> {
+        frame: Frame<C::Instant>,
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, CanTransport>>, OutOfMemoryError> {
         // Part 1: basic frame checks
         let (frame_header, tail) = match Self::frame_sanity_check(&frame) {
             Some(data) => data,
@@ -279,28 +279,31 @@ where
             }
         };
         // Check that the frame is actually destined for this node, and this node can handle services
+        // Exception: Loopback frames came from this node and are always accepted
         if let Header::Request(service_header) | Header::Response(service_header) = &frame_header {
-            if let Some(this_id) = self.id {
-                if service_header.destination != this_id {
-                    // This frame is a service request or response going to some other node
-                    return Ok(None);
-                }
-            } else {
-                // This node is anonymous, so it must ignore all service frames
+            if !(frame.loopback() || self.can_accept_service(service_header)) {
                 return Ok(None);
             }
         }
-
         self.accept_sane_frame(frame, frame_header, tail)
     }
 
+    /// Returns true if this node is not anonymous and matches the destination node ID of the
+    /// provided service header
+    fn can_accept_service(&self, service_header: &ServiceHeader<C::Instant, CanTransport>) -> bool {
+        match self.id {
+            Some(local_id) if local_id == service_header.destination => true,
+            Some(_) | None => false,
+        }
+    }
+
     /// Handles an incoming frame that has passed sanity checks and has a parsed header and tail byte
     fn accept_sane_frame(
         &mut self,
-        frame: Frame<I>,
-        frame_header: Header<I, CanTransport>,
+        frame: Frame<C::Instant>,
+        frame_header: Header<C::Instant, CanTransport>,
         tail: TailByte,
-    ) -> Result<Option<Transfer<Vec<u8>, I, CanTransport>>, OutOfMemoryError> {
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, CanTransport>>, OutOfMemoryError> {
         let kind = TransferKind::from_header(&frame_header);
         let subscriptions = self.subscriptions_for_kind(kind);
         if let Some(subscription) = subscriptions
@@ -334,7 +337,9 @@ where
 
     /// Runs basic sanity checks on an incoming frame. Returns the header and tail byte if the frame
     /// is valid.
-    fn frame_sanity_check(frame: &Frame<I>) -> Option<(Header<I, CanTransport>, TailByte)> {
+    fn frame_sanity_check(
+        frame: &Frame<C::Instant>,
+    ) -> Option<(Header<C::Instant, CanTransport>, TailByte)> {
         // Frame must have a tail byte to be valid
         let tail_byte = TailByte::parse(*frame.data().last()?);
 
@@ -360,7 +365,7 @@ where
         kind: TransferKind,
         port_id: PortId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
     ) -> Result<(), OutOfMemoryError> {
         // Remove any existing subscription, ignore result
         self.unsubscribe(kind, port_id);
@@ -382,7 +387,7 @@ where
         subscriptions.retain(|sub| sub.port_id() != port_id);
     }
 
-    fn subscriptions_for_kind(&mut self, kind: TransferKind) -> &mut Vec<Subscription<I>> {
+    fn subscriptions_for_kind(&mut self, kind: TransferKind) -> &mut Vec<Subscription<C::Instant>> {
         match kind {
             TransferKind::Message => &mut self.subscriptions_message,
             TransferKind::Response => &mut self.subscriptions_response,
@@ -410,7 +415,7 @@ where
     }
 
     /// Deletes all sessions that have expired
-    fn clean_expired_sessions(&mut self, now: I) {
+    fn clean_expired_sessions(&mut self, now: C::Instant) {
         clean_sessions_from_subscriptions(&mut self.subscriptions_message, &now);
         clean_sessions_from_subscriptions(&mut self.subscriptions_request, &now);
         clean_sessions_from_subscriptions(&mut self.subscriptions_response, &now);
diff --git a/canadensis_can/src/rx/session.rs b/canadensis_can/src/rx/session.rs
index 07897d9..a4cdd72 100644
--- a/canadensis_can/src/rx/session.rs
+++ b/canadensis_can/src/rx/session.rs
@@ -12,6 +12,8 @@ use core::fmt::Debug;
 pub struct Session<I> {
     /// Timestamp of the first frame received in this transfer
     transfer_timestamp: I,
+    /// Loopback flag of the first frame received in this transfer
+    loopback: bool,
     /// Transfer reassembly
     buildup: Buildup,
 }
@@ -28,9 +30,11 @@ where
         transfer_timestamp: I,
         transfer_id: CanTransferId,
         max_payload_length: usize,
+        loopback: bool,
     ) -> Result<Self, OutOfMemoryError> {
         Ok(Session {
             transfer_timestamp,
+            loopback,
             buildup: Buildup::new(transfer_id, max_payload_length)?,
         })
     }
@@ -55,6 +59,10 @@ where
             log::info!("Frame transfer ID does not match, ignoring");
             return Ok(None);
         }
+        if frame.loopback() != self.loopback {
+            log::info!("Frame loopback flag does not match, ignoring");
+            return Ok(None);
+        }
         // Check if this frame will make the transfer exceed the maximum length
         let new_payload_length = self.buildup.payload_length() + (frame.data().len() - 1);
         if new_payload_length > max_payload_length {
@@ -108,6 +116,7 @@ where
 
         Ok(Some(Transfer {
             header: transfer_header,
+            loopback: self.loopback,
             payload: transfer_data,
         }))
     }
diff --git a/canadensis_can/src/rx/subscription.rs b/canadensis_can/src/rx/subscription.rs
index d1ecf4c..cc8f33e 100644
--- a/canadensis_can/src/rx/subscription.rs
+++ b/canadensis_can/src/rx/subscription.rs
@@ -98,6 +98,7 @@ impl<I: Instant> Subscription<I> {
             payload.try_extend_from_slice(data_without_tail)?;
             let transfer = Transfer {
                 header: frame_header,
+                loopback: frame.loopback(),
                 payload,
             };
             Ok(Some(transfer))
@@ -138,6 +139,7 @@ impl<I: Instant> Subscription<I> {
                     frame_header.timestamp(),
                     tail.transfer_id,
                     self.payload_size_max,
+                    frame.loopback(),
                 )?)?);
                 log::debug!(
                     "Created new session for transfer ID {:?} on port {:?}",
@@ -186,6 +188,7 @@ impl<I: Instant> Subscription<I> {
 
         Ok(Some(Transfer {
             header: frame_header,
+            loopback: frame.loopback(),
             payload: transfer_data,
         }))
     }
diff --git a/canadensis_can/src/tx.rs b/canadensis_can/src/tx.rs
index 5cd3293..1ded13e 100644
--- a/canadensis_can/src/tx.rs
+++ b/canadensis_can/src/tx.rs
@@ -7,7 +7,7 @@ use core::iter;
 use core::marker::PhantomData;
 
 use canadensis_core::nb;
-use canadensis_core::time::{Clock, Instant};
+use canadensis_core::time::Clock;
 use canadensis_core::transfer::{Header, ServiceHeader, Transfer};
 use canadensis_core::transport::Transmitter;
 
@@ -23,7 +23,7 @@ mod breakdown;
 mod tx_test;
 
 /// Splits outgoing transfers into frames
-pub struct CanTransmitter<I, D> {
+pub struct CanTransmitter<C, D> {
     /// Transport MTU (including the tail byte)
     mtu: usize,
     /// Number of transfers successfully transmitted
@@ -35,14 +35,14 @@ pub struct CanTransmitter<I, D> {
     ///
     /// A failure to allocate memory is considered an error. CAN bus errors are ignored.
     error_count: u64,
-    _instant: PhantomData<I>,
+    _clock: PhantomData<C>,
     _driver: PhantomData<D>,
 }
 
-impl<I, D> Transmitter<I> for CanTransmitter<I, D>
+impl<C, D> Transmitter<C> for CanTransmitter<C, D>
 where
-    I: Instant,
-    D: TransmitDriver<I>,
+    C: Clock,
+    D: TransmitDriver<C>,
 {
     type Transport = CanTransport;
     type Driver = D;
@@ -54,23 +54,23 @@ where
     ///
     /// This function returns an error if the queue does not have enough space to hold all
     /// the required frames.
-    fn push<A, C>(
+    fn push<A>(
         &mut self,
-        transfer: Transfer<A, I, CanTransport>,
+        transfer: Transfer<A, C::Instant, CanTransport>,
         clock: &mut C,
         driver: &mut D,
     ) -> nb::Result<(), Self::Error>
     where
         A: AsRef<[u8]>,
-        C: Clock<Instant = I>,
     {
         // Convert the transfer payload into borrowed form
         let transfer = Transfer {
             header: transfer.header,
+            loopback: transfer.loopback,
             payload: transfer.payload.as_ref(),
         };
 
-        match self.push_inner(transfer, clock.now(), driver) {
+        match self.push_inner(transfer, clock, driver) {
             Ok(()) => {
                 self.transfer_count = self.transfer_count.wrapping_add(1);
                 Ok(())
@@ -82,11 +82,8 @@ where
         }
     }
 
-    fn flush<C>(&mut self, clock: &mut C, driver: &mut D) -> nb::Result<(), Self::Error>
-    where
-        C: Clock<Instant = I>,
-    {
-        driver.flush(clock.now()).map_err(|e| e.map(Error::Driver))
+    fn flush(&mut self, clock: &mut C, driver: &mut D) -> nb::Result<(), Self::Error> {
+        driver.flush(clock).map_err(|e| e.map(Error::Driver))
     }
 
     fn mtu(&self) -> usize {
@@ -95,9 +92,10 @@ where
     }
 }
 
-impl<I, D> CanTransmitter<I, D>
+impl<C, D> CanTransmitter<C, D>
 where
-    D: TransmitDriver<I>,
+    C: Clock,
+    D: TransmitDriver<C>,
 {
     /// Creates a transmitter
     ///
@@ -107,7 +105,7 @@ where
             mtu: mtu as usize,
             transfer_count: 0,
             error_count: 0,
-            _instant: PhantomData,
+            _clock: PhantomData,
             _driver: PhantomData,
         }
     }
@@ -121,13 +119,10 @@ where
 
     fn push_inner(
         &mut self,
-        transfer: Transfer<&[u8], I, CanTransport>,
-        now: I,
+        transfer: Transfer<&[u8], C::Instant, CanTransport>,
+        clock: &mut C,
         driver: &mut D,
-    ) -> nb::Result<(), Error<D::Error>>
-    where
-        I: Clone,
-    {
+    ) -> nb::Result<(), Error<D::Error>> {
         let frame_stats = crate::calculate_frame_stats(transfer.payload.len(), self.mtu);
         // Check that enough space is available in the queue for all the frames.
         // Return an error if space is not available.
@@ -153,10 +148,11 @@ where
                 // Filled up a frame
                 self.push_frame(
                     transfer.header.timestamp(),
+                    transfer.loopback,
                     can_id,
                     &frame_data,
                     driver,
-                    now.clone(),
+                    clock,
                 )
                 .map_err(|e| e.map(Error::Driver))?;
                 frames += 1;
@@ -173,10 +169,11 @@ where
                     // Filled up a frame
                     self.push_frame(
                         transfer.header.timestamp(),
+                        transfer.loopback,
                         can_id,
                         &frame_data,
                         driver,
-                        now.clone(),
+                        clock,
                     )
                     .map_err(|e| e.map(Error::Driver))?;
                 }
@@ -185,10 +182,11 @@ where
         let last_frame_data = breakdown.finish();
         self.push_frame(
             transfer.header.timestamp(),
+            transfer.loopback,
             can_id,
             &last_frame_data,
             driver,
-            now,
+            clock,
         )
         .map_err(|e| e.map(Error::Driver))?;
         Ok(())
@@ -199,18 +197,17 @@ where
     /// If the driver returns a removed lower-priority frame, this function discards it.
     fn push_frame(
         &mut self,
-        timestamp: I,
+        timestamp: C::Instant,
+        loopback: bool,
         id: CanId,
         data: &[u8],
         driver: &mut D,
-        now: I,
-    ) -> nb::Result<(), D::Error>
-    where
-        I: Clone,
-    {
-        let frame = Frame::new(timestamp, id, data);
+        clock: &mut C,
+    ) -> nb::Result<(), D::Error> {
+        let mut frame = Frame::new(timestamp, id, data);
+        frame.set_loopback(loopback);
         // If a lower-priority frame was removed, drop it
-        driver.transmit(frame, now).map(drop)
+        driver.transmit(frame, clock).map(drop)
     }
 
     /// Returns the number of transfers successfully transmitted
diff --git a/canadensis_codegen_rust/src/main.rs b/canadensis_codegen_rust/src/main.rs
index 830abca..62daf06 100644
--- a/canadensis_codegen_rust/src/main.rs
+++ b/canadensis_codegen_rust/src/main.rs
@@ -173,7 +173,7 @@ fn get_args() -> Args {
 
 /// Validates an external package name pair
 fn validate_external_package(package: &str) -> Result<ExternalPackage, String> {
-    ExternalPackage::parse(&package).ok_or_else(|| {
+    ExternalPackage::parse(package).ok_or_else(|| {
         "Invalid external package, expected [cyphal-package],[rust-module-path]".into()
     })
 }
diff --git a/canadensis_core/src/transfer.rs b/canadensis_core/src/transfer.rs
index db81fd4..e279632 100644
--- a/canadensis_core/src/transfer.rs
+++ b/canadensis_core/src/transfer.rs
@@ -276,6 +276,15 @@ impl<I, T: Transport + ?Sized> Header<I, T> {
 pub struct Transfer<A, I, T: Transport + ?Sized> {
     /// The transfer header
     pub header: Header<I, T>,
+    /// The loopback flag
+    ///
+    /// The exact meaning of this flag depends on the transport. Generally, when a transport
+    /// handles an outgoing loopback transfer, it creates a duplicate transfer with the loopback
+    /// flag set to true and sends that transfer back through the local receiving process.
+    ///
+    /// If the transport does not support loopback, this flag has no effect.
+    ///
+    pub loopback: bool,
     /// The actual transfer payload
     ///
     /// The type A usually implements `AsRef<[u8]>`. It is often a `Vec<u8>` or a `&[u8]`.
@@ -290,6 +299,7 @@ where
     fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
         f.debug_struct("Transfer")
             .field("header", &self.header)
+            .field("loopback", &self.loopback)
             .field("payload", &self.payload)
             .finish()
     }
@@ -304,7 +314,9 @@ where
     T::NodeId: PartialEq,
 {
     fn eq(&self, other: &Self) -> bool {
-        self.header == other.header && self.payload == other.payload
+        self.header == other.header
+            && self.loopback == other.loopback
+            && self.payload == other.payload
     }
 }
 impl<A, I, T: Transport + ?Sized> Clone for Transfer<A, I, T>
@@ -318,6 +330,7 @@ where
     fn clone(&self) -> Self {
         Transfer {
             header: self.header.clone(),
+            loopback: self.loopback,
             payload: self.payload.clone(),
         }
     }
@@ -328,6 +341,15 @@ where
 pub struct MessageTransfer<A, I, T: Transport + ?Sized> {
     /// The transfer header
     pub header: MessageHeader<I, T>,
+    /// The loopback flag
+    ///
+    /// The exact meaning of this flag depends on the transport. Generally, when a transport
+    /// handles an outgoing loopback transfer, it creates a duplicate transfer with the loopback
+    /// flag set to true and sends that transfer back through the local receiving process.
+    ///
+    /// If the transport does not support loopback, this flag has no effect.
+    ///
+    pub loopback: bool,
     /// The actual transfer payload
     ///
     /// The type A usually implements `AsRef<[u8]>`. It is often a `Vec<u8>` or a `&[u8]`.
@@ -342,6 +364,7 @@ where
     fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
         f.debug_struct("Transfer")
             .field("header", &self.header)
+            .field("loopback", &self.loopback)
             .field("payload", &self.payload)
             .finish()
     }
@@ -356,7 +379,9 @@ where
     T::NodeId: PartialEq,
 {
     fn eq(&self, other: &Self) -> bool {
-        self.header == other.header && self.payload == other.payload
+        self.header == other.header
+            && self.loopback == other.loopback
+            && self.payload == other.payload
     }
 }
 
@@ -365,6 +390,15 @@ where
 pub struct ServiceTransfer<A, I, T: Transport + ?Sized> {
     /// The transfer header
     pub header: ServiceHeader<I, T>,
+    /// The loopback flag
+    ///
+    /// The exact meaning of this flag depends on the transport. Generally, when a transport
+    /// handles an outgoing loopback transfer, it creates a duplicate transfer with the loopback
+    /// flag set to true and sends that transfer back through the local receiving process.
+    ///
+    /// If the transport does not support loopback, this flag has no effect.
+    ///
+    pub loopback: bool,
     /// The actual transfer payload
     ///
     /// The type A usually implements `AsRef<[u8]>`. It is often a `Vec<u8>` or a `&[u8]`.
@@ -379,6 +413,7 @@ where
     fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
         f.debug_struct("Transfer")
             .field("header", &self.header)
+            .field("loopback", &self.loopback)
             .field("payload", &self.payload)
             .finish()
     }
@@ -393,6 +428,8 @@ where
     T::NodeId: PartialEq,
 {
     fn eq(&self, other: &Self) -> bool {
-        self.header == other.header && self.payload == other.payload
+        self.header == other.header
+            && self.loopback == other.loopback
+            && self.payload == other.payload
     }
 }
diff --git a/canadensis_core/src/transport.rs b/canadensis_core/src/transport.rs
index e3fd9ba..76cc59f 100644
--- a/canadensis_core/src/transport.rs
+++ b/canadensis_core/src/transport.rs
@@ -22,9 +22,9 @@ pub trait Transport {
 }
 
 /// A transmitter that can send outgoing transfers
-pub trait Transmitter<I>
+pub trait Transmitter<C>
 where
-    I: Instant,
+    C: Clock,
 {
     /// The transport that this transmitter works with
     type Transport: Transport;
@@ -39,15 +39,14 @@ where
     ///
     /// The transport implementation may block until the entire transfer is sent, or put frames in
     /// a queue to be sent separately.
-    fn push<A, C>(
+    fn push<A>(
         &mut self,
-        transfer: Transfer<A, I, Self::Transport>,
+        transfer: Transfer<A, C::Instant, Self::Transport>,
         clock: &mut C,
         driver: &mut Self::Driver,
     ) -> nb::Result<(), Self::Error>
     where
-        A: AsRef<[u8]>,
-        C: Clock<Instant = I>;
+        A: AsRef<[u8]>;
 
     /// Attempts to send all queued outgoing frames
     ///
@@ -61,9 +60,7 @@ where
     /// * `Ok(())`: All frames were sent
     /// * `Err(nb::Error::WouldBlock)`: At least one frame could not be sent yet
     /// * `Err(nb::Error::Other(e))`: Some other error occurred
-    fn flush<C>(&mut self, clock: &mut C, driver: &mut Self::Driver) -> nb::Result<(), Self::Error>
-    where
-        C: Clock<Instant = I>;
+    fn flush(&mut self, clock: &mut C, driver: &mut Self::Driver) -> nb::Result<(), Self::Error>;
 
     /// Returns the maximum transmission unit of this transport, in bytes
     ///
@@ -75,9 +72,9 @@ where
 }
 
 /// A receiver that can assemble incoming frames into transfers
-pub trait Receiver<I>
+pub trait Receiver<C>
 where
-    I: Instant,
+    C: Clock,
 {
     /// The transport that this transmitter works with
     type Transport: Transport;
@@ -106,9 +103,9 @@ where
     /// incoming frames and delete sessions that have timed out.
     fn receive(
         &mut self,
-        now: I,
+        clock: &mut C,
         driver: &mut Self::Driver,
-    ) -> Result<Option<Transfer<Vec<u8>, I, Self::Transport>>, Self::Error>;
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, Self::Transport>>, Self::Error>;
 
     /// Subscribes to messages on a subject
     ///
@@ -128,7 +125,7 @@ where
         &mut self,
         subject: SubjectId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), Self::Error>;
 
@@ -157,7 +154,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), ServiceSubscribeError<Self::Error>>;
 
@@ -186,7 +183,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: I::Duration,
+        timeout: <<C as Clock>::Instant as Instant>::Duration,
         driver: &mut Self::Driver,
     ) -> Result<(), ServiceSubscribeError<Self::Error>>;
 
diff --git a/canadensis_dsdl_frontend/src/compile.rs b/canadensis_dsdl_frontend/src/compile.rs
index 95c00ab..6f9bc52 100644
--- a/canadensis_dsdl_frontend/src/compile.rs
+++ b/canadensis_dsdl_frontend/src/compile.rs
@@ -299,7 +299,7 @@ impl PersistentContext {
                 }
                 Statement::Field { ty, name, span } => {
                     let ty = convert_type(&mut ctx(self, &mut state), ty)?;
-                    let ty = ty.resolve(&mut ctx(self, &mut state), span.clone())?;
+                    let ty = ty.resolve(&mut ctx(self, &mut state), span)?;
                     let ty_alignment = ty.alignment();
                     let ty_length = ty.size();
 
diff --git a/canadensis_dsdl_frontend/src/operators/exponent.rs b/canadensis_dsdl_frontend/src/operators/exponent.rs
index 401a317..dfe2c73 100644
--- a/canadensis_dsdl_frontend/src/operators/exponent.rs
+++ b/canadensis_dsdl_frontend/src/operators/exponent.rs
@@ -38,14 +38,14 @@ fn approx_exponent(
 ) -> Result<Value, Error> {
     let base = base.to_f64().ok_or_else(|| {
         span_error!(
-            span.clone(),
+            span,
             "Can't convert base {} to a floating-point approximation",
             base
         )
     })?;
     let exponent = exponent.to_f64().ok_or_else(|| {
         span_error!(
-            span.clone(),
+            span,
             "Can't convert exponent {} to a floating-point approximation",
             exponent
         )
diff --git a/canadensis_dsdl_frontend/src/operators/mod.rs b/canadensis_dsdl_frontend/src/operators/mod.rs
index bb06888..a57ec02 100644
--- a/canadensis_dsdl_frontend/src/operators/mod.rs
+++ b/canadensis_dsdl_frontend/src/operators/mod.rs
@@ -48,7 +48,7 @@ where
             let new_elements: Result<Set, TypeError> = set
                 .into_iter()
                 .map(|element| match element {
-                    Value::Rational(rhs) => rational_op(lhs.clone(), rhs, span.clone()),
+                    Value::Rational(rhs) => rational_op(lhs.clone(), rhs, span),
                     _ => panic!(
                         "Element {:?} does not match set type {}",
                         element,
@@ -64,7 +64,7 @@ where
             let new_elements: Result<Set, TypeError> = set
                 .into_iter()
                 .map(|element| match element {
-                    Value::Rational(lhs) => rational_op(lhs, rhs.clone(), span.clone()),
+                    Value::Rational(lhs) => rational_op(lhs, rhs.clone(), span),
                     _ => panic!(
                         "Element {:?} does not match set type {}",
                         element,
diff --git a/canadensis_dsdl_frontend/src/types/constant.rs b/canadensis_dsdl_frontend/src/types/constant.rs
index b980d6f..0e86e41 100644
--- a/canadensis_dsdl_frontend/src/types/constant.rs
+++ b/canadensis_dsdl_frontend/src/types/constant.rs
@@ -37,7 +37,7 @@ impl Constant {
         value: Expression,
     ) -> Result<Self, Error> {
         let ty: PrimitiveType = ty.into();
-        let value_span = value.span.clone();
+        let value_span = value.span;
         let end_offset = value_span.end();
         if is_reserved_keyword(name.name) {
             return Err(span_error!(
diff --git a/canadensis_dsdl_frontend/src/types/directive.rs b/canadensis_dsdl_frontend/src/types/directive.rs
index 0b0cae4..7fb049b 100644
--- a/canadensis_dsdl_frontend/src/types/directive.rs
+++ b/canadensis_dsdl_frontend/src/types/directive.rs
@@ -23,7 +23,7 @@ pub(crate) fn evaluate_directive(
         }
         "extent" => match expression {
             Some(expression) => {
-                let expression_span = expression.span.clone();
+                let expression_span = expression.span;
                 match evaluate_expression(cx, expression)? {
                     Value::Rational(value) => {
                         if value.is_integer() && !value.is_negative() {
@@ -86,7 +86,7 @@ pub(crate) fn evaluate_directive(
         }
         "assert" => match expression {
             Some(expr) => {
-                let expr_span = expr.span.clone();
+                let expr_span = expr.span;
                 match evaluate_expression(cx, expr)? {
                     Value::Boolean(true) => Ok(()),
                     Value::Boolean(false) => Err(span_error!(expr_span, "Assertion failed")),
diff --git a/canadensis_dsdl_frontend/src/types/expression.rs b/canadensis_dsdl_frontend/src/types/expression.rs
index bd03ddc..8cc7796 100644
--- a/canadensis_dsdl_frontend/src/types/expression.rs
+++ b/canadensis_dsdl_frontend/src/types/expression.rs
@@ -278,7 +278,7 @@ fn evaluate_array_length(
     cx: &mut CompileContext<'_>,
     length: Expression<'_>,
 ) -> Result<u64, Error> {
-    let length_span = length.span.clone();
+    let length_span = length.span;
     match evaluate_expression(cx, length)? {
         Value::Rational(rational) => {
             if rational.is_integer() {
@@ -359,7 +359,7 @@ pub(crate) fn convert_type(
                     })
                 }
                 canadensis_dsdl_parser::ArrayLength::Exclusive(length) => {
-                    let length_span = length.span.clone();
+                    let length_span = length.span;
                     let length = evaluate_array_length(cx, length)?;
                     if length > 0 {
                         // Convert to inclusive length by subtracting 1
diff --git a/canadensis_dsdl_parser/src/ast.rs b/canadensis_dsdl_parser/src/ast.rs
index 5950874..95d1744 100644
--- a/canadensis_dsdl_parser/src/ast.rs
+++ b/canadensis_dsdl_parser/src/ast.rs
@@ -196,7 +196,7 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result<Expression<'_>, Error> {
 
                 expression = Expression {
                     expression: ExpressionType::Attribute(Box::new(expression), rhs.as_str()),
-                    span: pair_span.clone(),
+                    span: pair_span,
                 };
             }
 
@@ -250,7 +250,7 @@ where
         let new_expr_type = op_handler(operator_rule, Box::new(expression), Box::new(rhs));
         expression = Expression {
             expression: new_expr_type,
-            span: span.clone(),
+            span,
         };
     }
 
@@ -463,7 +463,7 @@ fn parse_real_point_notation(literal: Pair<'_, Rule>) -> Result<BigRational, Err
     let whole_number_digits: BigInt = whole_number_digits
         .as_str()
         .parse()
-        .map_err(|e| make_error(format!("Invalid real literal: {}", e), span.clone()))?;
+        .map_err(|e| make_error(format!("Invalid real literal: {}", e), span))?;
 
     if let Some(fractional_digits) = fractional_digits {
         debug_assert_eq!(fractional_digits.as_rule(), Rule::literal_real_digits);
diff --git a/canadensis_dsdl_parser/src/ast/unescape.rs b/canadensis_dsdl_parser/src/ast/unescape.rs
index 74ded59..c08efad 100644
--- a/canadensis_dsdl_parser/src/ast/unescape.rs
+++ b/canadensis_dsdl_parser/src/ast/unescape.rs
@@ -64,7 +64,7 @@ pub fn unescape_string(s: &str, span: Span<'_>) -> Result<String, Error> {
                     _ => {
                         return Err(make_error(
                             format!("Unexpected character '{}' after \\ in string literal", c),
-                            span.clone(),
+                            span,
                         ))
                     }
                 }
@@ -90,11 +90,11 @@ pub fn unescape_string(s: &str, span: Span<'_>) -> Result<String, Error> {
                             "Invalid 4-digit unicode escape sequence \"\\u{}\": {}",
                             escape_chars, e
                         ),
-                        span.clone(),
+                        span,
                     )
                 })?;
                 let constructed = char::from_u32(value.into()).ok_or_else(||
-                    make_error(format!("Unicode escape sequence \\u{:04x} does not represent a valid scalar value", value), span.clone())
+                    make_error(format!("Unicode escape sequence \\u{:04x} does not represent a valid scalar value", value), span)
                 )?;
                 unescaped.push(constructed);
                 State::Idle
@@ -136,11 +136,11 @@ pub fn unescape_string(s: &str, span: Span<'_>) -> Result<String, Error> {
                             "Invalid 8-digit unicode escape sequence \"\\U{}\": {}",
                             escape_chars, e
                         ),
-                        span.clone(),
+                        span,
                     )
                 })?;
                 let constructed = char::from_u32(value).ok_or_else(||
-                    make_error(format!("Unicode escape sequence \\U{:08x} does not represent a valid scalar value", value), span.clone())
+                    make_error(format!("Unicode escape sequence \\U{:08x} does not represent a valid scalar value", value), span)
                 )?;
                 unescaped.push(constructed);
                 State::Idle
diff --git a/canadensis_linux/src/lib.rs b/canadensis_linux/src/lib.rs
index f01b237..49dbd1b 100644
--- a/canadensis_linux/src/lib.rs
+++ b/canadensis_linux/src/lib.rs
@@ -33,7 +33,7 @@ impl LinuxCan {
     }
 }
 
-impl TransmitDriver<Microseconds64> for LinuxCan {
+impl TransmitDriver<SystemClock> for LinuxCan {
     type Error = io::Error;
 
     fn try_reserve(&mut self, _frames: usize) -> Result<(), OutOfMemoryError> {
@@ -44,9 +44,10 @@ impl TransmitDriver<Microseconds64> for LinuxCan {
     fn transmit(
         &mut self,
         frame: Frame<Microseconds64>,
-        now: Microseconds64,
+        clock: &mut SystemClock,
     ) -> nb::Result<Option<Frame<Microseconds64>>, Self::Error> {
         // Drop this frame if its deadline has passed
+        let now = clock.now();
         if frame.timestamp().overflow_safe_compare(&now) == Ordering::Less {
             log::warn!("Dropping frame that has missed its deadline");
             return Ok(None);
@@ -66,21 +67,24 @@ impl TransmitDriver<Microseconds64> for LinuxCan {
             })
     }
 
-    fn flush(&mut self, _now: Microseconds64) -> canadensis_core::nb::Result<(), Self::Error> {
+    fn flush(&mut self, _clock: &mut SystemClock) -> canadensis_core::nb::Result<(), Self::Error> {
         // Presumably this happens automatically
         Ok(())
     }
 }
 
-impl ReceiveDriver<Microseconds64> for LinuxCan {
+impl ReceiveDriver<SystemClock> for LinuxCan {
     type Error = io::Error;
 
-    fn receive(&mut self, now: Microseconds64) -> nb::Result<Frame<Microseconds64>, Self::Error> {
+    fn receive(
+        &mut self,
+        clock: &mut SystemClock,
+    ) -> nb::Result<Frame<Microseconds64>, Self::Error> {
         loop {
             let socketcan_frame = self.socket.read_frame()?;
             if socketcan_frame.data().len() <= canadensis_can::FRAME_CAPACITY {
                 let cyphal_frame = canadensis_can::Frame::new(
-                    now,
+                    clock.now(),
                     socketcan_frame.id().try_into().expect("Invalid CAN ID"),
                     socketcan_frame.data(),
                 );
diff --git a/canadensis_pnp_client/src/lib.rs b/canadensis_pnp_client/src/lib.rs
index ae8ace4..df54db4 100644
--- a/canadensis_pnp_client/src/lib.rs
+++ b/canadensis_pnp_client/src/lib.rs
@@ -26,7 +26,7 @@ use core::marker::PhantomData;
 use crc_any::CRCu64;
 
 /// A plug-and-play allocation client that can be used to find a node ID
-pub struct PnpClient<C: Clock, M, T: Transmitter<C::Instant>, R: Receiver<C::Instant>> {
+pub struct PnpClient<C: Clock, M, T: Transmitter<C>, R: Receiver<C>> {
     /// The unique ID of this node
     unique_id: [u8; 16],
     /// Publisher used to send messages
@@ -42,8 +42,8 @@ impl<C, M, T, R, P> PnpClient<C, M, T, R>
 where
     C: Clock,
     M: AllocationMessage<P>,
-    T: Transmitter<C::Instant, Transport = P>,
-    R: Receiver<C::Instant, Transport = P>,
+    T: Transmitter<C, Transport = P>,
+    R: Receiver<C, Transport = P>,
     P: Transport,
 {
     /// Creates a new plug-and-play client
@@ -87,10 +87,10 @@ where
     /// This function returns the node ID if one was assigned.
     pub fn receive(
         &mut self,
-        now: C::Instant,
+        clock: &mut C,
         driver: &mut R::Driver,
     ) -> Result<Option<P::NodeId>, R::Error> {
-        if let Some(transfer_in) = self.receiver.receive(now, driver)? {
+        if let Some(transfer_in) = self.receiver.receive(clock, driver)? {
             if let Ok(message) = M::deserialize_from_bytes(&transfer_in.payload) {
                 if message.matches_unique_id(&self.unique_id) {
                     if let Some(node_id) = message.node_id() {
diff --git a/canadensis_serial/src/rx.rs b/canadensis_serial/src/rx.rs
index 743de1c..7127e82 100644
--- a/canadensis_serial/src/rx.rs
+++ b/canadensis_serial/src/rx.rs
@@ -7,7 +7,7 @@ use core::mem;
 use fallible_collections::{FallibleVec, TryHashMap};
 
 use canadensis_core::subscription::SubscriptionManager;
-use canadensis_core::time::Instant;
+use canadensis_core::time::{Clock, Instant};
 use canadensis_core::transfer::{Header, Transfer};
 use canadensis_core::transport::Receiver;
 use canadensis_core::{nb, OutOfMemoryError, ServiceId, ServiceSubscribeError, SubjectId};
@@ -21,18 +21,21 @@ use crate::{make_payload_crc, Error, SerialNodeId, SerialTransferId, SerialTrans
 /// A serial transport receiver
 ///
 /// This implementation does not support multi-frame transfers or timestamps.
-pub struct SerialReceiver<I, D, S> {
-    state: State<I>,
+pub struct SerialReceiver<C, D, S>
+where
+    C: Clock,
+{
+    state: State<C::Instant>,
     node_id: Option<SerialNodeId>,
     subscriptions: S,
     _driver: PhantomData<D>,
 }
 
-impl<I, D, S> SerialReceiver<I, D, S>
+impl<C, D, S> SerialReceiver<C, D, S>
 where
-    I: Instant,
+    C: Clock,
     D: ReceiveDriver,
-    S: SubscriptionManager<Subscription<I>> + Default,
+    S: SubscriptionManager<Subscription<C::Instant>> + Default,
 {
     pub fn new(node_id: SerialNodeId) -> Self {
         SerialReceiver {
@@ -51,7 +54,7 @@ where
         }
     }
 
-    fn clean_expired_sessions(&mut self, now: I) {
+    fn clean_expired_sessions(&mut self, now: C::Instant) {
         self.subscriptions
             .for_each_message_subscription_mut(|sub| sub.clean_expired_sessions(now));
         self.subscriptions
@@ -63,8 +66,8 @@ where
     fn handle_byte(
         &mut self,
         byte: u8,
-        now: I,
-    ) -> Result<Option<Transfer<Vec<u8>, I, SerialTransport>>, Error<D::Error>> {
+        now: C::Instant,
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, SerialTransport>>, Error<D::Error>> {
         let state = mem::replace(&mut self.state, State::Idle);
         self.state = match state {
             State::Idle => {
@@ -195,11 +198,11 @@ where
     }
 }
 
-impl<I, D, S> Receiver<I> for SerialReceiver<I, D, S>
+impl<C, D, S> Receiver<C> for SerialReceiver<C, D, S>
 where
-    I: Instant + Default,
+    C: Clock,
     D: ReceiveDriver,
-    S: SubscriptionManager<Subscription<I>> + Default,
+    S: SubscriptionManager<Subscription<C::Instant>> + Default,
 {
     type Transport = SerialTransport;
     type Driver = D;
@@ -207,13 +210,13 @@ where
 
     fn receive(
         &mut self,
-        now: I,
+        clock: &mut C,
         driver: &mut D,
-    ) -> Result<Option<Transfer<Vec<u8>, I, Self::Transport>>, Self::Error> {
-        self.clean_expired_sessions(now);
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, Self::Transport>>, Self::Error> {
+        self.clean_expired_sessions(clock.now());
         loop {
             match driver.receive_byte() {
-                Ok(byte) => match self.handle_byte(byte, now) {
+                Ok(byte) => match self.handle_byte(byte, clock.now()) {
                     Ok(Some(transfer)) => break Ok(Some(transfer)),
                     Ok(None) => { /* Keep going and try another byte */ }
                     Err(e) => break Err(e),
@@ -228,7 +231,7 @@ where
         &mut self,
         subject: SubjectId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         _driver: &mut D,
     ) -> Result<(), Self::Error> {
         self.subscriptions
@@ -244,7 +247,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         _driver: &mut D,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         if self.node_id.is_some() {
@@ -264,7 +267,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         _driver: &mut D,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         if self.node_id.is_some() {
@@ -281,17 +284,17 @@ where
     }
 }
 
-impl<I, D, S> SerialReceiver<I, D, S>
+impl<C, D, S> SerialReceiver<C, D, S>
 where
-    I: Instant,
-    S: SubscriptionManager<Subscription<I>>,
+    C: Clock,
+    S: SubscriptionManager<Subscription<C::Instant>>,
 {
     /// Finds and returns a subscription that matches the provided header (and, for service
     /// transfers, has this node as its destination) if any exists
     fn find_subscription_mut(
         &mut self,
-        header: &Header<I, SerialTransport>,
-    ) -> Option<&mut Subscription<I>> {
+        header: &Header<C::Instant, SerialTransport>,
+    ) -> Option<&mut Subscription<C::Instant>> {
         match header {
             Header::Message(header) => self
                 .subscriptions
@@ -318,7 +321,10 @@ where
     /// Returns true if this receiver has a matching subscription, its last transfer ID is less
     /// than the provided header's transfer ID, and (for service transfers) this node is the
     /// destination
-    fn is_interested(&self, header: &Header<I, SerialTransport>) -> Option<&Subscription<I>> {
+    fn is_interested(
+        &self,
+        header: &Header<C::Instant, SerialTransport>,
+    ) -> Option<&Subscription<C::Instant>> {
         self.subscriptions
             .find_subscription(header)
             .and_then(|subscription| {
@@ -349,9 +355,9 @@ where
 
     fn complete_transfer(
         &mut self,
-        header: Header<I, SerialTransport>,
+        header: Header<C::Instant, SerialTransport>,
         mut payload_and_crc: Vec<u8>,
-    ) -> Option<Transfer<Vec<u8>, I, SerialTransport>> {
+    ) -> Option<Transfer<Vec<u8>, C::Instant, SerialTransport>> {
         if payload_and_crc.len() >= 4 {
             let mut crc_bytes = [0u8; 4];
             crc_bytes.copy_from_slice(&payload_and_crc[payload_and_crc.len() - 4..]);
@@ -377,7 +383,11 @@ where
                         },
                     );
                 }
-                Some(Transfer { header, payload })
+                Some(Transfer {
+                    header,
+                    loopback: false,
+                    payload,
+                })
             } else {
                 // The subscription was removed while receiving the transfer
                 None
diff --git a/canadensis_serial/src/tx.rs b/canadensis_serial/src/tx.rs
index c30b4ff..beb5004 100644
--- a/canadensis_serial/src/tx.rs
+++ b/canadensis_serial/src/tx.rs
@@ -5,7 +5,7 @@ use fallible_collections::FallibleVec;
 use heapless::Deque;
 use zerocopy::AsBytes;
 
-use canadensis_core::time::{Clock, Instant};
+use canadensis_core::time::Clock;
 use canadensis_core::transfer::Transfer;
 use canadensis_core::transport::Transmitter;
 use canadensis_core::{nb, OutOfMemoryError};
@@ -46,24 +46,23 @@ impl<D, const C: usize> Default for SerialTransmitter<D, C> {
     }
 }
 
-impl<I, D, const C: usize> Transmitter<I> for SerialTransmitter<D, C>
+impl<L, D, const C: usize> Transmitter<L> for SerialTransmitter<D, C>
 where
-    I: Instant,
+    L: Clock,
     D: TransmitDriver,
 {
     type Transport = SerialTransport;
     type Driver = D;
     type Error = Error<D::Error>;
 
-    fn push<A, CL>(
+    fn push<A>(
         &mut self,
-        transfer: Transfer<A, I, Self::Transport>,
-        _clock: &mut CL,
+        transfer: Transfer<A, L::Instant, Self::Transport>,
+        _clock: &mut L,
         _driver: &mut D,
     ) -> nb::Result<(), Self::Error>
     where
         A: AsRef<[u8]>,
-        CL: Clock<Instant = I>,
     {
         // Check queue capacity with worst-case escaping
         let frame_length = transfer.payload.as_ref().len() + PER_FRAME_ESCAPED_OVERHEAD;
@@ -104,10 +103,7 @@ where
         Ok(())
     }
 
-    fn flush<CL>(&mut self, _clock: &mut CL, driver: &mut D) -> nb::Result<(), Self::Error>
-    where
-        CL: Clock<Instant = I>,
-    {
+    fn flush(&mut self, _clock: &mut L, driver: &mut D) -> nb::Result<(), Self::Error> {
         while let Some(byte) = self.queue.pop_front() {
             match driver.send_byte(byte) {
                 Ok(()) => {}
diff --git a/canadensis_udp/examples/receive.rs b/canadensis_udp/examples/receive.rs
index cb2f431..041ccfa 100644
--- a/canadensis_udp/examples/receive.rs
+++ b/canadensis_udp/examples/receive.rs
@@ -15,7 +15,7 @@ use simplelog::{ColorChoice, TermLogger};
 use zerocopy::AsBytes;
 
 use canadensis_core::session::SessionDynamicMap;
-use canadensis_core::time::{Clock, MicrosecondDuration64, Microseconds64};
+use canadensis_core::time::{MicrosecondDuration64, Microseconds64};
 use canadensis_core::transport::Receiver;
 use canadensis_linux::SystemClock;
 use canadensis_udp::driver::StdUdpSocket;
@@ -38,7 +38,7 @@ fn main() {
     // Note: This MTU includes space for the header
     const MTU: usize = 1472;
     let mut receiver = UdpReceiver::<
-        Microseconds64,
+        SystemClock,
         SessionDynamicMap<Microseconds64, UdpNodeId, UdpTransferId, UdpSessionData>,
         StdUdpSocket,
         MTU,
@@ -54,7 +54,7 @@ fn main() {
 
     // Instead of a real asynchronous IO system, just poll periodically
     loop {
-        match receiver.receive(clock.now(), &mut socket) {
+        match receiver.receive(&mut clock, &mut socket) {
             Ok(Some(transfer)) => {
                 println!("{:#?}", transfer.header);
                 for byte in transfer.payload.as_bytes() {
diff --git a/canadensis_udp/examples/transmit.rs b/canadensis_udp/examples/transmit.rs
index 1aff0d0..93bc7a0 100644
--- a/canadensis_udp/examples/transmit.rs
+++ b/canadensis_udp/examples/transmit.rs
@@ -57,6 +57,7 @@ fn main() {
                 subject: SubjectId::try_from(73u16).unwrap(),
                 source: Some(local_node_id),
             }),
+            loopback: false,
             payload: &payload,
         };
 
diff --git a/canadensis_udp/src/rx.rs b/canadensis_udp/src/rx.rs
index 8ada810..1d9cbbe 100644
--- a/canadensis_udp/src/rx.rs
+++ b/canadensis_udp/src/rx.rs
@@ -7,7 +7,7 @@ use fallible_collections::FallibleVec;
 use zerocopy::FromBytes;
 
 use canadensis_core::session::{Session, SessionTracker};
-use canadensis_core::time::Instant;
+use canadensis_core::time::{Clock, Instant};
 use canadensis_core::transfer::{Header, MessageHeader, ServiceHeader, Transfer};
 use canadensis_core::transport::Receiver;
 use canadensis_core::{OutOfMemoryError, ServiceId, ServiceSubscribeError, SubjectId};
@@ -24,11 +24,11 @@ mod buildup;
 mod subscriptions;
 
 /// UDP transport receiver
-pub struct UdpReceiver<I, T, S, const MTU: usize>
+pub struct UdpReceiver<C, T, S, const MTU: usize>
 where
-    I: Instant,
+    C: Clock,
 {
-    subscriptions: Subscriptions<I, T>,
+    subscriptions: Subscriptions<C::Instant, T>,
     /// The ID of this node, or None if this node is anonymous
     node_id: Option<UdpNodeId>,
     /// The IP address of the local interface that the socket is bound to
@@ -37,10 +37,10 @@ where
     _session_tracker: PhantomData<T>,
 }
 
-impl<I, T, S, const MTU: usize> UdpReceiver<I, T, S, MTU>
+impl<C, T, S, const MTU: usize> UdpReceiver<C, T, S, MTU>
 where
-    I: Instant,
-    T: SessionTracker<I, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
+    C: Clock,
+    T: SessionTracker<C::Instant, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
     S: UdpSocket,
 {
     pub fn new(node_id: Option<UdpNodeId>, interface_address: Ipv4Addr) -> Self {
@@ -53,9 +53,9 @@ where
         }
     }
 
-    fn clean_expired_sessions(&mut self, now: I)
+    fn clean_expired_sessions(&mut self, now: C::Instant)
     where
-        T: SessionTracker<I, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
+        T: SessionTracker<C::Instant, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
     {
         for subscription in self.subscriptions.message_iter_mut() {
             subscription.clean_expired_sessions(now);
@@ -77,9 +77,10 @@ where
     /// * `Err(e)` if a socket or memory allocation error occurred
     fn accept_inner(
         &mut self,
-        now: I,
+        now: C::Instant,
         socket: &mut S,
-    ) -> Result<Option<Transfer<Vec<u8>, I, UdpTransport>>, Error<nb::Error<S::Error>>> {
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, UdpTransport>>, Error<nb::Error<S::Error>>>
+    {
         let mut buffer: [u8; MTU] = [0; MTU];
         let bytes_received = socket.recv(&mut buffer).map_err(Error::Socket)?;
         let buffer = &buffer[..bytes_received];
@@ -163,10 +164,10 @@ where
     }
 }
 
-impl<I, T, S, const MTU: usize> Receiver<I> for UdpReceiver<I, T, S, MTU>
+impl<C, T, S, const MTU: usize> Receiver<C> for UdpReceiver<C, T, S, MTU>
 where
-    I: Instant,
-    T: SessionTracker<I, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
+    C: Clock,
+    T: SessionTracker<C::Instant, UdpNodeId, UdpTransferId, UdpSessionData> + Default,
     S: UdpSocket,
 {
     type Transport = UdpTransport;
@@ -175,12 +176,12 @@ where
 
     fn receive(
         &mut self,
-        now: I,
+        clock: &mut C,
         socket: &mut S,
-    ) -> Result<Option<Transfer<Vec<u8>, I, Self::Transport>>, Self::Error> {
+    ) -> Result<Option<Transfer<Vec<u8>, C::Instant, Self::Transport>>, Self::Error> {
         // Loop until all incoming packets have been read
         let result = loop {
-            match self.accept_inner(now, socket) {
+            match self.accept_inner(clock.now(), socket) {
                 Ok(Some(transfer)) => break Ok(Some(transfer)),
                 Ok(None) => { /* Keep going and try to read another packet */ }
                 Err(Error::Socket(nb::Error::WouldBlock)) => {
@@ -191,7 +192,7 @@ where
                 Err(Error::Socket(nb::Error::Other(e))) => break Err(Error::Socket(e)),
             }
         };
-        self.clean_expired_sessions(now);
+        self.clean_expired_sessions(clock.now());
         result
     }
 
@@ -199,7 +200,7 @@ where
         &mut self,
         subject: SubjectId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         socket: &mut S,
     ) -> Result<(), Self::Error> {
         socket
@@ -219,7 +220,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         socket: &mut S,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         self.service_subscribe_check_multicast(socket)
@@ -242,7 +243,7 @@ where
         &mut self,
         service: ServiceId,
         payload_size_max: usize,
-        timeout: <I as Instant>::Duration,
+        timeout: <C::Instant as Instant>::Duration,
         socket: &mut S,
     ) -> Result<(), ServiceSubscribeError<Self::Error>> {
         self.service_subscribe_check_multicast(socket)
@@ -367,6 +368,7 @@ where
                 };
                 Ok(Some(Transfer {
                     header,
+                    loopback: false,
                     payload: reassembled,
                 }))
             }
diff --git a/canadensis_udp/src/tx.rs b/canadensis_udp/src/tx.rs
index d9e0eb8..e1d65ef 100644
--- a/canadensis_udp/src/tx.rs
+++ b/canadensis_udp/src/tx.rs
@@ -84,24 +84,23 @@ where
     }
 }
 
-impl<I, S, const MTU: usize> Transmitter<I> for UdpTransmitter<S, MTU>
+impl<C, S, const MTU: usize> Transmitter<C> for UdpTransmitter<S, MTU>
 where
-    I: Instant,
+    C: Clock,
     S: crate::driver::UdpSocket,
 {
     type Transport = UdpTransport;
     type Driver = S;
     type Error = Error<S::Error>;
 
-    fn push<A, C>(
+    fn push<A>(
         &mut self,
-        transfer: Transfer<A, I, Self::Transport>,
+        transfer: Transfer<A, C::Instant, Self::Transport>,
         clock: &mut C,
         socket: &mut S,
     ) -> nb::Result<(), Self::Error>
     where
         A: AsRef<[u8]>,
-        C: Clock<Instant = I>,
     {
         let deadline = transfer.header.timestamp();
         let (header_base, dest_address) = match transfer.header {
@@ -159,14 +158,11 @@ where
         .map_err(nb::Error::Other)
     }
 
-    fn flush<C>(
+    fn flush(
         &mut self,
         _clock: &mut C,
         _socket: &mut S,
-    ) -> canadensis_core::nb::Result<(), Self::Error>
-    where
-        C: Clock<Instant = I>,
-    {
+    ) -> canadensis_core::nb::Result<(), Self::Error> {
         // Because the push() function blocks until everything has been transmitted, nothing is
         // needed here.
         Ok(())


Test patch:
diff --git a/canadensis/tests/can_loopback.rs b/canadensis/tests/can_loopback.rs
new file mode 100644
index 0000000..75af8f0
--- /dev/null
+++ b/canadensis/tests/can_loopback.rs
@@ -0,0 +1,256 @@
+//! Tests the Cyphal/CAN loopback behavior by sending a loopback transfer and
+
+extern crate canadensis;
+extern crate canadensis_can;
+extern crate canadensis_data_types;
+extern crate canadensis_encoding;
+extern crate log;
+extern crate simplelog;
+
+use canadensis::node::CoreNode;
+use canadensis::requester::TransferIdFixedMap;
+use canadensis::{Node, PublishToken, ResponseToken, TransferHandler};
+use canadensis_can::driver::{ReceiveDriver, TransmitDriver};
+use canadensis_can::{CanNodeId, CanReceiver, CanTransmitter, CanTransport, Frame, Mtu};
+use canadensis_core::subscription::Subscription;
+use canadensis_core::time::{milliseconds, Clock, Microseconds64};
+use canadensis_core::transfer::{MessageTransfer, ServiceTransfer, Transfer};
+use canadensis_core::{OutOfMemoryError, Priority};
+use canadensis_data_types::uavcan::time::synchronization_1_0::{self, Synchronization};
+use canadensis_encoding::Deserialize;
+
+use log::LevelFilter;
+use simplelog::{ColorChoice, Config, TerminalMode};
+use std::cell::Cell;
+use std::collections::vec_deque::VecDeque;
+use std::convert::{Infallible, TryFrom};
+
+#[test]
+fn can_loopback_time_sync() {
+    simplelog::TermLogger::init(
+        LevelFilter::Trace,
+        Config::default(),
+        TerminalMode::default(),
+        ColorChoice::Auto,
+    )
+    .unwrap();
+
+    let clock_handle = StubClockHandle::new();
+    let node_id = CanNodeId::try_from(3_u8).unwrap();
+    let mut node: CoreNode<
+        StubClock<'_>,
+        CanTransmitter<StubClock<'_>, LoopbackOnlyDriver>,
+        CanReceiver<StubClock<'_>, LoopbackOnlyDriver>,
+        TransferIdFixedMap<CanTransport, 4>,
+        LoopbackOnlyDriver,
+        4,
+        4,
+    > = CoreNode::new(
+        clock_handle.clock(),
+        node_id,
+        CanTransmitter::new(Mtu::Can8),
+        CanReceiver::new(node_id, Mtu::Can8),
+        LoopbackOnlyDriver::default(),
+    );
+
+    // Need to subscribe to receive loopback transfers
+    node.subscribe_message(synchronization_1_0::SUBJECT, 8, milliseconds(100))
+        .unwrap();
+
+    let sync_token: PublishToken<Synchronization> = node
+        .start_publishing(
+            synchronization_1_0::SUBJECT,
+            milliseconds(100),
+            Priority::Nominal,
+        )
+        .unwrap();
+    clock_handle.set_time(10);
+    // Send a non-loopback transfer, which should be ignored
+    node.publish(
+        &sync_token,
+        &Synchronization {
+            previous_transmission_timestamp_microsecond: 3,
+        },
+    )
+    .unwrap();
+
+    let mut collector = LoopbackCollector::default();
+    clock_handle.set_time(20);
+    node.receive(&mut collector)
+        .expect("Unexpected error in receive");
+    assert_eq!(0, collector.transfers.len());
+
+    // Send a loopback transfer, which should be collected
+    // Outgoing frames will have their transmit deadline (timestamp) set to 30 microseconds
+    // + 100 milliseconds.
+    // The frames will actually get to the driver at a time of 30 microseconds, so the timestamp
+    // of the received loopback transfer should match that.
+    clock_handle.set_time(30);
+    let loopback_payload = Synchronization {
+        previous_transmission_timestamp_microsecond: 129,
+    };
+    node.publish_loopback(&sync_token, &loopback_payload)
+        .unwrap();
+    // Simulate a short delay before receiving
+    clock_handle.set_time(40);
+    node.receive(&mut collector)
+        .expect("Unexpected error in receive");
+    assert_eq!(1, collector.transfers.len());
+    let received_loopback = &collector.transfers[0];
+    assert_eq!(received_loopback.loopback, true);
+    assert_eq!(received_loopback.header.priority(), &Priority::Nominal);
+    assert_eq!(received_loopback.header.source(), Some(&node_id));
+    assert_eq!(
+        received_loopback.header.timestamp(),
+        Microseconds64::new(30)
+    );
+    let loopback_deserialized_payload =
+        Synchronization::deserialize_from_bytes(&received_loopback.payload).unwrap();
+    assert_eq!(
+        loopback_deserialized_payload.previous_transmission_timestamp_microsecond,
+        loopback_payload.previous_transmission_timestamp_microsecond
+    );
+}
+
+/// A CAN driver that handles loopback only
+///
+/// This driver discards all outgoing non-loopback frames and cannot receive any non-loopback
+/// frames.
+#[derive(Default)]
+struct LoopbackOnlyDriver {
+    loopback_frames: VecDeque<Frame<Microseconds64>>,
+}
+
+impl TransmitDriver<StubClock<'_>> for LoopbackOnlyDriver {
+    type Error = Infallible;
+
+    fn try_reserve(&mut self, _frames: usize) -> Result<(), OutOfMemoryError> {
+        // Using std, there's no good way to detect out of memory
+        Ok(())
+    }
+
+    fn transmit(
+        &mut self,
+        frame: Frame<Microseconds64>,
+        clock: &mut StubClock<'_>,
+    ) -> canadensis::nb::Result<Option<Frame<Microseconds64>>, Self::Error> {
+        log::trace!("LoopbackOnlyDriver::transmit");
+        let now = clock.now();
+        if frame.timestamp() < now {
+            log::debug!("Frame timed out");
+            return Ok(None);
+        }
+        if !frame.loopback() {
+            log::debug!("Discarding non-loopback frame");
+            return Ok(None);
+        }
+        let mut loopback_frame = frame;
+        loopback_frame.set_timestamp(now);
+        self.loopback_frames.push_back(loopback_frame);
+        Ok(None)
+    }
+
+    fn flush(&mut self, _clock: &mut StubClock<'_>) -> canadensis::nb::Result<(), Self::Error> {
+        // Nothing to do
+        Ok(())
+    }
+}
+impl ReceiveDriver<StubClock<'_>> for LoopbackOnlyDriver {
+    type Error = Infallible;
+
+    fn receive(
+        &mut self,
+        _clock: &mut StubClock<'_>,
+    ) -> canadensis::nb::Result<Frame<Microseconds64>, Self::Error> {
+        self.loopback_frames
+            .pop_front()
+            .map(|frame| {
+                log::trace!("Receiving loopback frame");
+                frame
+            })
+            .ok_or(canadensis::nb::Error::WouldBlock)
+    }
+
+    fn apply_filters<S>(&mut self, _local_node: Option<CanNodeId>, _subscriptions: S)
+    where
+        S: IntoIterator<Item = Subscription>,
+    {
+        // Not applicable
+    }
+
+    fn apply_accept_all(&mut self) {
+        // Not applicable
+    }
+}
+
+/// A transfer that collects all loopback transfers and panics if given any non-loopback transfer
+#[derive(Default)]
+struct LoopbackCollector {
+    transfers: Vec<Transfer<Vec<u8>, Microseconds64, CanTransport>>,
+}
+
+impl TransferHandler<Microseconds64, CanTransport> for LoopbackCollector {
+    fn handle_message<N: Node<Instant = Microseconds64, Transport = CanTransport>>(
+        &mut self,
+        _node: &mut N,
+        _transfer: &MessageTransfer<Vec<u8>, Microseconds64, CanTransport>,
+    ) -> bool {
+        panic!("handle_message() called (not loopback)");
+    }
+
+    fn handle_request<N: Node<Instant = Microseconds64, Transport = CanTransport>>(
+        &mut self,
+        _node: &mut N,
+        _token: ResponseToken<CanTransport>,
+        _transfer: &ServiceTransfer<Vec<u8>, Microseconds64, CanTransport>,
+    ) -> bool {
+        panic!("handle_request() called (not loopback)");
+    }
+
+    fn handle_response<N: Node<Instant = Microseconds64, Transport = CanTransport>>(
+        &mut self,
+        _node: &mut N,
+        _transfer: &ServiceTransfer<Vec<u8>, Microseconds64, CanTransport>,
+    ) -> bool {
+        panic!("handle_response() called (not loopback)");
+    }
+
+    fn handle_loopback<N: Node<Instant = Microseconds64, Transport = CanTransport>>(
+        &mut self,
+        _node: &mut N,
+        transfer: &Transfer<Vec<u8>, Microseconds64, CanTransport>,
+    ) -> bool {
+        self.transfers.push(transfer.clone());
+        true
+    }
+}
+
+struct StubClock<'t> {
+    time: &'t Cell<u64>,
+}
+
+impl Clock for StubClock<'_> {
+    type Instant = Microseconds64;
+
+    fn now(&mut self) -> Self::Instant {
+        Microseconds64::new(self.time.get())
+    }
+}
+
+struct StubClockHandle {
+    time: Cell<u64>,
+}
+
+impl StubClockHandle {
+    pub fn new() -> Self {
+        StubClockHandle { time: Cell::new(0) }
+    }
+    /// Sets the time that all associated stub clocks will return
+    pub fn set_time(&self, time: u64) {
+        self.time.set(time);
+    }
+    /// Creates and returns a stub clock that always returns the same time as this handle
+    pub fn clock(&self) -> StubClock<'_> {
+        StubClock { time: &self.time }
+    }
+}
diff --git a/canadensis_can/tests/rx.rs b/canadensis_can/tests/rx.rs
index 7331031..61a293d 100644
--- a/canadensis_can/tests/rx.rs
+++ b/canadensis_can/tests/rx.rs
@@ -6,13 +6,14 @@ extern crate canadensis_can;
 extern crate canadensis_core;
 
 use core::convert::{TryFrom, TryInto};
+use std::cell::Cell;
 use std::collections::VecDeque;
 
 use canadensis_can::driver::ReceiveDriver;
 use canadensis_can::{CanId, CanNodeId, CanReceiver, Frame, Mtu};
 use canadensis_core::nb;
 use canadensis_core::subscription::Subscription;
-use canadensis_core::time::{Instant, MicrosecondDuration32, Microseconds32};
+use canadensis_core::time::{Clock, Instant, MicrosecondDuration32, Microseconds32};
 use canadensis_core::transfer::*;
 use canadensis_core::transport::Receiver;
 use canadensis_core::{Priority, ServiceId, SubjectId};
@@ -41,8 +42,10 @@ fn test_heartbeat() {
         0x107d552a.try_into().unwrap(),
         &[0x00, 0x00, 0x00, 0x00, 0x04, 0x78, 0x68, 0xe0],
     ));
+    let clock = ClockOwner::default();
+    clock.set_ticks(0);
     let transfer = rx
-        .receive(instant(0), &mut driver)
+        .receive(&mut clock.make_clock(), &mut driver)
         .unwrap()
         .expect("Didn't get a transfer");
 
@@ -54,6 +57,7 @@ fn test_heartbeat() {
             subject: heartbeat_subject,
             source: Some(42u8.try_into().unwrap()),
         }),
+        loopback: false,
         payload: vec![0x00, 0x00, 0x00, 0x00, 0x04, 0x78, 0x68],
     };
     assert_eq!(expected, transfer);
@@ -73,8 +77,10 @@ fn test_string() {
         0x11133775.try_into().unwrap(),
         b"\x00\x18Hello world!\x00\xe0",
     ));
+    let clock = ClockOwner::default();
+    clock.set_ticks(0);
     let transfer = rx
-        .receive(instant(0), &mut driver)
+        .receive(&mut clock.make_clock(), &mut driver)
         .unwrap()
         .expect("Didn't get a transfer");
 
@@ -86,6 +92,7 @@ fn test_string() {
             subject: string_subject,
             source: None,
         }),
+        loopback: false,
         payload: b"\x00\x18Hello world!\x00".to_vec(),
     };
     assert_eq!(expected, transfer);
@@ -104,8 +111,10 @@ fn test_node_info_request() {
         0x136b957b.try_into().unwrap(),
         &[0xe1],
     ));
+    let clock = ClockOwner::default();
+    clock.set_ticks(0);
     let transfer = rx
-        .receive(instant(0), &mut driver)
+        .receive(&mut clock.make_clock(), &mut driver)
         .unwrap()
         .expect("Didn't get a transfer");
 
@@ -118,6 +127,7 @@ fn test_node_info_request() {
             source: 123u8.try_into().unwrap(),
             destination: 42u8.try_into().unwrap(),
         }),
+        loopback: false,
         payload: vec![],
     };
     assert_eq!(expected, transfer);
@@ -161,6 +171,7 @@ fn test_node_info_response() {
         (&[0xe7, 0x61], 200),
     ];
 
+    let clock = ClockOwner::default();
     for (i, &(frame_data, frame_time)) in frames_and_times.iter().enumerate() {
         let frame = Frame::new(
             instant(frame_time),
@@ -168,13 +179,14 @@ fn test_node_info_response() {
             frame_data,
         );
         driver.push(frame);
+        clock.set_ticks(frame_time);
         if i != frames_and_times.len() - 1 {
-            let maybe_transfer = rx.receive(instant(frame_time), &mut driver).unwrap();
+            let maybe_transfer = rx.receive(&mut clock.make_clock(), &mut driver).unwrap();
             assert!(maybe_transfer.is_none());
         } else {
             // End of transfer
             let transfer = rx
-                .receive(instant(frame_time), &mut driver)
+                .receive(&mut clock.make_clock(), &mut driver)
                 .unwrap()
                 .expect("Didn't get a transfer");
 
@@ -188,6 +200,7 @@ fn test_node_info_response() {
                     source: 42u8.try_into().unwrap(),
                     destination: 123u8.try_into().unwrap(),
                 }),
+                loopback: false,
                 payload: payload.to_vec(),
             };
             assert_eq!(expected, transfer);
@@ -197,6 +210,7 @@ fn test_node_info_response() {
 #[test]
 fn test_node_info_response_timeout() {
     let mut driver = StubDriver::default();
+    let clock = ClockOwner::default();
     let mut rx = CanReceiver::new(123u8.try_into().unwrap(), Mtu::Can8);
 
     let service = ServiceId::try_from(430).unwrap();
@@ -226,7 +240,8 @@ fn test_node_info_response_timeout() {
         );
         driver.push(frame);
         // When the last frame is accepted, it has timed out and the whole transfer gets discarded.
-        let maybe_transfer = rx.receive(instant(frame_time), &mut driver).unwrap();
+        clock.set_ticks(frame_time);
+        let maybe_transfer = rx.receive(&mut clock.make_clock(), &mut driver).unwrap();
         assert!(maybe_transfer.is_none());
     }
 }
@@ -234,6 +249,7 @@ fn test_node_info_response_timeout() {
 #[cfg(feature = "can-fd")]
 fn test_array() {
     let mut driver = StubDriver::default();
+    let clock = ClockOwner::default();
     let mut rx = CanReceiver::new(0u8.try_into().unwrap(), Mtu::CanFd64);
 
     let subject = SubjectId::try_from(4919).unwrap();
@@ -248,6 +264,7 @@ fn test_array() {
             subject,
             source: Some(59u8.try_into().unwrap()),
         }),
+        loopback: false,
         payload: [
             0x00, 0xb8, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
             0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
@@ -285,13 +302,14 @@ fn test_array() {
             frame_data,
         );
         driver.push(frame);
+        clock.set_ticks(i as u32);
         if i != frames.len() - 1 {
-            let maybe_transfer = rx.receive(instant(i as u32), &mut driver).unwrap();
+            let maybe_transfer = rx.receive(&mut clock.make_clock(), &mut driver).unwrap();
             assert!(maybe_transfer.is_none());
         } else {
             // End of transfer
             let transfer = rx
-                .receive(instant(i as u32), &mut driver)
+                .receive(&mut clock.make_clock(), &mut driver)
                 .unwrap()
                 .expect("Didn't get a transfer");
 
@@ -335,24 +353,39 @@ fn test_multi_frame_anonymous() {
             subject: subject_id,
             source: Some(64u8.try_into().unwrap()),
         }),
+        loopback: false,
         payload: vec![0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8],
     };
 
+    let clock = ClockOwner::default();
+    clock.set_ticks(1);
     driver.push(Frame::new(TestInstant::new(1), non_anonymous_id, frames[0]));
-    assert_eq!(Ok(None), receiver.receive(instant(1), &mut driver));
+    assert_eq!(
+        Ok(None),
+        receiver.receive(&mut clock.make_clock(), &mut driver)
+    );
+    clock.set_ticks(2);
     driver.push(Frame::new(TestInstant::new(2), non_anonymous_id, frames[1]));
     assert_eq!(
         Ok(Some(non_anonymous_transfer)),
-        receiver.receive(instant(2), &mut driver)
+        receiver.receive(&mut clock.make_clock(), &mut driver)
     );
     // Now make it anonymous
     let anonymous_id: CanId = 0b100_0_1_011_0000000001010_0_1000000_u32
         .try_into()
         .unwrap();
+    clock.set_ticks(1);
     driver.push(Frame::new(TestInstant::new(1), anonymous_id, frames[0]));
-    assert_eq!(Ok(None), receiver.receive(instant(1), &mut driver));
+    assert_eq!(
+        Ok(None),
+        receiver.receive(&mut clock.make_clock(), &mut driver)
+    );
+    clock.set_ticks(2);
     driver.push(Frame::new(TestInstant::new(2), anonymous_id, frames[1]));
-    assert_eq!(Ok(None), receiver.receive(instant(2), &mut driver));
+    assert_eq!(
+        Ok(None),
+        receiver.receive(&mut clock.make_clock(), &mut driver)
+    );
 }
 
 /// Tests an anonymous receiver receiving a multi-frame non-anonymous transfer
@@ -385,15 +418,19 @@ fn test_anonymous_receive_multi_frame() {
             subject: 8166.try_into().unwrap(),
             source: Some(126u8.try_into().unwrap()),
         }),
+        loopback: false,
         payload: vec![190, 159, 33, 213, 34, 64, 1, 103, 0],
     };
 
+    let clock = ClockOwner::default();
+    clock.set_ticks(frames[0].timestamp().as_microseconds());
     driver.push(frames[0].clone());
-    assert_eq!(Ok(None), rx.receive(frames[0].timestamp(), &mut driver));
+    assert_eq!(Ok(None), rx.receive(&mut clock.make_clock(), &mut driver));
+    clock.set_ticks(frames[1].timestamp().as_microseconds());
     driver.push(frames[1].clone());
     assert_eq!(
         Ok(Some(expected_transfer)),
-        rx.receive(frames[1].timestamp(), &mut driver)
+        rx.receive(&mut clock.make_clock(), &mut driver)
     );
 }
 
@@ -411,7 +448,9 @@ fn test_ignore_request_to_other_node() {
         0x136b957b.try_into().unwrap(),
         &[0xe1],
     ));
-    let transfer = rx.receive(instant(302), &mut driver).unwrap();
+    let clock = ClockOwner::default();
+    clock.set_ticks(302);
+    let transfer = rx.receive(&mut clock.make_clock(), &mut driver).unwrap();
 
     assert_eq!(transfer, None);
 }
@@ -421,20 +460,23 @@ fn test_ignore_request_to_other_node() {
 /// This does not keep the frames in order by priority, but it is correct as long as it is used for
 /// only one transfer at a time.
 #[derive(Default)]
-struct StubDriver<I> {
-    frames: VecDeque<Frame<I>>,
+struct StubDriver {
+    frames: VecDeque<Frame<Microseconds32>>,
 }
 
-impl<I> StubDriver<I> {
-    fn push(&mut self, frame: Frame<I>) {
+impl StubDriver {
+    fn push(&mut self, frame: Frame<Microseconds32>) {
         self.frames.push_back(frame)
     }
 }
 
-impl<I> ReceiveDriver<I> for StubDriver<I> {
+impl ReceiveDriver<StubClock<'_>> for StubDriver {
     type Error = ();
 
-    fn receive(&mut self, _now: I) -> nb::Result<Frame<I>, Self::Error> {
+    fn receive(
+        &mut self,
+        _clock: &mut StubClock<'_>,
+    ) -> nb::Result<Frame<Microseconds32>, Self::Error> {
         self.frames.pop_front().ok_or(nb::Error::WouldBlock)
     }
 
@@ -449,3 +491,27 @@ impl<I> ReceiveDriver<I> for StubDriver<I> {
         // Nothing to do
     }
 }
+
+struct StubClock<'a> {
+    count: &'a Cell<u32>,
+}
+impl Clock for StubClock<'_> {
+    type Instant = Microseconds32;
+
+    fn now(&mut self) -> Self::Instant {
+        Microseconds32::new(self.count.get())
+    }
+}
+#[derive(Default)]
+struct ClockOwner {
+    count: Cell<u32>,
+}
+
+impl ClockOwner {
+    pub fn set_ticks(&self, count: u32) {
+        self.count.set(count)
+    }
+    pub fn make_clock(&self) -> StubClock<'_> {
+        StubClock { count: &self.count }
+    }
+}
diff --git a/canadensis_can/tests/tx.rs b/canadensis_can/tests/tx.rs
index b67f077..be5685e 100644
--- a/canadensis_can/tests/tx.rs
+++ b/canadensis_can/tests/tx.rs
@@ -33,6 +33,7 @@ fn test_heartbeat() {
                 subject: SubjectId::try_from(7509).unwrap(),
                 source: Some(CanNodeId::try_from(42u8).unwrap()),
             }),
+            loopback: false,
             payload: &[0x00, 0x00, 0x00, 0x00, 0x04, 0x78, 0x68],
         },
         &mut ZeroClock,
@@ -60,6 +61,7 @@ fn test_heartbeat() {
                 subject: SubjectId::try_from(7509).unwrap(),
                 source: Some(CanNodeId::try_from(42u8).unwrap()),
             }),
+            loopback: false,
             payload: &[0x01, 0x00, 0x00, 0x00, 0x04, 0x78, 0x68],
         },
         &mut ZeroClock,
@@ -92,6 +94,7 @@ fn test_string() {
                 subject: SubjectId::try_from(4919).unwrap(),
                 source: None,
             }),
+            loopback: false,
             payload: &[
                 0x00, 0x18, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21,
             ],
@@ -129,6 +132,7 @@ fn test_node_info_request() {
                 source: CanNodeId::try_from(123u8).unwrap(),
                 destination: CanNodeId::try_from(42u8).unwrap(),
             }),
+            loopback: false,
             payload: &[],
         },
         &mut ZeroClock,
@@ -161,6 +165,7 @@ fn test_node_info_response() {
                 source: CanNodeId::try_from(42u8).unwrap(),
                 destination: CanNodeId::try_from(123u8).unwrap(),
             }),
+            loopback: false,
             payload: &b"\x01\x00\x00\x00\x01\x00\x00\
                     \x00\x00\x00\x00\x00\x00\x00\
                     \x00\x00\x00\x00\x00\x00\x00\
@@ -213,6 +218,7 @@ fn test_array() {
                 subject: SubjectId::try_from(4919).unwrap(),
                 source: Some(CanNodeId::try_from(59u8).unwrap()),
             }),
+            loopback: false,
             payload: &[
                 0x00, 0xb8, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
                 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
@@ -257,17 +263,26 @@ fn test_array() {
 /// This does not keep the frames in order by priority, but it is correct as long as it is used for
 /// only one transfer at a time.
 #[derive(Default)]
-struct MockDriver<I> {
-    queue: VecDeque<Frame<I>>,
+struct MockDriver<C>
+where
+    C: Clock,
+{
+    queue: VecDeque<Frame<C::Instant>>,
 }
 
-impl<I> MockDriver<I> {
-    fn pop_frame(&mut self) -> Option<Frame<I>> {
+impl<C> MockDriver<C>
+where
+    C: Clock,
+{
+    fn pop_frame(&mut self) -> Option<Frame<C::Instant>> {
         self.queue.pop_front()
     }
 }
 
-impl<I> TransmitDriver<I> for MockDriver<I> {
+impl<C> TransmitDriver<C> for MockDriver<C>
+where
+    C: Clock,
+{
     type Error = Infallible;
 
     fn try_reserve(&mut self, frames: usize) -> Result<(), OutOfMemoryError> {
@@ -277,19 +292,20 @@ impl<I> TransmitDriver<I> for MockDriver<I> {
 
     fn transmit(
         &mut self,
-        frame: Frame<I>,
-        _now: I,
-    ) -> canadensis_core::nb::Result<Option<Frame<I>>, Self::Error> {
+        frame: Frame<C::Instant>,
+        _clock: &mut C,
+    ) -> canadensis_core::nb::Result<Option<Frame<C::Instant>>, Self::Error> {
         self.queue.push_back(frame);
         Ok(None)
     }
 
-    fn flush(&mut self, _now: I) -> canadensis_core::nb::Result<(), Self::Error> {
+    fn flush(&mut self, _clock: &mut C) -> canadensis_core::nb::Result<(), Self::Error> {
         Ok(())
     }
 }
 
 /// A clock that produces a Microseconds32 value that is always zero
+#[derive(Default)]
 struct ZeroClock;
 
 impl Clock for ZeroClock {
diff --git a/canadensis_serial/tests/round_trip.rs b/canadensis_serial/tests/round_trip.rs
index de162fd..7f1cc26 100644
--- a/canadensis_serial/tests/round_trip.rs
+++ b/canadensis_serial/tests/round_trip.rs
@@ -36,6 +36,7 @@ fn round_trip_no_payload() {
             subject,
             source: Some(37u16.try_into().unwrap()),
         }),
+        loopback: false,
         payload: vec![],
     };
     tx.push(transfer.clone(), &mut ZeroClock, &mut driver)
@@ -44,17 +45,17 @@ fn round_trip_no_payload() {
     let wire_bytes: Vec<u8> = driver.iter().copied().collect();
     println!("{:02x?}", wire_bytes);
 
-    let mut rx = SerialReceiver::<
-        Microseconds32,
+    let mut rx: SerialReceiver<
+        ZeroClock,
         MockDriver,
         DynamicSubscriptionManager<Subscription<Microseconds32>>,
-    >::new(SerialNodeId::try_from(360).unwrap());
+    > = SerialReceiver::new(SerialNodeId::try_from(360).unwrap());
     rx.subscribe_message(subject, 0, MicrosecondDuration32::new(0), &mut driver)
         .unwrap();
 
     // Only need to call receive once. It will read all the available frames.
     let received = rx
-        .receive(Microseconds32::new(0), &mut driver)
+        .receive(&mut ZeroClock, &mut driver)
         .unwrap()
         .expect("No transfer");
 
diff --git a/canadensis_serial/tests/tx.rs b/canadensis_serial/tests/tx.rs
index 59b890b..bf6b690 100644
--- a/canadensis_serial/tests/tx.rs
+++ b/canadensis_serial/tests/tx.rs
@@ -22,6 +22,7 @@ fn transmit_capacity_1() {
             subject: 9u16.try_into().unwrap(),
             source: Some(37u16.try_into().unwrap()),
         }),
+        loopback: false,
         payload: [],
     };
     assert!(tx.push(transfer, &mut ZeroClock, &mut driver).is_err());
@@ -42,6 +43,7 @@ fn transmit_minimum_capacity() {
             subject: 9u16.try_into().unwrap(),
             source: Some(37u16.try_into().unwrap()),
         }),
+        loopback: false,
         payload: [],
     };
     tx.push(transfer, &mut ZeroClock, &mut driver).unwrap();
diff --git a/canadensis_udp/tests/loopback_transmit_receive.rs b/canadensis_udp/tests/loopback_transmit_receive.rs
index ea9726d..27ac9a0 100644
--- a/canadensis_udp/tests/loopback_transmit_receive.rs
+++ b/canadensis_udp/tests/loopback_transmit_receive.rs
@@ -51,6 +51,7 @@ fn transmit_receive_message_two_frames() {
             subject: SubjectId::try_from(73u16).unwrap(),
             source: Some(transmit_node_id),
         }),
+        loopback: false,
         payload,
     };
     check_loopback::<_, _, MTU>(
@@ -82,6 +83,7 @@ fn transmit_receive_message_one_byte_one_frame() {
             subject,
             source: Some(8.try_into().unwrap()),
         }),
+        loopback: false,
         payload: vec![0x27],
     };
     check_loopback::<_, _, 1472>(
@@ -109,6 +111,7 @@ fn transmit_receive_request_one_byte_one_frame() {
             source: 8.try_into().unwrap(),
             destination: 993.try_into().unwrap(),
         }),
+        loopback: false,
         payload: vec![0x27],
     };
     check_loopback::<_, _, 1472>(
@@ -136,6 +139,7 @@ fn transmit_receive_response_one_byte_one_frame() {
             source: 8.try_into().unwrap(),
             destination: 993.try_into().unwrap(),
         }),
+        loopback: false,
         payload: vec![0x27],
     };
     check_loopback::<_, _, 1472>(
@@ -150,7 +154,7 @@ fn transmit_receive_response_one_byte_one_frame() {
 }
 
 type TestUdpReceiver<const MTU: usize> = UdpReceiver<
-    Microseconds64,
+    SystemClock,
     SessionDynamicMap<Microseconds64, UdpNodeId, UdpTransferId, UdpSessionData>,
     StdUdpSocket,
     MTU,
@@ -208,7 +212,7 @@ fn check_loopback<S, U, const MTU: usize>(
 
         let timeout = Instant::now() + Duration::from_secs(1);
         loop {
-            match receiver.receive(clock.now(), &mut receive_socket) {
+            match receiver.receive(clock, &mut receive_socket) {
                 Ok(Some(received_transfer)) => {
                     assert_eq!(&received_transfer.payload, &transfer.payload);
                     break;
@@ -264,7 +268,7 @@ fn send_and_expect_not_received<const MTU: usize>(
 
         let timeout = Instant::now() + Duration::from_millis(100);
         loop {
-            match receiver.receive(clock.now(), receive_socket) {
+            match receiver.receive(clock, receive_socket) {
                 Ok(Some(received_transfer)) => {
                     panic!(
                         "Received transfer when not subscribed: {:#?}",