diff --git a/crates/tor-dirmgr/src/state.rs b/crates/tor-dirmgr/src/state.rs
index e290763d39879f11a70836e908570b10a7daffc7..4b914e9c7decdb2dad65851dd301f71695d3493b 100644
--- a/crates/tor-dirmgr/src/state.rs
+++ b/crates/tor-dirmgr/src/state.rs
@@ -24,6 +24,8 @@ use tracing::{info, warn};
 
 use crate::event::{DirStatus, DirStatusInner};
 
+#[cfg(feature = "dirfilter")]
+use crate::filter::{DirFilter, NilFilter};
 use crate::storage::DynStore;
 use crate::DocSource;
 use crate::{
@@ -141,28 +143,9 @@ pub(crate) trait DirState: Send {
     fn reset(self: Box<Self>) -> Result<Box<dyn DirState>>;
 }
 
-/// An object where we can put a usable netdir.
-///
-/// Note that there's only one implementation for this trait: DirMgr.
-/// We make this a trait anyway to make sure that the different states
-/// in this module can _only_ interact with the DirMgr through
-/// modifying the NetDir and looking at the configuration.
-pub(crate) trait WriteNetDir: 'static + Sync + Send {
-    /// Return the currently configured DynFilter for this state.
-    #[cfg(feature = "dirfilter")]
-    fn filter(&self) -> &dyn crate::filter::DirFilter;
-}
-
-impl<R: Runtime> WriteNetDir for crate::DirMgr<R> {
-    #[cfg(feature = "dirfilter")]
-    fn filter(&self) -> &dyn crate::filter::DirFilter {
-        self.filter.as_deref().unwrap_or(&crate::filter::NilFilter)
-    }
-}
-
 /// Initial state: fetching or loading a consensus directory.
 #[derive(Clone, Debug)]
