diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 0000000000000000000000000000000000000000..bd907936ff61cf7c1bb4d8c25486d3939416c627
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,3 @@
+disallowed-methods = [
+    { path = "std::time::SystemTime::now", reason = "prefere using SleepProvider::wallclock instead when possible" },
+]
diff --git a/crates/arti-bench/src/main.rs b/crates/arti-bench/src/main.rs
index 80114f9ac62b9cc018908bc45789befbdc1adf54..2d0cc5b664721baf9b24311fd1f5321a827b6751 100644
--- a/crates/arti-bench/src/main.rs
+++ b/crates/arti-bench/src/main.rs
@@ -34,6 +34,7 @@
 // This file uses `unwrap()` a fair deal, but this is fine in test/bench code
 // because it's OK if tests and benchmarks simply crash if things go wrong.
 #![allow(clippy::unwrap_used)]
+#![allow(clippy::disallowed_methods)]
 
 use anyhow::{anyhow, Result};
 use arti::cfg::ArtiConfig;
diff --git a/crates/arti-client/src/status.rs b/crates/arti-client/src/status.rs
index 742f0ee4e150f1cd581b2e868bb5143aca256ca7..0afe87d24cd134e55da40f2604e741d8af3d4ff0 100644
--- a/crates/arti-client/src/status.rs
+++ b/crates/arti-client/src/status.rs
@@ -38,7 +38,10 @@ impl BootstrapStatus {
     /// 0 is defined as "just started"; 1 is defined as "ready to use."
     pub fn as_frac(&self) -> f32 {
         // Coefficients chosen arbitrarily.
-        self.conn_status.frac() * 0.15 + self.dir_status.frac_at(SystemTime::now()) * 0.85
+        #[allow(clippy::disallowed_methods)]
+        let res =
+            self.conn_status.frac() * 0.15 + self.dir_status.frac_at(SystemTime::now()) * 0.85;
+        res
     }
 
     /// Return true if the status indicates that the client is ready for
@@ -47,6 +50,7 @@ impl BootstrapStatus {
     /// For the purposes of this function, the client is "ready for traffic" if,
     /// as far as we know, we can start acting on a new client request immediately.
     pub fn ready_for_traffic(&self) -> bool {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         self.conn_status.usable() && self.dir_status.usable_at(now)
     }
diff --git a/crates/tor-checkable/src/lib.rs b/crates/tor-checkable/src/lib.rs
index 4cd02c857b432b9f44f874a03098dbf562b79cb8..1d07cc806f1b58b6ed07f91909a4af748a7383a6 100644
--- a/crates/tor-checkable/src/lib.rs
+++ b/crates/tor-checkable/src/lib.rs
@@ -119,6 +119,7 @@ pub trait Timebound<T>: Sized {
 
     /// Unwrap this Timebound object if it is valid now.
     fn check_valid_now(self) -> Result<T, Self::Error> {
+        #[allow(clippy::disallowed_methods)]
         self.check_valid_at(&time::SystemTime::now())
     }
 
diff --git a/crates/tor-circmgr/src/build.rs b/crates/tor-circmgr/src/build.rs
index a3196e7cabdedc8ff9055f6cac614108181c9e46..c0e30ff364c8c3df07e257b609377311b1712c55 100644
--- a/crates/tor-circmgr/src/build.rs
+++ b/crates/tor-circmgr/src/build.rs
@@ -264,6 +264,11 @@ impl<R: Runtime, C: Buildable + Sync + Send + 'static> Builder<R, C> {
             Err(e) => Err(e),
         }
     }
+
+    /// Return a reference to this Builder runtime.
+    pub(crate) fn runtime(&self) -> &R {
+        &self.runtime
+    }
 }
 
 /// A factory object to build circuits.
@@ -387,6 +392,11 @@ impl<R: Runtime> CircuitBuilder<R> {
     pub(crate) fn guardmgr(&self) -> &tor_guardmgr::GuardMgr<R> {
         &self.guardmgr
     }
+
+    /// Return a reference to this builder's runtime
+    pub(crate) fn runtime(&self) -> &R {
+        self.builder.runtime()
+    }
 }
 
 /// Helper function: spawn a future as a background task, and run it with
diff --git a/crates/tor-circmgr/src/impls.rs b/crates/tor-circmgr/src/impls.rs
index 1107f5927554425bd44f8f5c9359ab3035287546..c09562b48d163afbeed0190dbfa7cc11dbf88089 100644
--- a/crates/tor-circmgr/src/impls.rs
+++ b/crates/tor-circmgr/src/impls.rs
@@ -65,6 +65,7 @@ impl<R: Runtime> crate::mgr::AbstractCircBuilder for crate::build::CircuitBuilde
             dir,
             Some(self.guardmgr()),
             self.path_config().as_ref(),
