Skip to content

Commit 6ac8f9f

Browse files
authored
scenarios: add recon scenario (#577)
* scenarios: add recon scenario * scenario: support signet in recon * scenarios: support signet in recon and test * lint
1 parent 20d514c commit 6ac8f9f

File tree

3 files changed

+116
-7
lines changed

3 files changed

+116
-7
lines changed

resources/scenarios/reconnaissance.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
3+
import socket
4+
5+
# The base class exists inside the commander container when deployed,
6+
# but requires a relative path inside the python source code for other functions.
7+
try:
8+
from commander import Commander
9+
except ImportError:
10+
from resources.scenarios.commander import Commander
11+
12+
# The entire Bitcoin Core test_framework directory is available as a library
13+
from test_framework.messages import MSG_TX, CInv, hash256, msg_getdata
14+
from test_framework.p2p import MAGIC_BYTES, P2PInterface
15+
16+
17+
# This message is provided to the user when they describe the scenario
18+
def cli_help():
19+
return "Demonstrate network reconnaissance using a scenario and P2PInterface"
20+
21+
22+
def get_signet_network_magic_from_node(node):
23+
template = node.getblocktemplate({"rules": ["segwit", "signet"]})
24+
challenge = template["signet_challenge"]
25+
challenge_bytes = bytes.fromhex(challenge)
26+
data = len(challenge_bytes).to_bytes() + challenge_bytes
27+
digest = hash256(data)
28+
return digest[0:4]
29+
30+
31+
# The actual scenario is a class like a Bitcoin Core functional test.
32+
# Commander is a subclass of BitcoinTestFramework instide Warnet
33+
# that allows to operate on containerized nodes instead of local nodes.
34+
class Reconnaissance(Commander):
35+
def set_test_params(self):
36+
# This setting is ignored but still required as
37+
# a sub-class of BitcoinTestFramework
38+
self.num_nodes = 1
39+
40+
# Scenario entrypoint
41+
def run_test(self):
42+
self.log.info("Getting peer info")
43+
44+
# Just like a typical Bitcoin Core functional test, this executes an
45+
# RPC on a node in the network. The actual node at self.nodes[0] may
46+
# be different depending on the user deploying the scenario. Users in
47+
# Warnet may have different namepsace access but everyone should always
48+
# have access to at least one node.
49+
peerinfo = self.nodes[0].getpeerinfo()
50+
for peer in peerinfo:
51+
# You can print out the the scenario logs with `warnet logs`
52+
# which have a list of all this node's peers' addresses and version
53+
self.log.info(f"{peer['addr']} {peer['subver']}")
54+
55+
# We pick a node on the network to attack
56+
victim = peerinfo[0]
57+
58+
# regtest or signet
59+
chain = self.nodes[0].chain
60+
61+
# The victim's address could be an explicit IP address
62+
# OR a kubernetes hostname (use default chain p2p port)
63+
if ":" in victim["addr"]:
64+
dstaddr = victim["addr"].split(":")[0]
65+
else:
66+
dstaddr = socket.gethostbyname(victim["addr"])
67+
if chain == "regtest":
68+
dstport = 18444
69+
if chain == "signet":
70+
dstport = 38333
71+
MAGIC_BYTES["signet"] = get_signet_network_magic_from_node(self.nodes[0])
72+
73+
# Now we will use a python-based Bitcoin p2p node to send very specific,
74+
# unusual or non-standard messages to a "victim" node.
75+
self.log.info(f"Attacking {dstaddr}:{dstport}")
76+
attacker = P2PInterface()
77+
attacker.peer_connect(dstaddr=dstaddr, dstport=dstport, net=chain, timeout_factor=1)()
78+
attacker.wait_until(lambda: attacker.is_connected, check_connected=False)
79+
80+
# Send a harmless network message we expect a response to and wait for it
81+
# Ask for TX with a 0 hash
82+
msg = msg_getdata()
83+
msg.inv.append(CInv(t=MSG_TX, h=0))
84+
attacker.send_and_ping(msg)
85+
attacker.wait_until(lambda: attacker.message_count["notfound"] > 0)
86+
self.log.info(f"Got notfound message from {dstaddr}:{dstport}")
87+
88+
89+
if __name__ == "__main__":
90+
Reconnaissance().main()

test/scenarios_test.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def setup_network(self):
3131
def test_scenarios(self):
3232
self.run_and_check_miner_scenario_from_file()
3333
self.run_and_check_scenario_from_file()
34+
self.check_regtest_recon()
3435

3536
def scenario_running(self, scenario_name: str):
3637
"""Check that we are only running a single scenario of the correct name"""
@@ -40,15 +41,9 @@ def scenario_running(self, scenario_name: str):
4041

4142
def run_and_check_scenario_from_file(self):
4243
scenario_file = "test/data/scenario_p2p_interface.py"
43-
44-
def check_scenario_clean_exit():
45-
active = scenarios_active()
46-
assert len(active) == 1
47-
return active[0]["status"] == "succeeded"
48-
4944
self.log.info(f"Running scenario from: {scenario_file}")
5045
self.warnet(f"run {scenario_file}")
51-
self.wait_for_predicate(lambda: check_scenario_clean_exit())
46+
self.wait_for_predicate(self.check_scenario_clean_exit)
5247

5348
def run_and_check_miner_scenario_from_file(self):
5449
scenario_file = "resources/scenarios/miner_std.py"
@@ -59,6 +54,16 @@ def run_and_check_miner_scenario_from_file(self):
5954
self.wait_for_predicate(lambda: self.check_blocks(2, start=start))
6055
self.stop_scenario()
6156

57+
def check_regtest_recon(self):
58+
scenario_file = "resources/scenarios/reconnaissance.py"
59+
self.log.info(f"Running scenario from file: {scenario_file}")
60+
self.warnet(f"run {scenario_file}")
61+
self.wait_for_predicate(self.check_scenario_clean_exit)
62+
63+
def check_scenario_clean_exit(self):
64+
active = scenarios_active()
65+
return all(scenario["status"] == "succeeded" for scenario in active)
66+
6267
def check_blocks(self, target_blocks, start: int = 0):
6368
count = int(self.warnet("bitcoin rpc tank-0000 getblockcount"))
6469
self.log.debug(f"Current block count: {count}, target: {start + target_blocks}")

test/signet_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from test_base import TestBase
88

9+
from warnet.status import _get_active_scenarios as scenarios_active
10+
911

1012
class SignetTest(TestBase):
1113
def __init__(self):
@@ -19,6 +21,7 @@ def run_test(self):
1921
try:
2022
self.setup_network()
2123
self.check_signet_miner()
24+
self.check_signet_recon()
2225
finally:
2326
self.cleanup()
2427

@@ -46,6 +49,17 @@ def block_one():
4649

4750
self.wait_for_predicate(block_one)
4851

52+
def check_signet_recon(self):
53+
scenario_file = "resources/scenarios/reconnaissance.py"
54+
self.log.info(f"Running scenario from file: {scenario_file}")
55+
self.warnet(f"run {scenario_file}")
56+
57+
def check_scenario_clean_exit():
58+
active = scenarios_active()
59+
return all(scenario["status"] == "succeeded" for scenario in active)
60+
61+
self.wait_for_predicate(check_scenario_clean_exit)
62+
4963

5064
if __name__ == "__main__":
5165
test = SignetTest()

0 commit comments

Comments
 (0)