aboutsummaryrefslogtreecommitdiff
path: root/test/functional/p2p_leak.py
blob: f800e815d8d630a9533b2e8e4da3263ce929f495 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python3
# Copyright (c) 2017-2022 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 message sending before handshake completion.

Before receiving a VERACK, a node should not send anything but VERSION/VERACK
and feature negotiation messages (WTXIDRELAY, SENDADDRV2).

This test connects to a node and sends it a few messages, trying to entice it
into sending us something it shouldn't."""

import time

from test_framework.messages import (
    msg_getaddr,
    msg_ping,
    msg_version,
)
from test_framework.p2p import (
    P2PInterface,
    P2P_SUBVERSION,
    P2P_SERVICES,
    P2P_VERSION_RELAY,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
    assert_equal,
    assert_greater_than_or_equal,
)

PEER_TIMEOUT = 3


class LazyPeer(P2PInterface):
    def __init__(self):
        super().__init__()
        self.unexpected_msg = False
        self.ever_connected = False
        self.got_wtxidrelay = False
        self.got_sendaddrv2 = False

    def bad_message(self, message):
        self.unexpected_msg = True
        print("should not have received message: %s" % message.msgtype)

    def on_open(self):
        self.ever_connected = True

    # Does not respond to "version" with "verack"
    def on_version(self, message): self.bad_message(message)
    def on_verack(self, message): self.bad_message(message)
    def on_inv(self, message): self.bad_message(message)
    def on_addr(self, message): self.bad_message(message)
    def on_getdata(self, message): self.bad_message(message)
    def on_getblocks(self, message): self.bad_message(message)
    def on_tx(self, message): self.bad_message(message)
    def on_block(self, message): self.bad_message(message)
    def on_getaddr(self, message): self.bad_message(message)
    def on_headers(self, message): self.bad_message(message)
    def on_getheaders(self, message): self.bad_message(message)
    def on_ping(self, message): self.bad_message(message)
    def on_mempool(self, message): self.bad_message(message)
    def on_pong(self, message): self.bad_message(message)
    def on_feefilter(self, message): self.bad_message(message)
    def on_sendheaders(self, message): self.bad_message(message)
    def on_sendcmpct(self, message): self.bad_message(message)
    def on_cmpctblock(self, message): self.bad_message(message)
    def on_getblocktxn(self, message): self.bad_message(message)
    def on_blocktxn(self, message): self.bad_message(message)
    def on_wtxidrelay(self, message): self.got_wtxidrelay = True
    def on_sendaddrv2(self, message): self.got_sendaddrv2 = True


# Peer that sends a version but not a verack.
class NoVerackIdlePeer(LazyPeer):
    def __init__(self):
        self.version_received = False
        super().__init__()

    def on_verack(self, message): pass
    # When version is received, don't reply with a verack. Instead, see if the
    # node will give us a message that it shouldn't. This is not an exhaustive
    # list!
    def on_version(self, message):
        self.version_received = True
        self.send_message(msg_ping())
        self.send_message(msg_getaddr())


class P2PVersionStore(P2PInterface):
    version_received = None

    def on_version(self, msg):
        # Responds with an appropriate verack
        super().on_version(msg)
        self.version_received = msg


class P2PLeakTest(BitcoinTestFramework):
    def set_test_params(self):
        self.num_nodes = 1
        self.extra_args = [[f"-peertimeout={PEER_TIMEOUT}"]]

    def create_old_version(self, nversion):
        old_version_msg = msg_version()
        old_version_msg.nVersion = nversion
        old_version_msg.strSubVer = P2P_SUBVERSION
        old_version_msg.nServices = P2P_SERVICES
        old_version_msg.relay = P2P_VERSION_RELAY
        return old_version_msg

    def run_test(self):
        self.log.info('Check that the node doesn\'t send unexpected messages before handshake completion')
        # Peer that never sends a version, nor any other messages. It shouldn't receive anything from the node.
        no_version_idle_peer = self.nodes[0].add_p2p_connection(LazyPeer(), send_version=False, wait_for_verack=False)

        # Peer that sends a version but not a verack.
        no_verack_idle_peer = self.nodes[0].add_p2p_connection(NoVerackIdlePeer(), wait_for_verack=False)

        # Pre-wtxidRelay peer that sends a version but not a verack and does not support feature negotiation
        # messages which start at nVersion == 70016
        pre_wtxidrelay_peer = self.nodes[0].add_p2p_connection(NoVerackIdlePeer(), send_version=False, wait_for_verack=False)
        pre_wtxidrelay_peer.send_message(self.create_old_version(70015))

        # Wait until the peer gets the verack in response to the version. Though, don't wait for the node to receive the
        # verack, since the peer never sent one
        no_verack_idle_peer.wait_for_verack()
        pre_wtxidrelay_peer.wait_for_verack()

        no_version_idle_peer.wait_until(lambda: no_version_idle_peer.ever_connected)
        no_verack_idle_peer.wait_until(lambda: no_verack_idle_peer.version_received)
        pre_wtxidrelay_peer.wait_until(lambda: pre_wtxidrelay_peer.version_received)

        # Mine a block and make sure that it's not sent to the connected peers
        self.generate(self.nodes[0], nblocks=1)

        # Give the node enough time to possibly leak out a message
        time.sleep(PEER_TIMEOUT + 2)

        self.log.info("Connect peer to ensure the net thread runs the disconnect logic at least once")
        self.nodes[0].add_p2p_connection(P2PInterface())

        # Make sure only expected messages came in
        assert not no_version_idle_peer.unexpected_msg
        assert not no_version_idle_peer.got_wtxidrelay
        assert not no_version_idle_peer.got_sendaddrv2

        assert not no_verack_idle_peer.unexpected_msg
        assert no_verack_idle_peer.got_wtxidrelay
        assert no_verack_idle_peer.got_sendaddrv2

        assert not pre_wtxidrelay_peer.unexpected_msg
        assert not pre_wtxidrelay_peer.got_wtxidrelay
        assert not pre_wtxidrelay_peer.got_sendaddrv2

        # Expect peers to be disconnected due to timeout
        assert not no_version_idle_peer.is_connected
        assert not no_verack_idle_peer.is_connected
        assert not pre_wtxidrelay_peer.is_connected

        self.log.info('Check that the version message does not leak the local address of the node')
        p2p_version_store = self.nodes[0].add_p2p_connection(P2PVersionStore())
        ver = p2p_version_store.version_received
        # Check that received time is within one hour of now
        assert_greater_than_or_equal(ver.nTime, time.time() - 3600)
        assert_greater_than_or_equal(time.time() + 3600, ver.nTime)
        assert_equal(ver.addrFrom.port, 0)
        assert_equal(ver.addrFrom.ip, '0.0.0.0')
        assert_equal(ver.nStartingHeight, 201)
        assert_equal(ver.relay, 1)

        self.log.info('Check that old peers are disconnected')
        p2p_old_peer = self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False)
        with self.nodes[0].assert_debug_log(["using obsolete version 31799; disconnecting"]):
            p2p_old_peer.send_message(self.create_old_version(31799))
            p2p_old_peer.wait_for_disconnect()


if __name__ == '__main__':
    P2PLeakTest(__file__).main()