diff --git a/src/net_processing.cpp b/src/net_processing.cpp
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -67,6 +67,14 @@
 // SHA256("main address relay")[0:8]
 static const uint64_t RANDOMIZER_ID_ADDRESS_RELAY = 0x3cac0035b5866b90ULL;
 
+/// Age after which a stale block will no longer be served if requested as
+/// protection against fingerprinting. Set to one month, denominated in seconds.
+static const int STALE_RELAY_AGE_LIMIT = 30 * 24 * 60 * 60;
+
+/// Age after which a block is considered historical for purposes of rate
+/// limiting block relay. Set to one week, denominated in seconds.
+static const int HISTORICAL_BLOCK_AGE = 7 * 24 * 60 * 60;
+
 // Internal stuff
 namespace {
 /** Number of nodes with fSyncStarted. */
@@ -924,6 +932,20 @@
 // blockchain -> download logic notification
 //
 
+// To prevent fingerprinting attacks, only send blocks/headers outside of the
+// active chain if they are no more than a month older (both in time, and in
+// best equivalent proof of work) than the best header chain we know about.
+static bool StaleBlockRequestAllowed(const CBlockIndex *pindex,
+                                     const Consensus::Params &consensusParams) {
+    AssertLockHeld(cs_main);
+    return (pindexBestHeader != nullptr) &&
+           (pindexBestHeader->GetBlockTime() - pindex->GetBlockTime() <
+            STALE_RELAY_AGE_LIMIT) &&
+           (GetBlockProofEquivalentTime(*pindexBestHeader, *pindex,
+                                        *pindexBestHeader, consensusParams) <
+            STALE_RELAY_AGE_LIMIT);
+}
+
 PeerLogicValidation::PeerLogicValidation(CConnman *connmanIn,
                                          CScheduler &scheduler)
     : connman(connmanIn), m_stale_tip_check_time(0) {
@@ -1254,21 +1276,9 @@
                     if (chainActive.Contains(mi->second)) {
                         send = true;
                     } else {
-                        static const int nOneMonth = 30 * 24 * 60 * 60;
-                        // To prevent fingerprinting attacks, only send blocks
-                        // outside of the active chain if they are valid, and no
-                        // more than a month older (both in time, and in best
-                        // equivalent proof of work) than the best header chain
-                        // we know about.
                         send = mi->second->IsValid(BlockValidity::SCRIPTS) &&
-                               (pindexBestHeader != nullptr) &&
-                               (pindexBestHeader->GetBlockTime() -
-                                    mi->second->GetBlockTime() <
-                                nOneMonth) &&
-                               (GetBlockProofEquivalentTime(
-                                    *pindexBestHeader, *mi->second,
-                                    *pindexBestHeader,
-                                    consensusParams) < nOneMonth);
+                               StaleBlockRequestAllowed(mi->second,
+                                                        consensusParams);
                         if (!send) {
                             LogPrintf("%s: ignoring request from peer=%i for "
                                       "old block that isn't in the main "
@@ -1279,15 +1289,13 @@
                 }
 
                 // Disconnect node in case we have reached the outbound limit
-                // for serving historical blocks never disconnect whitelisted
-                // nodes.
-                // assume > 1 week = historical
-                static const int nOneWeek = 7 * 24 * 60 * 60;
+                // for serving historical blocks.
+                // Never disconnect whitelisted nodes.
                 if (send && connman->OutboundTargetReached(true) &&
                     (((pindexBestHeader != nullptr) &&
                       (pindexBestHeader->GetBlockTime() -
                            mi->second->GetBlockTime() >
-                       nOneWeek)) ||
+                       HISTORICAL_BLOCK_AGE)) ||
                      inv.type == MSG_FILTERED_BLOCK) &&
                     !pfrom->fWhitelisted) {
                     LogPrint(BCLog::NET, "historical block serving limit "
@@ -2343,6 +2351,14 @@
                 return true;
             }
             pindex = (*mi).second;
+
+            if (!chainActive.Contains(pindex) &&
+                !StaleBlockRequestAllowed(pindex, chainparams.GetConsensus())) {
+                LogPrintf("%s: ignoring request from peer=%i for old block "
+                          "header that isn't in the main chain\n",
+                          __func__, pfrom->GetId());
+                return true;
+            }
         } else {
             // Find the last block the caller has in the main chain
             pindex = FindForkInGlobalIndex(chainActive, locator);
diff --git a/test/functional/p2p-fingerprint.py b/test/functional/p2p-fingerprint.py
new file mode 100755
--- /dev/null
+++ b/test/functional/p2p-fingerprint.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# Copyright (c) 2017 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test various fingerprinting protections.
+
+If an stale block more than a month old or its header are requested by a peer,
+the node should pretend that it does not have it to avoid fingerprinting.
+"""
+
+import time
+
+from test_framework.blocktools import (create_block, create_coinbase)
+from test_framework.mininode import (
+    CInv,
+    NetworkThread,
+    NodeConn,
+    NodeConnCB,
+    msg_headers,
+    msg_block,
+    msg_getdata,
+    msg_getheaders,
+    wait_until,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+    assert_equal,
+    p2p_port,
+)
+
+
+class P2PFingerprintTest(BitcoinTestFramework):
+    def set_test_params(self):
+        self.setup_clean_chain = True
+        self.num_nodes = 1
+
+    # Build a chain of blocks on top of given one
+    def build_chain(self, nblocks, prev_hash, prev_height, prev_median_time):
+        blocks = []
+        for _ in range(nblocks):
+            coinbase = create_coinbase(prev_height + 1)
+            block_time = prev_median_time + 1
+            block = create_block(int(prev_hash, 16), coinbase, block_time)
+            block.solve()
+
+            blocks.append(block)
+            prev_hash = block.hash
+            prev_height += 1
+            prev_median_time = block_time
+        return blocks
+
+    # Send a getdata request for a given block hash
+    def send_block_request(self, block_hash, node):
+        msg = msg_getdata()
+        # 2 == "Block"
+        msg.inv.append(CInv(2, block_hash))
+        node.send_message(msg)
+
+    # Send a getheaders request for a given single block hash
+    def send_header_request(self, block_hash, node):
+        msg = msg_getheaders()
+        msg.hashstop = block_hash
+        node.send_message(msg)
+
+    # Check whether last block received from node has a given hash
+    def last_block_equals(self, expected_hash, node):
+        block_msg = node.last_message.get("block")
+        return block_msg and block_msg.block.rehash() == expected_hash
+
+    # Check whether last block header received from node has a given hash
+    def last_header_equals(self, expected_hash, node):
+        headers_msg = node.last_message.get("headers")
+        return (headers_msg and
+                headers_msg.headers and
+                headers_msg.headers[0].rehash() == expected_hash)
+
+    # Checks that stale blocks timestamped more than a month ago are not served
+    # by the node while recent stale blocks and old active chain blocks are.
+    # This does not currently test that stale blocks timestamped within the
+    # last month but that have over a month's worth of work are also withheld.
+    def run_test(self):
+        node0 = NodeConnCB()
+
+        connections = []
+        connections.append(
+            NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0))
+        node0.add_connection(connections[0])
+
+        NetworkThread().start()
+        node0.wait_for_verack()
+
+        # Set node time to 60 days ago
+        self.nodes[0].setmocktime(int(time.time()) - 60 * 24 * 60 * 60)
+
+        # Generating a chain of 10 blocks
+        block_hashes = self.nodes[0].generate(nblocks=10)
+
+        # Create longer chain starting 2 blocks before current tip
+        height = len(block_hashes) - 2
+        block_hash = block_hashes[height - 1]
+        block_time = self.nodes[0].getblockheader(block_hash)["mediantime"] + 1
+        new_blocks = self.build_chain(5, block_hash, height, block_time)
+
+        # Force reorg to a longer chain
+        node0.send_message(msg_headers(new_blocks))
+        node0.wait_for_getdata()
+        for block in new_blocks:
+            node0.send_and_ping(msg_block(block))
+
+        # Check that reorg succeeded
+        assert_equal(self.nodes[0].getblockcount(), 13)
+
+        stale_hash = int(block_hashes[-1], 16)
+
+        # Check that getdata request for stale block succeeds
+        self.send_block_request(stale_hash, node0)
+
+        def test_function(): return self.last_block_equals(stale_hash, node0)
+        wait_until(test_function, timeout=3)
+
+        # Check that getheader request for stale block header succeeds
+        self.send_header_request(stale_hash, node0)
+
+        def test_function(): return self.last_header_equals(stale_hash, node0)
+        wait_until(test_function, timeout=3)
+
+        # Longest chain is extended so stale is much older than chain tip
+        self.nodes[0].setmocktime(0)
+        tip = self.nodes[0].generate(nblocks=1)[0]
+        assert_equal(self.nodes[0].getblockcount(), 14)
+
+        # Send getdata & getheaders to refresh last received getheader message
+        block_hash = int(tip, 16)
+        self.send_block_request(block_hash, node0)
+        self.send_header_request(block_hash, node0)
+        node0.sync_with_ping()
+
+        # Request for very old stale block should now fail
+        self.send_block_request(stale_hash, node0)
+        time.sleep(3)
+        assert not self.last_block_equals(stale_hash, node0)
+
+        # Request for very old stale block header should now fail
+        self.send_header_request(stale_hash, node0)
+        time.sleep(3)
+        assert not self.last_header_equals(stale_hash, node0)
+
+        # Verify we can fetch very old blocks and headers on the active chain
+        block_hash = int(block_hashes[2], 16)
+        self.send_block_request(block_hash, node0)
+        self.send_header_request(block_hash, node0)
+        node0.sync_with_ping()
+
+        self.send_block_request(block_hash, node0)
+
+        def test_function(): return self.last_block_equals(block_hash, node0)
+        wait_until(test_function, timeout=3)
+
+        self.send_header_request(block_hash, node0)
+
+        def test_function(): return self.last_header_equals(block_hash, node0)
+        wait_until(test_function, timeout=3)
+
+
+if __name__ == '__main__':
+    P2PFingerprintTest().main()
diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py
--- a/test/functional/test_framework/mininode.py
+++ b/test/functional/test_framework/mininode.py
@@ -1082,8 +1082,8 @@
 class msg_headers():
     command = b"headers"
 
-    def __init__(self):
-        self.headers = []
+    def __init__(self, headers=None):
+        self.headers = headers if headers is not None else []
 
     def deserialize(self, f):
         # comment in bitcoind indicates these should be deserialized as blocks
diff --git a/test/functional/timing.json b/test/functional/timing.json
--- a/test/functional/timing.json
+++ b/test/functional/timing.json
@@ -1,51 +1,51 @@
 [
  {
   "name": "abandonconflict.py",
-  "time": 15
+  "time": 11
  },
  {
   "name": "abc-checkdatasig-activation.py",
-  "time": 22
+  "time": 3
  },
  {
   "name": "abc-cmdline.py",
-  "time": 10
+  "time": 7
  },
  {
   "name": "abc-finalize-block.py",
-  "time": 7
+  "time": 5
  },
  {
   "name": "abc-high_priority_transaction.py",
-  "time": 20
+  "time": 6
  },
  {
   "name": "abc-magnetic-anomaly-activation.py",
-  "time": 6
+  "time": 5
  },
  {
   "name": "abc-magnetic-anomaly-mining.py",
-  "time": 29
+  "time": 3
  },
  {
   "name": "abc-mempool-accept-txn.py",
-  "time": 13
+  "time": 3
  },
  {
   "name": "abc-p2p-compactblocks.py",
-  "time": 411
+  "time": 253
  },
  {
   "name": "abc-p2p-fullblocktest.py",
-  "time": 79
+  "time": 31
  },
  {
   "name": "abc-parkedchain.py",
-  "time": 13
+  "time": 4
  },
  {
   "name": "abc-replay-protection.py",
-  "time": 4
+  "time": 3
  },
  {
   "name": "abc-rpc.py",
@@ -53,47 +53,47 @@
  },
  {
   "name": "abc-sync-chain.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "abc-transaction-ordering.py",
-  "time": 6
+  "time": 5
  },
  {
   "name": "assumevalid.py",
-  "time": 12
+  "time": 8
  },
  {
   "name": "bip65-cltv-p2p.py",
-  "time": 5
+  "time": 4
  },
  {
   "name": "bip68-112-113-p2p.py",
-  "time": 39
+  "time": 14
  },
  {
   "name": "bip68-sequence.py",
-  "time": 64
+  "time": 12
  },
  {
   "name": "bipdersig-p2p.py",
-  "time": 5
+  "time": 3
  },
  {
   "name": "bitcoin_cli.py",
-  "time": 13
+  "time": 2
  },
  {
   "name": "blockchain.py",
-  "time": 40
+  "time": 10
  },
  {
   "name": "dbcrash.py",
-  "time": 1320
+  "time": 537
  },
  {
   "name": "decodescript.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "disablewallet.py",
@@ -101,47 +101,47 @@
  },
  {
   "name": "disconnect_ban.py",
-  "time": 20
+  "time": 7
  },
  {
   "name": "example_test.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "fundrawtransaction.py",
-  "time": 47
+  "time": 31
  },
  {
   "name": "getblocktemplate_longpoll.py",
-  "time": 68
+  "time": 67
  },
  {
   "name": "getchaintips.py",
-  "time": 28
+  "time": 3
  },
  {
   "name": "httpbasics.py",
-  "time": 4
+  "time": 2
  },
  {
   "name": "import-rescan.py",
-  "time": 16
+  "time": 3
  },
  {
   "name": "importmulti.py",
-  "time": 26
+  "time": 5
  },
  {
   "name": "importprunedfunds.py",
-  "time": 4
+  "time": 2
  },
  {
   "name": "invalidateblock.py",
-  "time": 8
+  "time": 7
  },
  {
   "name": "invalidblockrequest.py",
-  "time": 4
+  "time": 3
  },
  {
   "name": "invalidtxrequest.py",
@@ -149,15 +149,15 @@
  },
  {
   "name": "keypool-topup.py",
-  "time": 32
+  "time": 8
  },
  {
   "name": "keypool.py",
-  "time": 37
+  "time": 6
  },
  {
   "name": "listsinceblock.py",
-  "time": 5
+  "time": 3
  },
  {
   "name": "listtransactions.py",
@@ -165,135 +165,139 @@
  },
  {
   "name": "maxuploadtarget.py",
-  "time": 26
+  "time": 21
  },
  {
   "name": "mempool_limit.py",
-  "time": 10
+  "time": 4
  },
  {
   "name": "mempool_packages.py",
-  "time": 26
+  "time": 9
  },
  {
   "name": "mempool_persist.py",
-  "time": 46
+  "time": 17
  },
  {
   "name": "mempool_reorg.py",
-  "time": 8
+  "time": 6
  },
  {
   "name": "mempool_resurrect_test.py",
-  "time": 4
+  "time": 2
  },
  {
   "name": "mempool_spendcoinbase.py",
-  "time": 4
+  "time": 2
  },
  {
   "name": "merkle_blocks.py",
-  "time": 4
+  "time": 3
  },
  {
   "name": "minchainwork.py",
-  "time": 16
+  "time": 5
  },
  {
   "name": "mining.py",
-  "time": 4
+  "time": 2
  },
  {
   "name": "multi_rpc.py",
-  "time": 10
+  "time": 4
  },
  {
   "name": "multiwallet.py",
-  "time": 8
+  "time": 5
  },
  {
   "name": "net.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "notifications.py",
-  "time": 20
+  "time": 5
  },
  {
   "name": "nulldummy.py",
-  "time": 5
+  "time": 2
  },
  {
   "name": "p2p-acceptblock.py",
-  "time": 5
+  "time": 4
  },
  {
   "name": "p2p-compactblocks.py",
-  "time": 30
+  "time": 12
  },
  {
   "name": "p2p-feefilter.py",
-  "time": 19
+  "time": 23
+ },
+ {
+  "name": "p2p-fingerprint.py",
+  "time": 8
  },
  {
   "name": "p2p-fullblocktest.py",
-  "time": 133
+  "time": 97
  },
  {
   "name": "p2p-leaktests.py",
-  "time": 8
+  "time": 7
  },
  {
   "name": "p2p-mempool.py",
-  "time": 12
+  "time": 2
  },
  {
   "name": "p2p-timeouts.py",
-  "time": 65
+  "time": 64
  },
  {
   "name": "preciousblock.py",
-  "time": 10
+  "time": 3
  },
  {
   "name": "prioritise_transaction.py",
-  "time": 9
+  "time": 4
  },
  {
   "name": "proxy_test.py",
-  "time": 7
+  "time": 3
  },
  {
   "name": "pruning.py",
-  "time": 1527
+  "time": 502
  },
  {
   "name": "rawtransactions.py",
-  "time": 38
+  "time": 16
  },
  {
   "name": "reindex.py",
-  "time": 27
+  "time": 12
  },
  {
   "name": "resendwallettransactions.py",
-  "time": 14
+  "time": 5
  },
  {
   "name": "rest.py",
-  "time": 32
+  "time": 6
  },
  {
   "name": "rpcbind_test.py",
-  "time": 27
+  "time": 24
  },
  {
   "name": "rpcnamedargs.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "sendheaders.py",
-  "time": 42
+  "time": 13
  },
  {
   "name": "signmessages.py",
@@ -305,58 +309,58 @@
  },
  {
   "name": "txn_clone.py",
-  "time": 5
+  "time": 3
  },
  {
   "name": "txn_clone.py --mineblock",
-  "time": 8
+  "time": 3
  },
  {
   "name": "txn_doublespend.py",
-  "time": 24
+  "time": 3
  },
  {
   "name": "txn_doublespend.py --mineblock",
-  "time": 16
+  "time": 3
  },
  {
   "name": "uptime.py",
-  "time": 3
+  "time": 2
  },
  {
   "name": "wallet.py",
-  "time": 75
+  "time": 31
  },
  {
   "name": "wallet_accounts.py",
-  "time": 26
+  "time": 2
  },
  {
   "name": "wallet_dump.py",
-  "time": 12
+  "time": 5
  },
  {
   "name": "wallet_encryption.py",
-  "time": 27
+  "time": 7
  },
  {
   "name": "wallet_hd.py",
-  "time": 142
+  "time": 17
  },
  {
   "name": "wallet_receivedby.py",
-  "time": 21
+  "time": 12
  },
  {
   "name": "walletbackup.py",
-  "time": 130
+  "time": 100
  },
  {
   "name": "zapwallettxes.py",
-  "time": 26
+  "time": 10
  },
  {
   "name": "zmq_test.py",
-  "time": 7
+  "time": 11
  }
 ]
\ No newline at end of file