-pub(crate) struct GetConsensusState<DM: WriteNetDir, R: Runtime> {
+pub(crate) struct GetConsensusState<R: Runtime> {
     /// How should we get the consensus from the cache, if at all?
     cache_usage: CacheUsage,
 
@@ -177,7 +160,7 @@ pub(crate) struct GetConsensusState<DM: WriteNetDir, R: Runtime> {
     /// If present, our next state.
     ///
     /// (This is present once we have a consensus.)
-    next: Option<GetCertsState<DM, R>>,
+    next: Option<GetCertsState<R>>,
 
     /// A list of RsaIdentity for the authorities that we believe in.
     ///
@@ -185,10 +168,6 @@ pub(crate) struct GetConsensusState<DM: WriteNetDir, R: Runtime> {
     /// more than half of these authorities.
     authority_ids: Vec<RsaIdentity>,
 
-    /// A weak reference to the directory manager that wants us to
-    /// fetch this information.  When this references goes away, we exit.
-    writedir: Weak<DM>,
-
     /// A `Runtime` implementation.
     rt: R,
     /// The configuration of the directory manager. Used for download configuration
@@ -196,24 +175,36 @@ pub(crate) struct GetConsensusState<DM: WriteNetDir, R: Runtime> {
     config: Arc<DirMgrConfig>,
     /// If one exists, the netdir we're trying to update.
     prev_netdir: Option<Arc<NetDir>>,
+
+    /// A filter that gets applied to directory objects before we use them.
+    #[cfg(feature = "dirfilter")]
+    filter: Arc<dyn DirFilter>,
 }
 
-impl<R: Runtime> GetConsensusState<DirMgr<R>, R> {
+impl<R: Runtime> GetConsensusState<R> {
     /// Bodge version of Self::new() with the old pre-refactor signature.
     /// This will go away when the refactor is complete.
+    #[allow(clippy::needless_pass_by_value)]
     pub(crate) fn bodge_new(writedir: Weak<DirMgr<R>>, cache_usage: CacheUsage) -> Result<Self> {
         if let Some(netdir) = Weak::upgrade(&writedir) {
             let config = netdir.config.get();
             let prev_netdir = netdir.opt_netdir();
             let rt = netdir.runtime.clone();
-            Ok(Self::new(rt, config, cache_usage, prev_netdir, writedir))
+            Ok(Self::new(
+                rt,
+                config,
+                cache_usage,
+                prev_netdir,
+                #[cfg(feature = "dirfilter")]
+                netdir.filter.clone().unwrap_or_else(|| Arc::new(NilFilter)),
+            ))
         } else {
             Err(Error::ManagerDropped)
         }
     }
 }
 
-impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
+impl<R: Runtime> GetConsensusState<R> {
     /// Create a new `GetConsensusState`, using the cache as per `cache_usage` and downloading as
     /// per the relevant sections of `config`. If `prev_netdir` is supplied, information from that
     /// directory may be used to complete the next one.
@@ -222,8 +213,7 @@ impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
         config: Arc<DirMgrConfig>,
         cache_usage: CacheUsage,
         prev_netdir: Option<Arc<NetDir>>,
-        // NOTE(eta): This `writedir` is going away soon.
-        writedir: Weak<DM>,
+        #[cfg(feature = "dirfilter")] filter: Arc<dyn DirFilter>,
     ) -> Self {
         let authority_ids = config
             .authorities()
@@ -237,15 +227,16 @@ impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
             after,
             next: None,
             authority_ids,
-            writedir,
             rt,
             config,
             prev_netdir,
+            #[cfg(feature = "dirfilter")]
+            filter,
         }
     }
 }
 
-impl<DM: WriteNetDir, R: Runtime> DirState for GetConsensusState<DM, R> {
+impl<R: Runtime> DirState for GetConsensusState<R> {
     fn describe(&self) -> String {
         if self.next.is_some() {
             "About to fetch certificates."
@@ -332,7 +323,7 @@ impl<DM: WriteNetDir, R: Runtime> DirState for GetConsensusState<DM, R> {
     }
 }
 
-impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
+impl<R: Runtime> GetConsensusState<R> {
     /// Helper: try to set the current consensus text from an input
     /// string `text`.  Refuse it if the authorities could never be
     /// correct, or if it is ill-formed.
@@ -346,11 +337,7 @@ impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
             let (signedval, remainder, parsed) =
                 MdConsensus::parse(text).map_err(|e| Error::from_netdoc(source.clone(), e))?;
             #[cfg(feature = "dirfilter")]
-            let parsed = if let Some(wd) = Weak::upgrade(&self.writedir) {
-                wd.filter().filter_consensus(parsed)?
-            } else {
-                parsed
-            };
+            let parsed = self.filter.filter_consensus(parsed)?;
             let now = self.rt.wallclock();
             let timely = parsed.check_valid_at(&now)?;
             let meta = ConsensusMeta::from_unvalidated(signedval, remainder, &timely);
@@ -382,10 +369,11 @@ impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
             consensus_meta,
             missing_certs: desired_certs,
             certs: Vec::new(),
-            writedir: Weak::clone(&self.writedir),
             rt: self.rt.clone(),
             config: self.config.clone(),
             prev_netdir: self.prev_netdir.take(),
+            #[cfg(feature = "dirfilter")]
+            filter: self.filter.clone(),
         });
 
         // Unwrap should be safe because `next` was just assigned
@@ -408,7 +396,7 @@ impl<DM: WriteNetDir, R: Runtime> GetConsensusState<DM, R> {
 /// we are given a bad consensus signed with fictional certificates
 /// that we can never find.
 #[derive(Clone, Debug)]
-struct GetCertsState<DM: WriteNetDir, R: Runtime> {
+struct GetCertsState<R: Runtime> {
     /// The cache usage we had in mind when we began.  Used to reset.
     cache_usage: CacheUsage,
     /// Where did we get our consensus?
@@ -422,8 +410,6 @@ struct GetCertsState<DM: WriteNetDir, R: Runtime> {
     missing_certs: HashSet<AuthCertKeyIds>,
     /// A list of the certificates we've been able to load or download.
     certs: Vec<AuthCert>,
-    /// Reference to our directory manager.
-    writedir: Weak<DM>,
 
     /// A `Runtime` implementation.
     rt: R,
@@ -432,9 +418,13 @@ struct GetCertsState<DM: WriteNetDir, R: Runtime> {
     config: Arc<DirMgrConfig>,
     /// If one exists, the netdir we're trying to update.
     prev_netdir: Option<Arc<NetDir>>,
+
+    /// A filter that gets applied to directory objects before we use them.
+    #[cfg(feature = "dirfilter")]
+    filter: Arc<dyn DirFilter>,
 }
 
-impl<DM: WriteNetDir, R: Runtime> DirState for GetCertsState<DM, R> {
+impl<R: Runtime> DirState for GetCertsState<R> {
     fn describe(&self) -> String {
         let total = self.certs.len() + self.missing_certs.len();
         format!(
@@ -565,10 +555,11 @@ impl<DM: WriteNetDir, R: Runtime> DirState for GetCertsState<DM, R> {
                 self.cache_usage,
                 validated,
                 self.consensus_meta,
-                self.writedir,
                 self.rt,
                 self.config,
                 self.prev_netdir,
+                #[cfg(feature = "dirfilter")]
+                self.filter,
             )))
         } else {
             Ok(self)
@@ -583,20 +574,19 @@ impl<DM: WriteNetDir, R: Runtime> DirState for GetCertsState<DM, R> {
             self.config,
             self.cache_usage,
             self.prev_netdir,
-            self.writedir,
+            #[cfg(feature = "dirfilter")]
+            self.filter,
         )))
     }
 }
 
 /// Final state: we're fetching or loading microdescriptors
 #[derive(Debug, Clone)]
-struct GetMicrodescsState<DM: WriteNetDir, R: Runtime> {
+struct GetMicrodescsState<R: Runtime> {
     /// How should we get the consensus from the cache, if at all?
     cache_usage: CacheUsage,
     /// Total number of microdescriptors listed in the consensus.
     n_microdescs: usize,
-    /// The dirmgr to inform about a usable directory.
-    writedir: Weak<DM>,
     /// The current status of our netdir.
     partial: PendingNetDir,
     /// Metadata for the current consensus.
@@ -616,6 +606,10 @@ struct GetMicrodescsState<DM: WriteNetDir, R: Runtime> {
     config: Arc<DirMgrConfig>,
     /// If one exists, the netdir we're trying to update.
     prev_netdir: Option<Arc<NetDir>>,
+
+    /// A filter that gets applied to directory objects before we use them.
+    #[cfg(feature = "dirfilter")]
+    filter: Arc<dyn DirFilter>,
 }
 
 /// Information about a network directory that might not be ready to become _the_ current network
@@ -745,17 +739,17 @@ impl PendingNetDir {
     }
 }
 
-impl<DM: WriteNetDir, R: Runtime> GetMicrodescsState<DM, R> {
+impl<R: Runtime> GetMicrodescsState<R> {
     /// Create a new [`GetMicrodescsState`] from a provided
     /// microdescriptor consensus.
     fn new(
         cache_usage: CacheUsage,
         consensus: MdConsensus,
         meta: ConsensusMeta,
-        writedir: Weak<DM>,
         rt: R,
         config: Arc<DirMgrConfig>,
         prev_netdir: Option<Arc<NetDir>>,
+        #[cfg(feature = "dirfilter")] filter: Arc<dyn DirFilter>,
     ) -> Self {
         let reset_time = consensus.lifetime().valid_until();
         let n_microdescs = consensus.relays().len();
@@ -769,7 +763,6 @@ impl<DM: WriteNetDir, R: Runtime> GetMicrodescsState<DM, R> {
         GetMicrodescsState {
             cache_usage,
             n_microdescs,
-            writedir,
             partial: PendingNetDir::Partial(partial_dir),
             meta,
             newly_listed: Vec::new(),
@@ -777,6 +770,9 @@ impl<DM: WriteNetDir, R: Runtime> GetMicrodescsState<DM, R> {
             rt,
             config,
             prev_netdir,
+
+            #[cfg(feature = "dirfilter")]
+            filter,
         }
     }
 
@@ -788,13 +784,10 @@ impl<DM: WriteNetDir, R: Runtime> GetMicrodescsState<DM, R> {
         I: IntoIterator<Item = Microdesc>,
     {
         #[cfg(feature = "dirfilter")]
-        let mds: Vec<Microdesc> = if let Some(wd) = Weak::upgrade(&self.writedir) {
-            mds.into_iter()
-                .filter_map(|m| wd.filter().filter_md(m).ok())
-                .collect()
-        } else {
-            mds.into_iter().collect()
-        };
+        let mds: Vec<Microdesc> = mds
+            .into_iter()
+            .filter_map(|m| self.filter.filter_md(m).ok())
+            .collect();
         let is_partial = matches!(self.partial, PendingNetDir::Partial(..));
         for md in mds {
             if is_partial {
@@ -806,7 +799,7 @@ impl<DM: WriteNetDir, R: Runtime> GetMicrodescsState<DM, R> {
     }
 }
 
-impl<DM: WriteNetDir, R: Runtime> DirState for GetMicrodescsState<DM, R> {
+impl<R: Runtime> DirState for GetMicrodescsState<R> {
     fn describe(&self) -> String {
         format!(
             "Downloading microdescriptors (we are missing {}).",
@@ -963,7 +956,8 @@ impl<DM: WriteNetDir, R: Runtime> DirState for GetMicrodescsState<DM, R> {
             self.config,
             cache_usage,
             self.prev_netdir,
-            self.writedir,
+            #[cfg(feature = "dirfilter")]
+            self.filter,
         )))
     }
 }
@@ -1076,21 +1070,6 @@ mod test {
         Arc::new(cfg)
     }
 
-    struct DirRcv;
-
-    impl DirRcv {
-        fn new() -> Self {
-            DirRcv {}
-        }
-    }
-
-    impl WriteNetDir for DirRcv {
-        #[cfg(feature = "dirfilter")]
-        fn filter(&self) -> &dyn crate::filter::DirFilter {
-            &crate::filter::NilFilter
-        }
-    }
-
     // Test data
     const CONSENSUS: &str = include_str!("../testdata/mdconsensus1.txt");
     const CONSENSUS2: &str = include_str!("../testdata/mdconsensus2.txt");
@@ -1152,7 +1131,6 @@ mod test {
     #[test]
     fn get_consensus_state() {
         tor_rtcompat::test_with_one_runtime!(|rt| async move {
-            let rcv = Arc::new(DirRcv::new());
             let rt = make_time_shifted_runtime(test_time(), rt);
             let cfg = make_dirmgr_config(None);
 
@@ -1163,7 +1141,8 @@ mod test {
                 cfg,
                 CacheUsage::CacheOkay,
                 None,
-                Arc::downgrade(&rcv),
+                #[cfg(feature = "dirfilter")]
+                Arc::new(NilFilter),
             );
 
             // Is description okay?
@@ -1230,7 +1209,8 @@ mod test {
                 cfg,
                 CacheUsage::CacheOkay,
                 None,
-                Arc::downgrade(&rcv),
+                #[cfg(feature = "dirfilter")]
+                Arc::new(NilFilter),
             );
             let outcome = state.add_from_download(CONSENSUS, &req, Some(&store));
             assert!(outcome.unwrap());
@@ -1253,8 +1233,14 @@ mod test {
 
             // Try again, but this time get the state from the cache.
             let cfg = make_dirmgr_config(Some(test_authorities()));
-            let mut state =
-                GetConsensusState::new(rt, cfg, CacheUsage::CacheOkay, None, Arc::downgrade(&rcv));
+            let mut state = GetConsensusState::new(
+                rt,
+                cfg,
+                CacheUsage::CacheOkay,
+                None,
+                #[cfg(feature = "dirfilter")]
+                Arc::new(NilFilter),
+            );
             let text: crate::storage::InputString = CONSENSUS.to_owned().into();
             let map = vec![(docid, text.into())].into_iter().collect();
             let outcome = state.add_from_cache(map);
@@ -1267,8 +1253,7 @@ mod test {
     fn get_certs_state() {
         tor_rtcompat::test_with_one_runtime!(|rt| async move {
             /// Construct a GetCertsState with our test data
-            fn new_getcerts_state(rt: impl Runtime) -> (Arc<DirRcv>, Box<dyn DirState>) {
-                let rcv = Arc::new(DirRcv::new());
+            fn new_getcerts_state(rt: impl Runtime) -> Box<dyn DirState> {
                 let rt = make_time_shifted_runtime(test_time(), rt);
                 let cfg = make_dirmgr_config(Some(test_authorities()));
                 let mut state = GetConsensusState::new(
@@ -1276,17 +1261,18 @@ mod test {
                     cfg,
                     CacheUsage::CacheOkay,
                     None,
-                    Arc::downgrade(&rcv),
+                    #[cfg(feature = "dirfilter")]
+                    Arc::new(NilFilter),
                 );
                 let req = tor_dirclient::request::ConsensusRequest::new(ConsensusFlavor::Microdesc);
                 let req = crate::docid::ClientRequest::Consensus(req);
                 let outcome = state.add_from_download(CONSENSUS, &req, None);
                 assert!(outcome.unwrap());
-                (rcv, Box::new(state).advance().unwrap())
+                Box::new(state).advance().unwrap()
             }
 
             let (_tempdir, store) = temp_store();
-            let (_rcv, mut state) = new_getcerts_state(rt.clone());
+            let mut state = new_getcerts_state(rt.clone());
             // Basic properties: description, status, reset time.
             assert_eq!(
                 &state.describe(),
@@ -1370,7 +1356,7 @@ mod test {
             );
 
             // If we start from scratch and reset, we're back in GetConsensus.
-            let (_rcv, state) = new_getcerts_state(rt);
+            let state = new_getcerts_state(rt);
             let state = state.reset().unwrap();
             assert_eq!(&state.describe(), "Looking for a consensus.");
 
@@ -1383,10 +1369,7 @@ mod test {
     fn get_microdescs_state() {
         tor_rtcompat::test_with_one_runtime!(|rt| async move {
             /// Construct a GetCertsState with our test data
-            fn new_getmicrodescs_state(
-                rt: impl Runtime,
-            ) -> (Arc<DirRcv>, GetMicrodescsState<DirRcv, impl Runtime>) {
-                let rcv = Arc::new(DirRcv::new());
+            fn new_getmicrodescs_state(rt: impl Runtime) -> GetMicrodescsState<impl Runtime> {
                 let rt = make_time_shifted_runtime(test_time(), rt);
                 let cfg = make_dirmgr_config(Some(test_authorities()));
                 let (signed, rest, consensus) = MdConsensus::parse(CONSENSUS2).unwrap();
@@ -1394,29 +1377,28 @@ mod test {
                     .dangerously_assume_timely()
                     .dangerously_assume_wellsigned();
                 let meta = ConsensusMeta::from_consensus(signed, rest, &consensus);
-                let state = GetMicrodescsState::new(
+                GetMicrodescsState::new(
                     CacheUsage::CacheOkay,
                     consensus,
                     meta,
-                    Arc::downgrade(&rcv),
                     rt,
                     cfg,
                     None,
-                );
-
-                (rcv, state)
+                    #[cfg(feature = "dirfilter")]
+                    Arc::new(NilFilter),
+                )
             }
             fn d64(s: &str) -> MdDigest {
                 base64::decode(s).unwrap().try_into().unwrap()
             }
 
             // If we start from scratch and reset, we're back in GetConsensus.
-            let (_rcv, state) = new_getmicrodescs_state(rt.clone());
+            let state = new_getmicrodescs_state(rt.clone());
             let state = Box::new(state).reset().unwrap();
             assert_eq!(&state.describe(), "Looking for a consensus.");
 
             // Check the basics.
-            let (_rcv, mut state) = new_getmicrodescs_state(rt.clone());
+            let mut state = new_getmicrodescs_state(rt.clone());
             assert_eq!(
                 &state.describe(),
                 "Downloading microdescriptors (we are missing 4)."