Skip to content

Commit 87d1c04

Browse files
committed
feat: add peer-ids to blacklist using sysctl
1 parent 42aa09a commit 87d1c04

File tree

4 files changed

+421
-4
lines changed

4 files changed

+421
-4
lines changed

hathor/p2p/netfilter/utils.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,84 @@
1717
from hathor.p2p.netfilter.rule import NetfilterRule
1818
from hathor.p2p.netfilter.targets import NetfilterReject
1919

20+
# Global mapping to track peer_id -> rule UUID for blacklist management
21+
_peer_id_to_rule_uuid: dict[str, str] = {}
2022

21-
def add_peer_id_blacklist(peer_id_blacklist: list[str]) -> None:
22-
""" Add a list of peer ids to a blacklist using netfilter reject
23+
24+
def add_blacklist_peers(peer_ids: str | list[str]) -> list[str]:
25+
"""Add peer(s) to the blacklist.
26+
27+
Args:
28+
peer_ids: A single peer_id string or a list of peer_id strings
29+
30+
Returns:
31+
List of peer_ids that were successfully added (not already blacklisted)
2332
"""
33+
if isinstance(peer_ids, str):
34+
peer_ids = [peer_ids]
35+
2436
post_peerid = get_table('filter').get_chain('post_peerid')
37+
added_peers: list[str] = []
2538

26-
for peer_id in peer_id_blacklist:
39+
for peer_id in peer_ids:
2740
if not peer_id:
2841
continue
42+
43+
# Skip if already blacklisted
44+
if peer_id in _peer_id_to_rule_uuid:
45+
continue
46+
2947
match = NetfilterMatchPeerId(peer_id)
3048
rule = NetfilterRule(match, NetfilterReject())
3149
post_peerid.add_rule(rule)
50+
_peer_id_to_rule_uuid[peer_id] = rule.uuid
51+
added_peers.append(peer_id)
52+
53+
return added_peers
54+
55+
56+
def remove_blacklist_peers(peer_ids: str | list[str]) -> list[str]:
57+
"""Remove peer(s) from the blacklist.
58+
59+
Args:
60+
peer_ids: A single peer_id string or a list of peer_id strings
61+
62+
Returns:
63+
List of peer_ids that were successfully removed
64+
"""
65+
if isinstance(peer_ids, str):
66+
peer_ids = [peer_ids]
67+
68+
post_peerid = get_table('filter').get_chain('post_peerid')
69+
removed_peers: list[str] = []
70+
71+
for peer_id in peer_ids:
72+
if not peer_id:
73+
continue
74+
75+
rule_uuid = _peer_id_to_rule_uuid.get(peer_id)
76+
if rule_uuid is None:
77+
continue
78+
79+
if post_peerid.delete_rule(rule_uuid):
80+
del _peer_id_to_rule_uuid[peer_id]
81+
removed_peers.append(peer_id)
82+
83+
return removed_peers
84+
85+
86+
def list_blacklist_peers() -> list[str]:
87+
"""List all currently blacklisted peer_ids.
88+
89+
Returns:
90+
List of blacklisted peer_id strings
91+
"""
92+
return list(_peer_id_to_rule_uuid.keys())
93+
94+
95+
def add_peer_id_blacklist(peer_id_blacklist: list[str]) -> None:
96+
"""Add a list of peer ids to a blacklist using netfilter reject.
97+
98+
This is a legacy function that wraps add_blacklist_peers for backward compatibility.
99+
"""
100+
add_blacklist_peers(peer_id_blacklist)

hathor/sysctl/p2p/manager.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616

1717
from hathor.p2p.manager import ConnectionsManager
18+
from hathor.p2p.netfilter.utils import add_blacklist_peers, list_blacklist_peers, remove_blacklist_peers
1819
from hathor.p2p.peer_id import PeerId
1920
from hathor.p2p.sync_version import SyncVersion
2021
from hathor.p2p.utils import discover_hostname
@@ -122,6 +123,21 @@ def __init__(self, connections: ConnectionsManager) -> None:
122123
None,
123124
self.reload_entrypoints_and_connections,
124125
)
126+
self.register(
127+
'blacklist.add_peers',
128+
None,
129+
self.set_blacklist_add_peers,
130+
)
131+
self.register(
132+
'blacklist.remove_peers',
133+
None,
134+
self.set_blacklist_remove_peers,
135+
)
136+
self.register(
137+
'blacklist.list_peers',
138+
self.get_blacklist_list_peers,
139+
None,
140+
)
125141

126142
def set_force_sync_rotate(self) -> None:
127143
"""Force a sync rotate."""
@@ -269,3 +285,43 @@ def refresh_auto_hostname(self) -> None:
269285
def reload_entrypoints_and_connections(self) -> None:
270286
"""Kill all connections and reload entrypoints from the peer config file."""
271287
self.connections.reload_entrypoints_and_connections()
288+
289+
@signal_handler_safe
290+
def set_blacklist_add_peers(self, peer_ids: str | list[str]) -> None:
291+
"""Add peer(s) to the blacklist. Accepts a single peer-id string or a list of peer-ids."""
292+
# Validate peer IDs
293+
peer_id_list = [peer_ids] if isinstance(peer_ids, str) else peer_ids
294+
try:
295+
for peer_id in peer_id_list:
296+
if peer_id: # Skip empty strings
297+
PeerId(peer_id) # Validate format
298+
except ValueError as e:
299+
raise SysctlException(f'Invalid peer-id format: {e}')
300+
301+
added_peers = add_blacklist_peers(peer_ids)
302+
if added_peers:
303+
self.log.info('Added peers to blacklist', peer_ids=added_peers)
304+
else:
305+
self.log.info('No new peers added to blacklist (already blacklisted or empty)')
306+
307+
@signal_handler_safe
308+
def set_blacklist_remove_peers(self, peer_ids: str | list[str]) -> None:
309+
"""Remove peer(s) from the blacklist. Accepts a single peer-id string or a list of peer-ids."""
310+
# Validate peer IDs
311+
peer_id_list = [peer_ids] if isinstance(peer_ids, str) else peer_ids
312+
try:
313+
for peer_id in peer_id_list:
314+
if peer_id: # Skip empty strings
315+
PeerId(peer_id) # Validate format
316+
except ValueError as e:
317+
raise SysctlException(f'Invalid peer-id format: {e}')
318+
319+
removed_peers = remove_blacklist_peers(peer_ids)
320+
if removed_peers:
321+
self.log.info('Removed peers from blacklist', peer_ids=removed_peers)
322+
else:
323+
self.log.info('No peers removed from blacklist (not found or empty)')
324+
325+
def get_blacklist_list_peers(self) -> list[str]:
326+
"""List all currently blacklisted peer_ids."""
327+
return list_blacklist_peers()

hathor_tests/p2p/netfilter/test_utils.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
from hathor.p2p.netfilter import get_table
2-
from hathor.p2p.netfilter.utils import add_peer_id_blacklist
2+
from hathor.p2p.netfilter.utils import (
3+
add_blacklist_peers,
4+
add_peer_id_blacklist,
5+
list_blacklist_peers,
6+
remove_blacklist_peers,
7+
)
38
from hathor_tests import unittest
49

510

611
class NetfilterUtilsTest(unittest.TestCase):
12+
def setUp(self) -> None:
13+
"""Clean up rules and tracking before each test."""
14+
super().setUp()
15+
post_peerid = get_table('filter').get_chain('post_peerid')
16+
post_peerid.rules = []
17+
# Clear the global tracking dictionary
18+
from hathor.p2p.netfilter import utils
19+
utils._peer_id_to_rule_uuid.clear()
20+
721
def test_peer_id_blacklist(self) -> None:
822
post_peerid = get_table('filter').get_chain('post_peerid')
923

@@ -24,3 +38,128 @@ def test_peer_id_blacklist(self) -> None:
2438
self.assertEqual(data['match']['type'], 'NetfilterMatchPeerId')
2539
self.assertIn(data['match']['match_params']['peer_id'], blacklist)
2640
self.assertEqual(data['target']['type'], 'NetfilterReject')
41+
42+
def test_add_blacklist_peers_with_list(self) -> None:
43+
"""Test adding multiple peers with a list."""
44+
post_peerid = get_table('filter').get_chain('post_peerid')
45+
46+
# Initially empty
47+
self.assertEqual(len(post_peerid.rules), 0)
48+
self.assertEqual(list_blacklist_peers(), [])
49+
50+
# Add peers
51+
peer_ids = ['peer1', 'peer2', 'peer3']
52+
added = add_blacklist_peers(peer_ids)
53+
54+
# All peers should be added
55+
self.assertEqual(sorted(added), sorted(peer_ids))
56+
self.assertEqual(len(post_peerid.rules), 3)
57+
self.assertEqual(sorted(list_blacklist_peers()), sorted(peer_ids))
58+
59+
def test_add_blacklist_peers_with_string(self) -> None:
60+
"""Test adding a single peer with a string."""
61+
post_peerid = get_table('filter').get_chain('post_peerid')
62+
63+
# Add single peer
64+
peer_id = 'single_peer'
65+
added = add_blacklist_peers(peer_id)
66+
67+
self.assertEqual(added, [peer_id])
68+
self.assertEqual(len(post_peerid.rules), 1)
69+
self.assertEqual(list_blacklist_peers(), [peer_id])
70+
71+
def test_add_blacklist_peers_skip_duplicates(self) -> None:
72+
"""Test that adding duplicate peers is skipped."""
73+
post_peerid = get_table('filter').get_chain('post_peerid')
74+
75+
# Add peers first time
76+
peer_ids = ['peer1', 'peer2']
77+
added1 = add_blacklist_peers(peer_ids)
78+
self.assertEqual(sorted(added1), sorted(peer_ids))
79+
self.assertEqual(len(post_peerid.rules), 2)
80+
81+
# Try to add same peers again
82+
added2 = add_blacklist_peers(peer_ids)
83+
self.assertEqual(added2, []) # Nothing added
84+
self.assertEqual(len(post_peerid.rules), 2) # Still 2 rules
85+
86+
# Add mix of new and existing
87+
added3 = add_blacklist_peers(['peer1', 'peer3'])
88+
self.assertEqual(added3, ['peer3']) # Only new peer added
89+
self.assertEqual(len(post_peerid.rules), 3)
90+
91+
def test_add_blacklist_peers_skip_empty(self) -> None:
92+
"""Test that empty strings are skipped."""
93+
peer_ids = ['peer1', '', 'peer2', '']
94+
added = add_blacklist_peers(peer_ids)
95+
96+
self.assertEqual(sorted(added), ['peer1', 'peer2'])
97+
self.assertEqual(sorted(list_blacklist_peers()), ['peer1', 'peer2'])
98+
99+
def test_remove_blacklist_peers_with_list(self) -> None:
100+
"""Test removing multiple peers with a list."""
101+
# Add peers first
102+
peer_ids = ['peer1', 'peer2', 'peer3']
103+
add_blacklist_peers(peer_ids)
104+
self.assertEqual(sorted(list_blacklist_peers()), sorted(peer_ids))
105+
106+
# Remove some peers
107+
to_remove = ['peer1', 'peer3']
108+
removed = remove_blacklist_peers(to_remove)
109+
110+
self.assertEqual(sorted(removed), sorted(to_remove))
111+
self.assertEqual(list_blacklist_peers(), ['peer2'])
112+
113+
def test_remove_blacklist_peers_with_string(self) -> None:
114+
"""Test removing a single peer with a string."""
115+
# Add peers first
116+
add_blacklist_peers(['peer1', 'peer2'])
117+
118+
# Remove one peer
119+
removed = remove_blacklist_peers('peer1')
120+
121+
self.assertEqual(removed, ['peer1'])
122+
self.assertEqual(list_blacklist_peers(), ['peer2'])
123+
124+
def test_remove_blacklist_peers_nonexistent(self) -> None:
125+
"""Test removing peers that don't exist."""
126+
# Add one peer
127+
add_blacklist_peers('peer1')
128+
129+
# Try to remove nonexistent peers
130+
removed = remove_blacklist_peers(['peer2', 'peer3'])
131+
132+
self.assertEqual(removed, [])
133+
self.assertEqual(list_blacklist_peers(), ['peer1'])
134+
135+
# Remove mix of existing and nonexistent
136+
removed2 = remove_blacklist_peers(['peer1', 'peer2'])
137+
self.assertEqual(removed2, ['peer1'])
138+
self.assertEqual(list_blacklist_peers(), [])
139+
140+
def test_remove_blacklist_peers_skip_empty(self) -> None:
141+
"""Test that empty strings are skipped during removal."""
142+
add_blacklist_peers(['peer1', 'peer2'])
143+
144+
removed = remove_blacklist_peers(['peer1', '', 'peer2'])
145+
146+
self.assertEqual(sorted(removed), ['peer1', 'peer2'])
147+
self.assertEqual(list_blacklist_peers(), [])
148+
149+
def test_list_blacklist_peers(self) -> None:
150+
"""Test listing blacklisted peers."""
151+
# Initially empty
152+
self.assertEqual(list_blacklist_peers(), [])
153+
154+
# Add some peers
155+
peer_ids = ['peer1', 'peer2', 'peer3']
156+
add_blacklist_peers(peer_ids)
157+
self.assertEqual(sorted(list_blacklist_peers()), sorted(peer_ids))
158+
159+
# Remove one
160+
remove_blacklist_peers('peer2')
161+
self.assertEqual(sorted(list_blacklist_peers()), ['peer1', 'peer3'])
162+
163+
# Remove all
164+
remove_blacklist_peers(['peer1', 'peer3'])
165+
self.assertEqual(list_blacklist_peers(), [])

0 commit comments

Comments
 (0)