diff --git a/src/cryptonote_protocol/levin_notify.cpp b/src/cryptonote_protocol/levin_notify.cpp
index 26cd93b5a..4b41b5bfc 100644
--- a/src/cryptonote_protocol/levin_notify.cpp
+++ b/src/cryptonote_protocol/levin_notify.cpp
@@ -187,14 +187,15 @@ namespace levin
   {
     struct zone
     {
-      explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in)
+      explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in, bool is_public)
         : p2p(std::move(p2p)),
           noise(std::move(noise_in)),
           next_epoch(io_service),
           strand(io_service),
           map(),
           channels(),
-          connection_count(0)
+          connection_count(0),
+          is_public(is_public)
       {
         for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count)
           channels.emplace_back(io_service);
@@ -207,6 +208,7 @@ namespace levin
       net::dandelionpp::connection_map map;//!< Tracks outgoing uuid's for noise channels or Dandelion++ stems
       std::deque<noise_channel> channels;  //!< Never touch after init; only update elements on `noise_channel.strand`
       std::atomic<std::size_t> connection_count; //!< Only update in strand, can be read at any time
+      const bool is_public;                      //!< Zone is public ipv4/ipv6 connections
     };
   } // detail
 
@@ -276,7 +278,10 @@ namespace levin
         std::vector<boost::uuids::uuid> connections;
         connections.reserve(connection_id_reserve_size);
         zone_->p2p->foreach_connection([this, &connections] (detail::p2p_context& context) {
-          if (this->source_ != context.m_connection_id)
+          /* Only send to outgoing connections when "flooding" over i2p/tor.
+             Otherwise this makes the tx linkable to a hidden service address,
+             making things linkable across connections. */
+          if (this->source_ != context.m_connection_id && (this->zone_->is_public || !context.m_is_income))
             connections.emplace_back(context.m_connection_id);
           return true;
         });
@@ -476,8 +481,8 @@ namespace levin
     };
   } // anonymous
 
-  notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise)
-    : zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise)))
+  notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, bool is_public)
+    : zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise), is_public))
   {
     if (!zone_->p2p)
       throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"};
@@ -528,7 +533,7 @@ namespace levin
       channel.next_noise.cancel();
   }
 
-  bool notify::send_txs(std::vector<cryptonote::blobdata> txs, const boost::uuids::uuid& source, const bool pad_txs)
+  bool notify::send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, const bool pad_txs)
   {
     if (!zone_)
       return false;
diff --git a/src/cryptonote_protocol/levin_notify.h b/src/cryptonote_protocol/levin_notify.h
index 82d22680a..484243af5 100644
--- a/src/cryptonote_protocol/levin_notify.h
+++ b/src/cryptonote_protocol/levin_notify.h
@@ -86,7 +86,7 @@ namespace levin
     {}
 
     //! Construct an instance with available notification `zones`.
-    explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise);
+    explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise, bool is_public);
 
     notify(const notify&) = delete;
     notify(notify&&) = default;
diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl
index 24c87cef8..c3dca3f6c 100644
--- a/src/p2p/net_node.inl
+++ b/src/p2p/net_node.inl
@@ -384,7 +384,7 @@ namespace nodetool
     m_use_ipv6 = command_line::get_arg(vm, arg_p2p_use_ipv6);
     m_require_ipv4 = command_line::get_arg(vm, arg_p2p_require_ipv4);
     public_zone.m_notifier = cryptonote::levin::notify{
-      public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr
+      public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr, true
     };
 
     if (command_line::has_arg(vm, arg_p2p_add_peer))
@@ -495,7 +495,7 @@ namespace nodetool
       }
 
       zone.m_notifier = cryptonote::levin::notify{
-        zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise)
+        zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise), false
       };
     }
 
diff --git a/tests/unit_tests/levin.cpp b/tests/unit_tests/levin.cpp
index 3188167f9..e5ca4e41e 100644
--- a/tests/unit_tests/levin.cpp
+++ b/tests/unit_tests/levin.cpp
@@ -38,6 +38,7 @@
 #include "byte_slice.h"
 #include "crypto/crypto.h"
 #include "cryptonote_basic/connection_context.h"
+#include "cryptonote_core/cryptonote_core.h"
 #include "cryptonote_protocol/cryptonote_protocol_defs.h"
 #include "cryptonote_protocol/levin_notify.h"
 #include "int-util.h"
@@ -119,12 +120,13 @@ namespace
         epee::levin::async_protocol_handler<cryptonote::levin::detail::p2p_context> handler_;
 
     public:
-        test_connection(boost::asio::io_service& io_service, cryptonote::levin::connections& connections, boost::uuids::random_generator& random_generator)
-          : context_(),
-            endpoint_(io_service),
+        test_connection(boost::asio::io_service& io_service, cryptonote::levin::connections& connections, boost::uuids::random_generator& random_generator, const bool is_incoming)
+          : endpoint_(io_service),
+            context_(),
             handler_(std::addressof(endpoint_), connections, context_)
         {
-            const_cast<boost::uuids::uuid&>(context_.m_connection_id) = random_generator();
+            using base_type = epee::net_utils::connection_context_base;
+            static_cast<base_type&>(context_) = base_type{random_generator(), {}, is_incoming, false};
             handler_.after_init_connection();
         }
 
@@ -262,19 +264,19 @@ namespace
             EXPECT_EQ(0u, receiver_.notified_size());
         }
 
-        void add_connection()
+        void add_connection(const bool is_incoming)
         {
-            contexts_.emplace_back(io_service_, *connections_, random_generator_);
+            contexts_.emplace_back(io_service_, *connections_, random_generator_, is_incoming);
             EXPECT_TRUE(connection_ids_.emplace(contexts_.back().get_id()).second);
             EXPECT_EQ(connection_ids_.size(), connections_->get_connections_count());
         }
 
-        cryptonote::levin::notify make_notifier(const std::size_t noise_size)
+        cryptonote::levin::notify make_notifier(const std::size_t noise_size, bool is_public)
         {
             epee::byte_slice noise = nullptr;
             if (noise_size)
                 noise = epee::levin::make_noise_notify(noise_size);
-            return cryptonote::levin::notify{io_service_, connections_, std::move(noise)};
+            return cryptonote::levin::notify{io_service_, connections_, std::move(noise), is_public};
         }
 
         boost::uuids::random_generator random_generator_;
@@ -437,10 +439,10 @@ TEST_F(levin_notify, defaulted)
 
 TEST_F(levin_notify, flood)
 {
-    cryptonote::levin::notify notifier = make_notifier(0);
+    cryptonote::levin::notify notifier = make_notifier(0, true);
 
     for (unsigned count = 0; count < 10; ++count)
-        add_connection();
+        add_connection(count % 2 == 0);
 
     {
         const auto status = notifier.get_status();
@@ -500,16 +502,87 @@ TEST_F(levin_notify, flood)
     }
 }
 
+TEST_F(levin_notify, private_flood)
+{
+    cryptonote::levin::notify notifier = make_notifier(0, false);
+
+    for (unsigned count = 0; count < 10; ++count)
+        add_connection(count % 2 == 0);
+
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled);
+    }
+    notifier.new_out_connection();
+    io_service_.poll();
+    {
+        const auto status = notifier.get_status();
+        EXPECT_FALSE(status.has_noise);
+        EXPECT_FALSE(status.connections_filled); // not tracked
+    }
+
+    std::vector<cryptonote::blobdata> txs(2);
+    txs[0].resize(100, 'e');
+    txs[1].resize(200, 'f');
+
+    ASSERT_EQ(10u, contexts_.size());
+    {
+        auto context = contexts_.begin();
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), false));
+
+        io_service_.reset();
+        ASSERT_LT(0u, io_service_.poll());
+        EXPECT_EQ(0u, context->process_send_queue());
+        for (++context; context != contexts_.end(); ++context)
+        {
+            const bool is_incoming = ((context - contexts_.begin()) % 2 == 0);
+            EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue());
+        }
+
+        ASSERT_EQ(5u, receiver_.notified_size());
+        for (unsigned count = 0; count < 5; ++count)
+        {
+            auto notification = receiver_.get_notification<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
+            EXPECT_EQ(txs, notification.txs);
+            EXPECT_TRUE(notification._.empty());
+        }
+    }
+
+    ASSERT_EQ(10u, contexts_.size());
+    {
+        auto context = contexts_.begin();
+        EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), true));
+
+        io_service_.reset();
+        ASSERT_LT(0u, io_service_.poll());
+        EXPECT_EQ(0u, context->process_send_queue());
+        for (++context; context != contexts_.end(); ++context)
+        {
+            const bool is_incoming = ((context - contexts_.begin()) % 2 == 0);
+            EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue());
+        }
+
+        ASSERT_EQ(5u, receiver_.notified_size());
+        for (unsigned count = 0; count < 5; ++count)
+        {
+            auto notification = receiver_.get_notification<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
+            EXPECT_EQ(txs, notification.txs);
+            EXPECT_FALSE(notification._.empty());
+        }
+    }
+}
+
 TEST_F(levin_notify, noise)
 {
     for (unsigned count = 0; count < 10; ++count)
-        add_connection();
+        add_connection(count % 2 == 0);
 
     std::vector<cryptonote::blobdata> txs(1);
     txs[0].resize(1900, 'h');
 
     const boost::uuids::uuid incoming_id = random_generator_();
-    cryptonote::levin::notify notifier = make_notifier(2048);
+    cryptonote::levin::notify notifier = make_notifier(2048, false);
 
     {
         const auto status = notifier.get_status();