+            self.runtime().wallclock(),
         )?;
 
         let plan = Plan {
diff --git a/crates/tor-circmgr/src/path/exitpath.rs b/crates/tor-circmgr/src/path/exitpath.rs
index 195ff6c288067601c553abcbcb2fe9efc2df41b1..67150fc4a901fb392ea172302d992711344f5b64 100644
--- a/crates/tor-circmgr/src/path/exitpath.rs
+++ b/crates/tor-circmgr/src/path/exitpath.rs
@@ -122,6 +122,7 @@ impl<'a> ExitPathBuilder<'a> {
         netdir: DirInfo<'a>,
         guards: Option<&GuardMgr<RT>>,
         config: &PathConfig,
+        now: SystemTime,
     ) -> Result<(TorPath<'a>, Option<GuardMonitor>, Option<GuardUsable>)> {
         let netdir = match netdir {
             DirInfo::Fallbacks(_) => {
@@ -136,7 +137,7 @@ impl<'a> ExitPathBuilder<'a> {
         let lifetime = netdir.lifetime();
 
         // Check if the consensus isn't expired by > 72 hours
-        if SystemTime::now() > lifetime.valid_until() + Duration::new(72 * 60 * 60, 0) {
+        if now > lifetime.valid_until() + Duration::new(72 * 60 * 60, 0) {
             return Err(Error::ExpiredConsensus);
         }
 
@@ -232,6 +233,7 @@ mod test {
     use std::convert::TryInto;
     use tor_linkspec::ChanTarget;
     use tor_netdir::testnet;
+    use tor_rtcompat::SleepProvider;
 
     fn assert_exit_path_ok(relays: &[Relay<'_>]) {
         assert_eq!(relays.len(), 3);
@@ -263,10 +265,12 @@ mod test {
         let dirinfo = (&netdir).into();
         let config = PathConfig::default();
         let guards: OptDummyGuardMgr<'_> = None;
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
 
         for _ in 0..1000 {
             let (path, _, _) = ExitPathBuilder::from_target_ports(ports.clone())
-                .pick_path(&mut rng, dirinfo, guards, &config)
+                .pick_path(&mut rng, dirinfo, guards, &config, now)
                 .unwrap();
 
             assert_same_path_when_owned(&path);
@@ -285,7 +289,7 @@ mod test {
         let config = PathConfig::default();
         for _ in 0..1000 {
             let (path, _, _) = ExitPathBuilder::from_chosen_exit(chosen.clone())
-                .pick_path(&mut rng, dirinfo, guards, &config)
+                .pick_path(&mut rng, dirinfo, guards, &config, now)
                 .unwrap();
             assert_same_path_when_owned(&path);
             if let TorPathInner::Path(p) = path.inner {
@@ -307,11 +311,13 @@ mod test {
             .unwrap();
         let dirinfo = (&netdir).into();
         let guards: OptDummyGuardMgr<'_> = None;
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
 
         let config = PathConfig::default();
         for _ in 0..1000 {
             let (path, _, _) = ExitPathBuilder::for_any_exit()
-                .pick_path(&mut rng, dirinfo, guards, &config)
+                .pick_path(&mut rng, dirinfo, guards, &config, now)
                 .unwrap();
             assert_same_path_when_owned(&path);
             if let TorPathInner::Path(p) = path.inner {
@@ -353,21 +359,24 @@ mod test {
         let dirinfo = (&netdir).into();
         let guards: OptDummyGuardMgr<'_> = None;
         let config = PathConfig::default();
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
 
         // With target ports
         let outcome = ExitPathBuilder::from_target_ports(vec![TargetPort::ipv4(80)])
-            .pick_path(&mut rng, dirinfo, guards, &config);
+            .pick_path(&mut rng, dirinfo, guards, &config, now);
         assert!(outcome.is_err());
         assert!(matches!(outcome, Err(Error::NoExit(_))));
 
         // For any exit
-        let outcome = ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, guards, &config);
+        let outcome =
+            ExitPathBuilder::for_any_exit().pick_path(&mut rng, dirinfo, guards, &config, now);
         assert!(outcome.is_err());
         assert!(matches!(outcome, Err(Error::NoExit(_))));
 
         // For any exit (non-strict, so this will work).
-        let outcome =
-            ExitPathBuilder::for_timeout_testing().pick_path(&mut rng, dirinfo, guards, &config);
+        let outcome = ExitPathBuilder::for_timeout_testing()
+            .pick_path(&mut rng, dirinfo, guards, &config, now);
         assert!(outcome.is_ok());
     }
 
@@ -396,7 +405,7 @@ mod test {
             let mut distinct_exit = HashSet::new();
             for _ in 0..20 {
                 let (path, mon, usable) = ExitPathBuilder::from_target_ports(vec![port443])
-                    .pick_path(&mut rng, dirinfo, Some(&guards), &config)
+                    .pick_path(&mut rng, dirinfo, Some(&guards), &config, rt.wallclock())
                     .unwrap();
                 assert_eq!(path.len(), 3);
                 assert_same_path_when_owned(&path);
@@ -428,7 +437,7 @@ mod test {
             // Now we'll try a forced exit that is not the same same as our
             // actual guard.
             let (path, mon, usable) = ExitPathBuilder::from_chosen_exit(exit_relay.clone())
-                .pick_path(&mut rng, dirinfo, Some(&guards), &config)
+                .pick_path(&mut rng, dirinfo, Some(&guards), &config, rt.wallclock())
                 .unwrap();
             assert_eq!(path.len(), 3);
             if let TorPathInner::Path(p) = path.inner {
@@ -451,7 +460,7 @@ mod test {
             // Finally, try with our exit forced to be our regular guard,
             // and make sure we get a different guard.
             let (path, mon, usable) = ExitPathBuilder::from_chosen_exit(guard_relay.clone())
-                .pick_path(&mut rng, dirinfo, Some(&guards), &config)
+                .pick_path(&mut rng, dirinfo, Some(&guards), &config, rt.wallclock())
                 .unwrap();
             assert_eq!(path.len(), 3);
             if let TorPathInner::Path(p) = path.inner {
diff --git a/crates/tor-circmgr/src/usage.rs b/crates/tor-circmgr/src/usage.rs
index eb75e9be46e5d967b0315d105fa2d97ba6577788..ecd2984a5dafc60f37675def9e5797ba4a60a390 100644
--- a/crates/tor-circmgr/src/usage.rs
+++ b/crates/tor-circmgr/src/usage.rs
@@ -4,6 +4,7 @@ use rand::Rng;
 use serde::{Deserialize, Serialize};
 use std::fmt::{self, Display};
 use std::sync::Arc;
+use std::time::SystemTime;
 use tor_error::bad_api_usage;
 use tracing::debug;
 
@@ -187,6 +188,7 @@ impl TargetCircUsage {
         netdir: crate::DirInfo<'a>,
         guards: Option<&GuardMgr<RT>>,
         config: &crate::PathConfig,
+        now: SystemTime,
     ) -> Result<(
         TorPath<'a>,
         SupportedCircUsage,
@@ -201,7 +203,7 @@ impl TargetCircUsage {
             TargetCircUsage::Preemptive { port, .. } => {
                 // FIXME(eta): this is copypasta from `TargetCircUsage::Exit`.
                 let (path, mon, usable) = ExitPathBuilder::from_target_ports(port.iter().copied())
-                    .pick_path(rng, netdir, guards, config)?;
+                    .pick_path(rng, netdir, guards, config, now)?;
                 let policy = path
                     .exit_policy()
                     .expect("ExitPathBuilder gave us a one-hop circuit?");
@@ -220,7 +222,7 @@ impl TargetCircUsage {
                 isolation,
             } => {
                 let (path, mon, usable) = ExitPathBuilder::from_target_ports(p.clone())
-                    .pick_path(rng, netdir, guards, config)?;
+                    .pick_path(rng, netdir, guards, config, now)?;
                 let policy = path
                     .exit_policy()
                     .expect("ExitPathBuilder gave us a one-hop circuit?");
@@ -236,7 +238,7 @@ impl TargetCircUsage {
             }
             TargetCircUsage::TimeoutTesting => {
                 let (path, mon, usable) = ExitPathBuilder::for_timeout_testing()
-                    .pick_path(rng, netdir, guards, config)?;
+                    .pick_path(rng, netdir, guards, config, now)?;
                 let policy = path.exit_policy();
                 let usage = match policy {
                     Some(policy) if policy.allows_some_port() => SupportedCircUsage::Exit {
@@ -665,6 +667,8 @@ pub(crate) mod test {
         let di = (&netdir).into();
         let config = crate::PathConfig::default();
         let guards: OptDummyGuardMgr<'_> = None;
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
 
         // Only doing basic tests for now.  We'll test the path
         // building code a lot more closely in the tests for TorPath
@@ -672,7 +676,7 @@ pub(crate) mod test {
 
         // First, a one-hop directory circuit
         let (p_dir, u_dir, _, _) = TargetCircUsage::Dir
-            .build_path(&mut rng, di, guards, &config)
+            .build_path(&mut rng, di, guards, &config, now)
             .unwrap();
         assert!(matches!(u_dir, SupportedCircUsage::Dir));
         assert_eq!(p_dir.len(), 1);
@@ -689,7 +693,7 @@ pub(crate) mod test {
             isolation: isolation.clone(),
         };
         let (p_exit, u_exit, _, _) = exit_usage
-            .build_path(&mut rng, di, guards, &config)
+            .build_path(&mut rng, di, guards, &config, now)
             .unwrap();
         assert!(matches!(
             u_exit,
@@ -703,7 +707,7 @@ pub(crate) mod test {
 
         // Now try testing circuits.
         let (path, usage, _, _) = TargetCircUsage::TimeoutTesting
-            .build_path(&mut rng, di, guards, &config)
+            .build_path(&mut rng, di, guards, &config, now)
             .unwrap();
         let path = match OwnedPath::try_from(&path).unwrap() {
             OwnedPath::ChannelOnly(_) => panic!("Impossible path type."),
@@ -741,9 +745,11 @@ pub(crate) mod test {
         let di = (&netdir).into();
         let config = crate::PathConfig::default();
         let guards: OptDummyGuardMgr<'_> = None;
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
 
         let (path, usage, _, _) = TargetCircUsage::TimeoutTesting
-            .build_path(&mut rng, di, guards, &config)
+            .build_path(&mut rng, di, guards, &config, now)
             .unwrap();
         assert_eq!(path.len(), 3);
         assert_isoleq!(usage, SupportedCircUsage::NoUsage);
diff --git a/crates/tor-dirclient/src/lib.rs b/crates/tor-dirclient/src/lib.rs
index 457c257bfd2023937038a5a7b134b11aecd8db6f..5f7fa58e1f45d6f2b97c700a73ccfddea478269c 100644
--- a/crates/tor-dirclient/src/lib.rs
+++ b/crates/tor-dirclient/src/lib.rs
@@ -502,6 +502,7 @@ mod test {
     ) -> (RequestResult<()>, Vec<u8>) {
         // We don't need to do anything fancy here, since we aren't simulating
         // a timeout.
+        #[allow(clippy::disallowed_methods)]
         let mock_time = MockSleepProvider::new(std::time::SystemTime::now());
 
         let mut output = Vec::new();
diff --git a/crates/tor-dirclient/src/request.rs b/crates/tor-dirclient/src/request.rs
index 5c5a72f136a79fe7abac021a6ebc7449de2655db..7d376dac1c76939e4066b73fa4571af800addfca 100644
--- a/crates/tor-dirclient/src/request.rs
+++ b/crates/tor-dirclient/src/request.rs
@@ -469,6 +469,7 @@ mod test {
         .unwrap();
 
         let d2 = b"blah blah blah 12 blah blah blah";
+        #[allow(clippy::disallowed_methods)]
         let d3 = SystemTime::now();
         let mut req = ConsensusRequest::default();
 
diff --git a/crates/tor-dirmgr/src/bootstrap.rs b/crates/tor-dirmgr/src/bootstrap.rs
index 0734e388e3322cbf406f9f4e1293b81a6ea65a26..ec29c13a9566a9fdd2ea0c8b640c3148065e1f6f 100644
--- a/crates/tor-dirmgr/src/bootstrap.rs
+++ b/crates/tor-dirmgr/src/bootstrap.rs
@@ -9,6 +9,7 @@ use std::{
 
 use crate::{
     docid::{self, ClientRequest},
+    state::WriteNetDir,
     upgrade_weak_ref, DirMgr, DirState, DocId, DocumentText, Error, Readiness, Result,
 };
 
@@ -262,10 +263,11 @@ pub(crate) async fn download<R: Runtime>(
         // In theory this could be inside the loop below maybe?  If we
         // want to drop the restriction that the missing() members of a
         // state must never grow, then we'll need to move it inside.
-        {
+        let mut now = {
             let dirmgr = upgrade_weak_ref(&dirmgr)?;
             load_once(&dirmgr, &mut state).await?;
-        }
+            dirmgr.now()
+        };
 
         // Skip the downloads if we can...
         if state.can_advance() {
@@ -283,9 +285,9 @@ pub(crate) async fn download<R: Runtime>(
         // document, or we run out of tries, or we run out of time.
         'next_attempt: for attempt in retry_config.attempts() {
             info!("{}: {}", attempt + 1, state.describe());
-            let reset_time = no_more_than_a_week_from(SystemTime::now(), state.reset_time());
+            let reset_time = no_more_than_a_week_from(now, state.reset_time());
 
-            {
+            now = {
                 let dirmgr = upgrade_weak_ref(&dirmgr)?;
                 futures::select_biased! {
                     outcome = download_attempt(&dirmgr, &mut state, parallelism.into()).fuse() => {
@@ -308,7 +310,8 @@ pub(crate) async fn download<R: Runtime>(
                         continue 'next_state;
                     },
                 };
-            }
+                dirmgr.now()
+            };
 
             // Exit if there is nothing more to download.
             if state.is_ready(Readiness::Complete) {
@@ -329,7 +332,7 @@ pub(crate) async fn download<R: Runtime>(
             } else {
                 // We should wait a bit, and then retry.
                 // TODO: we shouldn't wait on the final attempt.
-                let reset_time = no_more_than_a_week_from(SystemTime::now(), state.reset_time());
+                let reset_time = no_more_than_a_week_from(now, state.reset_time());
                 let delay = retry.next_delay(&mut rand::thread_rng());
                 futures::select_biased! {
                     _ = runtime.sleep_until_wallclock(reset_time).fuse() => {
@@ -373,9 +376,11 @@ mod test {
     use std::convert::TryInto;
     use std::sync::Mutex;
     use tor_netdoc::doc::microdesc::MdDigest;
+    use tor_rtcompat::SleepProvider;
 
     #[test]
     fn week() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_day = Duration::new(86400, 0);
 
@@ -520,14 +525,13 @@ mod test {
     fn all_in_cache() {
         // Let's try bootstrapping when everything is in the cache.
         tor_rtcompat::test_with_one_runtime!(|rt| async {
+            let now = rt.wallclock();
             let (_tempdir, mgr) = new_mgr(rt);
 
             {
                 let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
                 for h in [H1, H2, H3, H4, H5] {
-                    store
-                        .store_microdescs(&[("ignore", &h)], SystemTime::now())
-                        .unwrap();
+                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
                 }
             }
             let mgr = Arc::new(mgr);
@@ -553,14 +557,13 @@ mod test {
         // Let's try bootstrapping with all of phase1 and part of
         // phase 2 in cache.
         tor_rtcompat::test_with_one_runtime!(|rt| async {
+            let now = rt.wallclock();
             let (_tempdir, mgr) = new_mgr(rt);
 
             {
                 let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
                 for h in [H1, H2, H3] {
-                    store
-                        .store_microdescs(&[("ignore", &h)], SystemTime::now())
-                        .unwrap();
+                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
                 }
             }
             {
diff --git a/crates/tor-dirmgr/src/event.rs b/crates/tor-dirmgr/src/event.rs
index 3a28445a890a34fb3505914ddf409d32832a1ce7..b68f92a9a095b75511a0bcc3efbb172c743f6362 100644
--- a/crates/tor-dirmgr/src/event.rs
+++ b/crates/tor-dirmgr/src/event.rs
@@ -650,6 +650,7 @@ mod test {
 
     #[test]
     fn dir_status_basics() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let hour = Duration::new(3600, 0);
 
diff --git a/crates/tor-dirmgr/src/lib.rs b/crates/tor-dirmgr/src/lib.rs
index 23a594199fe8943a3f346250f26685e00191b33f..15f12cbb5d58d45260510ca68c79ac23989f036f 100644
--- a/crates/tor-dirmgr/src/lib.rs
+++ b/crates/tor-dirmgr/src/lib.rs
@@ -1126,6 +1126,7 @@ mod test {
     use std::time::Duration;
     use tempfile::TempDir;
     use tor_netdoc::doc::{authcert::AuthCertKeyIds, netstatus::Lifetime};
+    use tor_rtcompat::SleepProvider;
 
     pub(crate) fn new_mgr<R: Runtime>(runtime: R) -> (TempDir, DirMgr<R>) {
         let dir = TempDir::new().unwrap();
@@ -1151,12 +1152,12 @@ mod test {
     #[test]
     fn load_and_store_internals() {
         tor_rtcompat::test_with_one_runtime!(|rt| async {
-            let (_tempdir, mgr) = new_mgr(rt);
-
-            let now = SystemTime::now();
+            let now = rt.wallclock();
             let tomorrow = now + Duration::from_secs(86400);
             let later = tomorrow + Duration::from_secs(86400);
 
+            let (_tempdir, mgr) = new_mgr(rt);
+
             // Seed the storage with a bunch of junk.
             let d1 = [5_u8; 32];
             let d2 = [7; 32];
@@ -1266,12 +1267,12 @@ mod test {
     #[test]
     fn make_consensus_request() {
         tor_rtcompat::test_with_one_runtime!(|rt| async {
-            let (_tempdir, mgr) = new_mgr(rt);
-
-            let now = SystemTime::now();
+            let now = rt.wallclock();
             let tomorrow = now + Duration::from_secs(86400);
             let later = tomorrow + Duration::from_secs(86400);
 
+            let (_tempdir, mgr) = new_mgr(rt);
+
             // Try with an empty store.
             let req = mgr
                 .make_consensus_request(now, ConsensusFlavor::Microdesc)
@@ -1363,6 +1364,9 @@ mod test {
     #[test]
     fn expand_response() {
         tor_rtcompat::test_with_one_runtime!(|rt| async {
+            let now = rt.wallclock();
+            let day = Duration::from_secs(86400);
+
             let (_tempdir, mgr) = new_mgr(rt);
 
             // Try a simple request: nothing should happen.
@@ -1386,8 +1390,6 @@ mod test {
             // we can ask for a diff.
             {
                 let mut store = mgr.store.lock().unwrap();
-                let now = SystemTime::now();
-                let day = Duration::from_secs(86400);
                 let d_in = [0x99; 32]; // This one, we can fake.
                 let cmeta = ConsensusMeta::new(
                     Lifetime::new(now, now + day, now + 2 * day).unwrap(),
diff --git a/crates/tor-dirmgr/src/state.rs b/crates/tor-dirmgr/src/state.rs
index b99d47f5c2c58d9140beb18144a617c65cc7fb5f..b2f2915d08082b76cc75de350de610631993ef69 100644
--- a/crates/tor-dirmgr/src/state.rs
+++ b/crates/tor-dirmgr/src/state.rs
@@ -110,7 +110,7 @@ impl<R: Runtime> WriteNetDir for crate::DirMgr<R> {
         }
     }
     fn now(&self) -> SystemTime {
-        SystemTime::now()
+        self.runtime.wallclock()
     }
 
     #[cfg(feature = "dirfilter")]
diff --git a/crates/tor-guardmgr/src/guard.rs b/crates/tor-guardmgr/src/guard.rs
index ca4dec56927ad0fb62edde72d52f630df0aa8791..5f722cb764d79c91036568b75fcad4c7a14ef25c 100644
--- a/crates/tor-guardmgr/src/guard.rs
+++ b/crates/tor-guardmgr/src/guard.rs
@@ -693,6 +693,7 @@ mod test {
     fn basic_guard() -> Guard {
         let id = basic_id();
         let ports = vec!["127.0.0.7:7777".parse().unwrap()];
+        #[allow(clippy::disallowed_methods)]
         let added = SystemTime::now();
         Guard::new(id, ports, added)
     }
@@ -813,9 +814,11 @@ mod test {
     fn record_success() {
         let t1 = Instant::now() - Duration::from_secs(10);
         // has to be in the future, since the guard's "added_at" time is based on now.
-        let t2 = SystemTime::now() + Duration::from_secs(300 * 86400);
+        #[allow(clippy::disallowed_methods)]
+        let now = SystemTime::now();
+        let t2 = now + Duration::from_secs(300 * 86400);
         let t3 = Instant::now() + Duration::from_secs(310 * 86400);
-        let t4 = SystemTime::now() + Duration::from_secs(320 * 86400);
+        let t4 = now + Duration::from_secs(320 * 86400);
 
         let mut g = basic_guard();
         g.record_failure(t1, true);
@@ -868,6 +871,7 @@ mod test {
     fn expiration() {
         const DAY: Duration = Duration::from_secs(24 * 60 * 60);
         let params = GuardParams::default();
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
 
         let g = basic_guard();
@@ -899,6 +903,7 @@ mod test {
             .unwrap_if_sufficient()
             .unwrap();
         let params = GuardParams::default();
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
 
         // Construct a guard from a relay from the netdir.
@@ -955,6 +960,7 @@ mod test {
         .unwrap();
 
         //let params = GuardParams::default();
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
 
         // Try a guard that isn't in the netdir at all.
@@ -1037,6 +1043,7 @@ mod test {
         let mut g = basic_guard();
         let params = GuardParams::default();
 
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
 
         let _ignore = g.record_success(now, &params);
diff --git a/crates/tor-guardmgr/src/sample.rs b/crates/tor-guardmgr/src/sample.rs
index 085f042d41f0f79f65eff8cd693962d4fed6e416..254f7a9fbc692760cc18d230b04fa28f33250372 100644
--- a/crates/tor-guardmgr/src/sample.rs
+++ b/crates/tor-guardmgr/src/sample.rs
@@ -869,6 +869,7 @@ mod test {
         let mut samples: Vec<HashSet<GuardId>> = Vec::new();
         for _ in 0..3 {
             let mut guards = GuardSet::default();
+            #[allow(clippy::disallowed_methods)]
             guards.extend_sample_as_needed(SystemTime::now(), &params, &netdir);
             assert_eq!(guards.guards.len(), params.min_filtered_sample_size);
             assert_eq!(guards.confirmed.len(), 0);
@@ -881,10 +882,14 @@ mod test {
                 assert!(relay.is_flagged_guard());
                 assert!(relay.is_dir_cache());
                 assert!(guards.contains_relay(&relay));
-                assert!(!guard.is_expired(&params, SystemTime::now()));
+                #[allow(clippy::disallowed_methods)]
+                {
+                    assert!(!guard.is_expired(&params, SystemTime::now()));
+                }
             }
 
             // Make sure that the sample doesn't expand any further.
+            #[allow(clippy::disallowed_methods)]
             guards.extend_sample_as_needed(SystemTime::now(), &params, &netdir);
             assert_eq!(guards.guards.len(), params.min_filtered_sample_size);
             guards.assert_consistency();
@@ -904,8 +909,10 @@ mod test {
             min_filtered_sample_size: 5,
             ..GuardParams::default()
         };
+
+        #[allow(clippy::disallowed_methods)]
         let t1 = SystemTime::now();
-        let t2 = SystemTime::now() + Duration::from_secs(20);
+        let t2 = t1 + Duration::from_secs(20);
 
         let mut guards = GuardSet::default();
         guards.extend_sample_as_needed(t1, &params, &netdir);
@@ -940,9 +947,10 @@ mod test {
             n_primary: 4,
             ..GuardParams::default()
         };
+        #[allow(clippy::disallowed_methods)]
         let t1 = SystemTime::now();
-        let t2 = SystemTime::now() + Duration::from_secs(20);
-        let t3 = SystemTime::now() + Duration::from_secs(30);
+        let t2 = t1 + Duration::from_secs(20);
+        let t3 = t2 + Duration::from_secs(30);
 
         let mut guards = GuardSet::default();
         guards.extend_sample_as_needed(t1, &params, &netdir);
@@ -986,6 +994,7 @@ mod test {
     fn expiration() {
         let netdir = netdir();
         let params = GuardParams::default();
+        #[allow(clippy::disallowed_methods)]
         let t1 = SystemTime::now();
 
         let mut guards = GuardSet::default();
@@ -1020,6 +1029,7 @@ mod test {
             n_primary: 2,
             ..GuardParams::default()
         };
+        #[allow(clippy::disallowed_methods)]
         let st1 = SystemTime::now();
         let i1 = Instant::now();
         let sec = Duration::from_secs(1);
@@ -1143,6 +1153,7 @@ mod test {
             max_sample_bw_fraction: 1.0,
             ..GuardParams::default()
         };
+        #[allow(clippy::disallowed_methods)]
         let mut st = SystemTime::now();
         let mut inst = Instant::now();
         let sec = Duration::from_secs(1);
@@ -1185,6 +1196,7 @@ mod test {
 
         let mut guards = GuardSet::default();
 
+        #[allow(clippy::disallowed_methods)]
         guards.extend_sample_as_needed(SystemTime::now(), &params, &netdir);
         guards.select_primary_guards(&params);
 
@@ -1222,11 +1234,13 @@ mod test {
         };
         let usage = crate::GuardUsageBuilder::default().build().unwrap();
         let mut guards = GuardSet::default();
+        #[allow(clippy::disallowed_methods)]
         guards.extend_sample_as_needed(SystemTime::now(), &params, &netdir);
         guards.select_primary_guards(&params);
         assert_eq!(guards.primary.len(), 2);
 
         let (_kind, p_id1) = guards.pick_guard(&usage, &params).unwrap();
+        #[allow(clippy::disallowed_methods)]
         guards.record_success(&p_id1, &params, SystemTime::now());
         assert_eq!(guards.missing_primary_microdescriptors(&netdir), 0);
 
diff --git a/crates/tor-guardmgr/src/util.rs b/crates/tor-guardmgr/src/util.rs
index 48aaab1068af2b38545fa1285f6487d917252779..5e89e1a675eb1f14b119616f5db092acf07eddfc 100644
--- a/crates/tor-guardmgr/src/util.rs
+++ b/crates/tor-guardmgr/src/util.rs
@@ -66,6 +66,7 @@ mod test {
 
     #[test]
     fn test_randomize_time() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_hour = Duration::from_secs(3600);
         let ten_sec = Duration::from_secs(10);
diff --git a/crates/tor-netdir/src/testnet.rs b/crates/tor-netdir/src/testnet.rs
index a8ca364f4cce071a571bd9dc25d4861b1080b925..d08a0ef17f0fc80e6c75c6aace5eba8f7f1a519f 100644
--- a/crates/tor-netdir/src/testnet.rs
+++ b/crates/tor-netdir/src/testnet.rs
@@ -138,6 +138,7 @@ where
         f | RelayFlags::EXIT | RelayFlags::GUARD,
     ];
 
+    #[allow(clippy::disallowed_methods)]
     let now = SystemTime::now();
     let one_day = Duration::new(86400, 0);
     let mut bld = MdConsensus::builder();
diff --git a/crates/tor-netdir/src/weight.rs b/crates/tor-netdir/src/weight.rs
index a1170bbe5cbd221a275250dd3e79ff40b2fd870d..ecf8048851883d7a33d9b744ed1a13cc33f4fef8 100644
--- a/crates/tor-netdir/src/weight.rs
+++ b/crates/tor-netdir/src/weight.rs
@@ -619,6 +619,7 @@ mod test {
     #[test]
     fn weightset_from_consensus() {
         use rand::Rng;
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_hour = Duration::new(3600, 0);
         let mut rng = rand::thread_rng();
diff --git a/crates/tor-netdoc/src/doc/authcert/build.rs b/crates/tor-netdoc/src/doc/authcert/build.rs
index 7c29e171c361112a41e77a286fb4c5c7cbbcfa77..eb09e251d80485f02928297fa6faf3500ce83a47 100644
--- a/crates/tor-netdoc/src/doc/authcert/build.rs
+++ b/crates/tor-netdoc/src/doc/authcert/build.rs
@@ -146,6 +146,7 @@ mod test {
 
     #[test]
     fn simple_cert() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_hour = Duration::new(3600, 0);
         let later = now + one_hour * 2;
@@ -166,6 +167,7 @@ mod test {
 
     #[test]
     fn failing_cert() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_hour = Duration::new(3600, 0);
         let later = now + one_hour * 2;
diff --git a/crates/tor-netdoc/src/doc/netstatus/build.rs b/crates/tor-netdoc/src/doc/netstatus/build.rs
index 0863b8eb291ee334f621b4b577aaa03426a87548..e1eb350c7e04ace7076f57c8b419a94497cf3b79 100644
--- a/crates/tor-netdoc/src/doc/netstatus/build.rs
+++ b/crates/tor-netdoc/src/doc/netstatus/build.rs
@@ -377,6 +377,7 @@ mod test {
 
     #[test]
     fn consensus() {
+        #[allow(clippy::disallowed_methods)]
         let now = SystemTime::now();
         let one_hour = Duration::new(3600, 0);
 
diff --git a/crates/tor-rtcompat/src/lib.rs b/crates/tor-rtcompat/src/lib.rs
index 33f3ed9f464384976f28cce4e51f077714f3f9ae..3ed74c724c26a3991d076177adf1a43c094bb66d 100644
--- a/crates/tor-rtcompat/src/lib.rs
+++ b/crates/tor-rtcompat/src/lib.rs
@@ -468,7 +468,7 @@ mod test {
     use native_tls_crate as native_tls;
     use std::io::Result as IoResult;
     use std::net::{Ipv4Addr, SocketAddrV4};
-    use std::time::{Duration, Instant, SystemTime};
+    use std::time::{Duration, Instant};
 
     // Test "sleep" with a tiny delay, and make sure that at least that
     // much delay happens.
@@ -519,14 +519,14 @@ mod test {
         let rt = runtime.clone();
         runtime.block_on(async {
             let i1 = Instant::now();
-            let now = SystemTime::now();
+            let now = runtime.wallclock();
             let one_millis = Duration::from_millis(1);
             let one_millis_later = now + one_millis;
 
             rt.sleep_until_wallclock(one_millis_later).await;
 
             let i2 = Instant::now();
-            let newtime = SystemTime::now();
+            let newtime = runtime.wallclock();
             assert!(newtime >= one_millis_later);
             assert!(i2 - i1 >= one_millis);
         });
diff --git a/crates/tor-rtcompat/src/timer.rs b/crates/tor-rtcompat/src/timer.rs
index f2e0dc7e5f8cc94ee6014ccf60524fd48832c638..d06765186a999c397038d36fed352e094c5f3424 100644
--- a/crates/tor-rtcompat/src/timer.rs
+++ b/crates/tor-rtcompat/src/timer.rs
@@ -203,6 +203,7 @@ mod test {
         }
         let minute = Duration::from_secs(60);
         let second = Duration::from_secs(1);
+        #[allow(clippy::disallowed_methods)]
         let start = SystemTime::now();
 
         let target = start + 30 * minute;
diff --git a/crates/tor-rtcompat/src/traits.rs b/crates/tor-rtcompat/src/traits.rs
index 0ec17c8e220fe89315644a0091bd005933c24cae..8707f3d88c575eb3c3e5e652c62ebb64950304f8 100644
--- a/crates/tor-rtcompat/src/traits.rs
+++ b/crates/tor-rtcompat/src/traits.rs
@@ -95,6 +95,7 @@ pub trait SleepProvider {
     ///
     /// (This is the same as `SystemTime::now`, if not running in test mode.)
     fn wallclock(&self) -> SystemTime {
+        #[allow(clippy::disallowed_methods)]
         SystemTime::now()
     }
 
diff --git a/crates/tor-rtmock/src/sleep_runtime.rs b/crates/tor-rtmock/src/sleep_runtime.rs
index bcbc9c306bce814d8cd5e0b98377ceb650b8fd12..2a1df51356b97eeb9a4c90f89528e1d4791177d6 100644
--- a/crates/tor-rtmock/src/sleep_runtime.rs
+++ b/crates/tor-rtmock/src/sleep_runtime.rs
@@ -26,6 +26,7 @@ impl<R: Runtime> MockSleepRuntime<R> {
     /// Create a new runtime that wraps `runtime`, but overrides
     /// its view of time with a [`MockSleepProvider`].
     pub fn new(runtime: R) -> Self {
+        #[allow(clippy::disallowed_methods)]
         let sleep = MockSleepProvider::new(SystemTime::now());
         MockSleepRuntime { runtime, sleep }
     }
diff --git a/crates/tor-rtmock/src/time.rs b/crates/tor-rtmock/src/time.rs
index c4d703e7b6aa292751e0ca2318e879f5bb0c9b44..e0e3c0cf10b6278d47a464dde2c54652cff94c4e 100644
--- a/crates/tor-rtmock/src/time.rs
+++ b/crates/tor-rtmock/src/time.rs
@@ -439,6 +439,7 @@ mod test {
 
     #[test]
     fn basics_of_time_travel() {
+        #[allow(clippy::disallowed_methods)]
         let w1 = SystemTime::now();
         let sp = MockSleepProvider::new(w1);
         let i1 = sp.now();
@@ -461,6 +462,7 @@ mod test {
             use std::sync::atomic::AtomicBool;
             use std::sync::atomic::Ordering;
 
+            #[allow(clippy::disallowed_methods)]
             let sp = MockSleepProvider::new(SystemTime::now());
             let one_hour = Duration::new(3600, 0);
 
diff --git a/crates/tor-rtmock/tests/rtcompat_timing.rs b/crates/tor-rtmock/tests/rtcompat_timing.rs
index 6c4daf68bcadb11980388e8259f8c295496a5af8..21d6622cbce65eb2f5f68440abe982f13bc9a17a 100644
--- a/crates/tor-rtmock/tests/rtcompat_timing.rs
+++ b/crates/tor-rtmock/tests/rtcompat_timing.rs
@@ -17,6 +17,7 @@ fn timeouts() {
         oneshot::Sender<()>,
         Timeout<oneshot::Receiver<()>, tor_rtmock::time::Sleeping>,
     ) {
+        #[allow(clippy::disallowed_methods)]
         let start = SystemTime::now();
         let (send, recv) = oneshot::channel::<()>();
         let mock_sp = MockSleepProvider::new(start);