GitHub Action
commited on
Commit
·
4d94786
1
Parent(s):
1e1ee41
Sync from GitHub with Git LFS
Browse files- agents/_not_used/peer_sync.py +731 -0
- agents/_not_used/peer_sync.py.old +259 -0
- agents/peer_sync.py +247 -400
- agents/tools/storage.py +72 -51
- assets/logo-hand-small.png +3 -0
agents/_not_used/peer_sync.py
ADDED
@@ -0,0 +1,731 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# agent/peer_sync.py
|
2 |
+
|
3 |
+
import socket
|
4 |
+
import json
|
5 |
+
import time
|
6 |
+
import threading
|
7 |
+
import select
|
8 |
+
import netifaces
|
9 |
+
import re
|
10 |
+
import ipaddress
|
11 |
+
import asyncio
|
12 |
+
import dateutil.parser
|
13 |
+
|
14 |
+
from datetime import datetime, timezone as UTC
|
15 |
+
from tools.storage import Storage
|
16 |
+
|
17 |
+
storage = Storage()
|
18 |
+
|
19 |
+
# ---------------------------
|
20 |
+
# Конфигурация (будем пересчитывать после bootstrap)
|
21 |
+
# ---------------------------
|
22 |
+
my_id = storage.get_config_value("agent_id")
|
23 |
+
agent_name = storage.get_config_value("agent_name", "unknown")
|
24 |
+
|
25 |
+
# placeholders — реальные значения пересчитаются в start_sync()
|
26 |
+
local_addresses = []
|
27 |
+
global_addresses = []
|
28 |
+
all_addresses = []
|
29 |
+
local_ports = []
|
30 |
+
print(f"[PeerSync] (init) my_id={my_id} name={agent_name}")
|
31 |
+
|
32 |
+
# ---------------------------
|
33 |
+
# Загрузка bootstrap
|
34 |
+
# ---------------------------
|
35 |
+
def load_bootstrap_peers(filename="bootstrap.txt"):
|
36 |
+
try:
|
37 |
+
with open(filename, "r", encoding="utf-8") as f:
|
38 |
+
lines = f.readlines()
|
39 |
+
except FileNotFoundError:
|
40 |
+
print(f"[Bootstrap] File {filename} not found")
|
41 |
+
return
|
42 |
+
|
43 |
+
for line in lines:
|
44 |
+
line = line.strip()
|
45 |
+
if not line or line.startswith("#"):
|
46 |
+
continue
|
47 |
+
|
48 |
+
# Разделяем строку на ключ:значение по ";"
|
49 |
+
parts = [p.strip() for p in line.split(";") if p.strip()]
|
50 |
+
data = {}
|
51 |
+
for part in parts:
|
52 |
+
if ":" not in part:
|
53 |
+
continue
|
54 |
+
key, val = part.split(":", 1)
|
55 |
+
key = key.strip().upper()
|
56 |
+
val = val.strip()
|
57 |
+
if val.startswith('"') and val.endswith('"'):
|
58 |
+
val = val[1:-1].replace("\\n", "\n")
|
59 |
+
data[key] = val
|
60 |
+
|
61 |
+
# Проверка обязательных полей
|
62 |
+
did = data.get("DID")
|
63 |
+
pubkey = data.get("KEY")
|
64 |
+
addresses_json = data.get("ADDRESS")
|
65 |
+
name = data.get("NAME")
|
66 |
+
|
67 |
+
if not did or not pubkey or not addresses_json:
|
68 |
+
print(f"[Bootstrap] Missing required fields in line: {line}")
|
69 |
+
continue
|
70 |
+
|
71 |
+
# Парсим адреса
|
72 |
+
try:
|
73 |
+
addresses = json.loads(addresses_json)
|
74 |
+
except Exception as e:
|
75 |
+
print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
|
76 |
+
continue
|
77 |
+
|
78 |
+
# Расширяем any:// в tcp/udp и приводим к формату адресов
|
79 |
+
expanded_addresses = []
|
80 |
+
for addr in addresses:
|
81 |
+
if isinstance(addr, dict):
|
82 |
+
# старый формат с address/pow → конвертим
|
83 |
+
if "address" in addr:
|
84 |
+
addr_str = addr["address"]
|
85 |
+
if addr_str.startswith("any://"):
|
86 |
+
hostport = addr_str[len("any://"):]
|
87 |
+
variants = [f"tcp://{hostport}", f"udp://{hostport}"]
|
88 |
+
else:
|
89 |
+
variants = [addr_str]
|
90 |
+
|
91 |
+
for v in variants:
|
92 |
+
expanded_addresses.append({
|
93 |
+
"addr": v,
|
94 |
+
"nonce": addr.get("pow", {}).get("nonce"),
|
95 |
+
"pow_hash": addr.get("pow", {}).get("hash"),
|
96 |
+
"difficulty": addr.get("pow", {}).get("difficulty"),
|
97 |
+
"datetime": addr.get("datetime", "")
|
98 |
+
})
|
99 |
+
# уже новый формат → оставляем как есть
|
100 |
+
elif "addr" in addr:
|
101 |
+
expanded_addresses.append(addr)
|
102 |
+
|
103 |
+
elif isinstance(addr, str):
|
104 |
+
if addr.startswith("any://"):
|
105 |
+
hostport = addr[len("any://"):]
|
106 |
+
expanded_addresses.extend([
|
107 |
+
{"addr": f"tcp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""},
|
108 |
+
{"addr": f"udp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""}
|
109 |
+
])
|
110 |
+
else:
|
111 |
+
expanded_addresses.append({
|
112 |
+
"addr": addr,
|
113 |
+
"nonce": None,
|
114 |
+
"pow_hash": None,
|
115 |
+
"difficulty": None,
|
116 |
+
"datetime": ""
|
117 |
+
})
|
118 |
+
|
119 |
+
# Сохраняем в storage
|
120 |
+
print(f"[DEBUG] Saving peer {did} with addresses:")
|
121 |
+
for a in expanded_addresses:
|
122 |
+
print(a)
|
123 |
+
storage.add_or_update_peer(
|
124 |
+
peer_id=did,
|
125 |
+
name=name,
|
126 |
+
addresses=expanded_addresses,
|
127 |
+
source="bootstrap",
|
128 |
+
status="offline",
|
129 |
+
pubkey=pubkey,
|
130 |
+
capabilities=None,
|
131 |
+
heard_from=None
|
132 |
+
)
|
133 |
+
|
134 |
+
print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
|
135 |
+
|
136 |
+
# ---------------------------
|
137 |
+
# UDP Discovery
|
138 |
+
# ---------------------------
|
139 |
+
def udp_discovery():
|
140 |
+
import dateutil.parser # для парсинга ISO datetime
|
141 |
+
DISCOVERY_INTERVAL = 30
|
142 |
+
|
143 |
+
try:
|
144 |
+
# --- Создаём слушающие сокеты один раз (на основе текущих локальных адресов в storage) ---
|
145 |
+
listen_sockets = []
|
146 |
+
cfg_local_addresses = storage.get_config_value("local_addresses", [])
|
147 |
+
print(f"[UDP Discovery] Local addresses (init): {cfg_local_addresses}")
|
148 |
+
|
149 |
+
for entry in cfg_local_addresses:
|
150 |
+
addr_str = entry.get("addr") if isinstance(entry, dict) else entry
|
151 |
+
if not addr_str:
|
152 |
+
continue
|
153 |
+
|
154 |
+
proto, hostport = addr_str.split("://", 1)
|
155 |
+
host, port = storage.parse_hostport(hostport)
|
156 |
+
if not port or proto.lower() != "udp":
|
157 |
+
continue
|
158 |
+
|
159 |
+
# IPv4
|
160 |
+
if not host.startswith("["):
|
161 |
+
try:
|
162 |
+
sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
163 |
+
sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
164 |
+
sock4.bind(("", port))
|
165 |
+
listen_sockets.append(sock4)
|
166 |
+
print(f"[UDP Discovery] Listening IPv4 on *:{port}")
|
167 |
+
except Exception as e:
|
168 |
+
print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
|
169 |
+
|
170 |
+
# IPv6
|
171 |
+
else:
|
172 |
+
try:
|
173 |
+
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
174 |
+
sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
175 |
+
sock6.bind(("::", port))
|
176 |
+
listen_sockets.append(sock6)
|
177 |
+
print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
|
178 |
+
except Exception as e:
|
179 |
+
print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
|
180 |
+
|
181 |
+
except Exception as init_e:
|
182 |
+
print(f"[UDP Discovery] init error: {init_e}")
|
183 |
+
return
|
184 |
+
|
185 |
+
# --- Основной цикл ---
|
186 |
+
while True:
|
187 |
+
try:
|
188 |
+
agent_pubkey = storage.get_config_value("agent_pubkey")
|
189 |
+
|
190 |
+
# Приём входящих пакетов
|
191 |
+
if listen_sockets:
|
192 |
+
rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
|
193 |
+
for sock in rlist:
|
194 |
+
try:
|
195 |
+
data, addr = sock.recvfrom(2048)
|
196 |
+
print(f"[UDP Discovery] RAW from {addr}: {data!r}")
|
197 |
+
|
198 |
+
try:
|
199 |
+
msg = json.loads(data.decode("utf-8"))
|
200 |
+
except Exception as e:
|
201 |
+
print(f"[UDP Discovery] JSON decode error from {addr}: {e}")
|
202 |
+
continue
|
203 |
+
|
204 |
+
peer_id = msg.get("id")
|
205 |
+
if peer_id == my_id:
|
206 |
+
continue
|
207 |
+
name = msg.get("name", "unknown")
|
208 |
+
addresses = msg.get("addresses", []) or []
|
209 |
+
|
210 |
+
valid_addresses = []
|
211 |
+
for a in addresses:
|
212 |
+
addr_str = a.get("addr")
|
213 |
+
nonce = a.get("nonce")
|
214 |
+
pow_hash = a.get("pow_hash")
|
215 |
+
difficulty = a.get("difficulty")
|
216 |
+
dt = a.get("datetime")
|
217 |
+
pubkey = a.get("pubkey")
|
218 |
+
|
219 |
+
if not addr_str:
|
220 |
+
continue
|
221 |
+
|
222 |
+
# нормализуем адрес (везде используем единый формат)
|
223 |
+
addr_norm = storage.normalize_address(addr_str)
|
224 |
+
if not addr_norm:
|
225 |
+
print(f"[UDP Discovery] Can't normalize addr {addr_str}, skip")
|
226 |
+
continue
|
227 |
+
|
228 |
+
# Проверка PoW — только если есть все поля
|
229 |
+
if nonce is not None and pow_hash and difficulty is not None:
|
230 |
+
if not pubkey:
|
231 |
+
print(f"[UDP Discovery] Peer {peer_id} addr {addr_norm} missing pubkey, skip PoW check")
|
232 |
+
continue
|
233 |
+
ok = storage.verify_pow(peer_id, pubkey, addr_norm, nonce, pow_hash, dt, difficulty)
|
234 |
+
print(f"[UDP Discovery] Verify PoW for {addr_norm} = {ok}")
|
235 |
+
if not ok:
|
236 |
+
continue
|
237 |
+
|
238 |
+
# Проверка datetime
|
239 |
+
existing = storage.get_peer_address(peer_id, addr_norm)
|
240 |
+
try:
|
241 |
+
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
242 |
+
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
243 |
+
except Exception as e:
|
244 |
+
print(f"[UDP Discovery] datetime parse error: {e}")
|
245 |
+
continue
|
246 |
+
|
247 |
+
if existing_dt and dt_obj and existing_dt >= dt_obj:
|
248 |
+
print(f"[UDP Discovery] Skip {addr_norm}: old datetime {dt}")
|
249 |
+
continue
|
250 |
+
|
251 |
+
# if all checks OK, ensure we keep canonical form in the stored dict
|
252 |
+
a_copy = dict(a)
|
253 |
+
a_copy["addr"] = addr_norm
|
254 |
+
valid_addresses.append(a_copy)
|
255 |
+
|
256 |
+
if valid_addresses:
|
257 |
+
storage.add_or_update_peer(
|
258 |
+
peer_id=peer_id,
|
259 |
+
name=name,
|
260 |
+
addresses=valid_addresses,
|
261 |
+
source="discovery",
|
262 |
+
status="online"
|
263 |
+
)
|
264 |
+
print(f"[UDP Discovery] Accepted peer {peer_id} ({addr}), {len(valid_addresses)} addresses")
|
265 |
+
|
266 |
+
except Exception as e:
|
267 |
+
print(f"[UDP Discovery] receive error: {e}")
|
268 |
+
|
269 |
+
# --- Отправка broadcast/multicast ---
|
270 |
+
# берем текущие локальные адреса из storage (актуально)
|
271 |
+
cfg_local_addresses = storage.get_config_value("local_addresses", [])
|
272 |
+
valid_local_addresses = []
|
273 |
+
|
274 |
+
for a in cfg_local_addresses:
|
275 |
+
addr_str = a.get("addr") if isinstance(a, dict) else a
|
276 |
+
nonce = a.get("nonce") if isinstance(a, dict) else None
|
277 |
+
pow_hash = a.get("pow_hash") if isinstance(a, dict) else None
|
278 |
+
difficulty = a.get("difficulty") if isinstance(a, dict) else None
|
279 |
+
dt = a.get("datetime") if isinstance(a, dict) else None
|
280 |
+
# prefer explicit pubkey per-address, otherwise agent_pubkey
|
281 |
+
addr_pubkey = a.get("pubkey") if isinstance(a, dict) else None
|
282 |
+
pubkey_used = addr_pubkey or agent_pubkey
|
283 |
+
|
284 |
+
if not addr_str:
|
285 |
+
continue
|
286 |
+
|
287 |
+
addr_norm = storage.normalize_address(addr_str)
|
288 |
+
if not addr_norm:
|
289 |
+
print(f"[UDP Discovery] Can't normalize local addr {addr_str}, skip")
|
290 |
+
continue
|
291 |
+
|
292 |
+
# Проверка PoW только если есть необходимые поля
|
293 |
+
if nonce is not None and pow_hash and difficulty is not None:
|
294 |
+
if not pubkey_used:
|
295 |
+
# если у агента нет pubkey в конфигах, не делаем жёсткую проверку PoW,
|
296 |
+
# потому что невозможно подтвердить — логируем и пропускаем проверку
|
297 |
+
print(f"[UDP Discovery] No pubkey for self addr {addr_norm}, skipping PoW self-check (will broadcast anyway)")
|
298 |
+
ok = True
|
299 |
+
else:
|
300 |
+
ok = storage.verify_pow(my_id, pubkey_used, addr_norm, nonce, pow_hash, dt, difficulty)
|
301 |
+
print(f"[UDP Discovery] Self-check PoW for {addr_norm} = {ok}")
|
302 |
+
if not ok:
|
303 |
+
print(f"[UDP Discovery] Self addr {addr_norm} failed PoW, skip")
|
304 |
+
continue
|
305 |
+
|
306 |
+
# attach pubkey for broadcast so receivers can verify
|
307 |
+
a_copy = dict(a) if isinstance(a, dict) else {"addr": addr_str}
|
308 |
+
a_copy["addr"] = addr_norm
|
309 |
+
if "pubkey" not in a_copy and agent_pubkey:
|
310 |
+
a_copy["pubkey"] = agent_pubkey
|
311 |
+
valid_local_addresses.append(a_copy)
|
312 |
+
|
313 |
+
msg_data = json.dumps({
|
314 |
+
"id": my_id,
|
315 |
+
"name": agent_name,
|
316 |
+
"addresses": valid_local_addresses
|
317 |
+
}).encode("utf-8")
|
318 |
+
|
319 |
+
print(f"[UDP Discovery] Broadcasting: {msg_data}")
|
320 |
+
|
321 |
+
for entry in valid_local_addresses:
|
322 |
+
addr_str = entry.get("addr")
|
323 |
+
proto, hostport = addr_str.split("://", 1)
|
324 |
+
host, port = storage.parse_hostport(hostport)
|
325 |
+
if not port or proto.lower() != "udp":
|
326 |
+
continue
|
327 |
+
|
328 |
+
# IPv4 broadcast
|
329 |
+
if not host.startswith("["):
|
330 |
+
for iface in netifaces.interfaces():
|
331 |
+
addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
|
332 |
+
for a in addrs:
|
333 |
+
if "broadcast" in a:
|
334 |
+
try:
|
335 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
336 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
337 |
+
print(f"[UDP Discovery] Sending broadcast -> {a['broadcast']}:{port}")
|
338 |
+
sock.sendto(msg_data, (a["broadcast"], port))
|
339 |
+
sock.close()
|
340 |
+
except Exception as e:
|
341 |
+
print(f"[UDP Discovery] Broadcast error {a['broadcast']}:{port}: {e}")
|
342 |
+
|
343 |
+
# IPv6 multicast ff02::1
|
344 |
+
else:
|
345 |
+
for iface in netifaces.interfaces():
|
346 |
+
ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
|
347 |
+
for a in ifaddrs:
|
348 |
+
addr_ipv6 = a.get("addr")
|
349 |
+
if not addr_ipv6:
|
350 |
+
continue
|
351 |
+
multicast_addr = f"ff02::1%{iface}" if addr_ipv6.startswith("fe80:") else "ff02::1"
|
352 |
+
try:
|
353 |
+
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
354 |
+
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
|
355 |
+
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
|
356 |
+
print(f"[UDP Discovery] Sending multicast -> {multicast_addr}:{port}")
|
357 |
+
sock6.sendto(msg_data, (multicast_addr, port))
|
358 |
+
sock6.close()
|
359 |
+
except Exception as e:
|
360 |
+
print(f"[UDP Discovery] Multicast error {multicast_addr}:{port}: {e}")
|
361 |
+
|
362 |
+
time.sleep(DISCOVERY_INTERVAL)
|
363 |
+
|
364 |
+
except Exception as main_e:
|
365 |
+
print(f"[UDP Discovery] main loop error: {main_e}")
|
366 |
+
time.sleep(DISCOVERY_INTERVAL)
|
367 |
+
|
368 |
+
# ---------------------------
|
369 |
+
# TCP Peer Exchange (исходящие)
|
370 |
+
# ---------------------------
|
371 |
+
def tcp_peer_exchange():
|
372 |
+
import dateutil.parser # для корректного парсинга ISO datetime
|
373 |
+
PEER_EXCHANGE_INTERVAL = 20 # секунды для отладки
|
374 |
+
|
375 |
+
while True:
|
376 |
+
# получаем свежий список пиров из БД
|
377 |
+
peers = storage.get_known_peers(my_id, limit=50)
|
378 |
+
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
379 |
+
|
380 |
+
for peer in peers:
|
381 |
+
peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
|
382 |
+
addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
|
383 |
+
|
384 |
+
if peer_id == my_id:
|
385 |
+
continue
|
386 |
+
|
387 |
+
try:
|
388 |
+
addr_list = json.loads(addresses_json)
|
389 |
+
except Exception as e:
|
390 |
+
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
391 |
+
addr_list = []
|
392 |
+
|
393 |
+
for addr_entry in addr_list:
|
394 |
+
addr_str = addr_entry.get("addr")
|
395 |
+
nonce = addr_entry.get("nonce")
|
396 |
+
pow_hash = addr_entry.get("pow_hash")
|
397 |
+
difficulty = addr_entry.get("difficulty")
|
398 |
+
dt = addr_entry.get("datetime")
|
399 |
+
pubkey = addr_entry.get("pubkey")
|
400 |
+
|
401 |
+
# нормализация адреса (включая скобки IPv6 и т.п.)
|
402 |
+
norm = storage.normalize_address(addr_str)
|
403 |
+
if not norm:
|
404 |
+
continue
|
405 |
+
|
406 |
+
# Проверка PoW
|
407 |
+
if nonce is not None and pow_hash and difficulty is not None:
|
408 |
+
if not pubkey:
|
409 |
+
print(f"[PeerExchange] Peer {peer_id} addr {norm} missing pubkey, skip PoW check")
|
410 |
+
continue
|
411 |
+
ok = storage.verify_pow(peer_id, pubkey, norm, nonce, pow_hash, dt, difficulty)
|
412 |
+
print(f"[PeerExchange] Verify PoW for {peer_id}@{norm} = {ok}")
|
413 |
+
if not ok:
|
414 |
+
continue
|
415 |
+
|
416 |
+
# Проверка datetime с использованием dateutil
|
417 |
+
existing = storage.get_peer_address(peer_id, norm)
|
418 |
+
try:
|
419 |
+
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
420 |
+
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
421 |
+
except Exception as e:
|
422 |
+
print(f"[PeerExchange] datetime parse error for {norm}: {e}")
|
423 |
+
continue
|
424 |
+
|
425 |
+
if existing_dt and dt_obj and existing_dt >= dt_obj:
|
426 |
+
print(f"[PeerExchange] Skip {norm}: old datetime {dt}")
|
427 |
+
continue
|
428 |
+
|
429 |
+
# Парсим host и port
|
430 |
+
proto, hostport = norm.split("://", 1)
|
431 |
+
if proto not in ["tcp", "any"]:
|
432 |
+
continue
|
433 |
+
host, port = storage.parse_hostport(hostport)
|
434 |
+
if not host or not port:
|
435 |
+
continue
|
436 |
+
|
437 |
+
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
438 |
+
|
439 |
+
try:
|
440 |
+
# IPv6 link-local
|
441 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
442 |
+
scope_id = storage.get_ipv6_scope(host)
|
443 |
+
if scope_id is None:
|
444 |
+
print(f"[PeerExchange] Skipping {host}, no scope_id")
|
445 |
+
continue
|
446 |
+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
447 |
+
sock.settimeout(3)
|
448 |
+
sock.connect((host, port, 0, scope_id))
|
449 |
+
else:
|
450 |
+
sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
|
451 |
+
socket.SOCK_STREAM)
|
452 |
+
sock.settimeout(3)
|
453 |
+
sock.connect((host, port))
|
454 |
+
|
455 |
+
# Получаем актуальные адреса на момент отправки (не использовать stale all_addresses)
|
456 |
+
cfg_local_addresses = storage.get_config_value("local_addresses", [])
|
457 |
+
cfg_global_addresses = storage.get_config_value("global_addresses", [])
|
458 |
+
cur_all_addresses = cfg_local_addresses + cfg_global_addresses
|
459 |
+
|
460 |
+
# Отправка handshake — фильтр адресов для public/private
|
461 |
+
if storage.is_private(host):
|
462 |
+
send_addresses = cur_all_addresses
|
463 |
+
else:
|
464 |
+
# фильтруем только публичные
|
465 |
+
send_addresses = []
|
466 |
+
for a in cur_all_addresses:
|
467 |
+
a_addr = a.get("addr") if isinstance(a, dict) else a
|
468 |
+
try:
|
469 |
+
host_only = storage.parse_hostport(a_addr.split("://", 1)[1])[0]
|
470 |
+
if is_public(host_only):
|
471 |
+
send_addresses.append(a)
|
472 |
+
except Exception:
|
473 |
+
continue
|
474 |
+
|
475 |
+
handshake = {
|
476 |
+
"type": "PEER_EXCHANGE_REQUEST",
|
477 |
+
"id": my_id,
|
478 |
+
"name": agent_name,
|
479 |
+
"addresses": send_addresses,
|
480 |
+
}
|
481 |
+
raw_handshake = json.dumps(handshake).encode("utf-8")
|
482 |
+
print(f"[PeerExchange] Sending handshake -> {host}:{port}: {raw_handshake}")
|
483 |
+
sock.sendall(raw_handshake)
|
484 |
+
|
485 |
+
# Читаем ответ
|
486 |
+
data = sock.recv(64 * 1024)
|
487 |
+
sock.close()
|
488 |
+
|
489 |
+
if not data:
|
490 |
+
print(f"[PeerExchange] No data from {host}:{port}")
|
491 |
+
continue
|
492 |
+
|
493 |
+
print(f"[PeerExchange] RAW recv from {host}:{port}: {data!r}")
|
494 |
+
|
495 |
+
try:
|
496 |
+
peers_recv = json.loads(data.decode("utf-8"))
|
497 |
+
print(f"[PeerExchange] Parsed recv from {host}:{port}: {peers_recv}")
|
498 |
+
for p in peers_recv:
|
499 |
+
new_addrs = []
|
500 |
+
for a in p.get("addresses", []):
|
501 |
+
try:
|
502 |
+
existing_addr = storage.get_peer_address(p["id"], a.get("addr"))
|
503 |
+
existing_dt = dateutil.parser.isoparse(existing_addr.get("datetime")) if existing_addr else None
|
504 |
+
dt_obj = dateutil.parser.isoparse(a.get("datetime")) if a.get("datetime") else None
|
505 |
+
if existing_addr is None or (existing_dt and dt_obj and existing_dt < dt_obj) or existing_dt is None:
|
506 |
+
new_addrs.append(a)
|
507 |
+
else:
|
508 |
+
print(f"[PeerExchange] Ignored old {a.get('addr')} from {p['id']}")
|
509 |
+
except Exception as e:
|
510 |
+
print(f"[PeerExchange] Error parsing datetime for {a.get('addr')}: {e}")
|
511 |
+
continue
|
512 |
+
|
513 |
+
if new_addrs:
|
514 |
+
storage.add_or_update_peer(
|
515 |
+
p["id"],
|
516 |
+
p.get("name", "unknown"),
|
517 |
+
new_addrs,
|
518 |
+
source="peer_exchange",
|
519 |
+
status="online"
|
520 |
+
)
|
521 |
+
print(f"[PeerExchange] Stored {len(new_addrs)} new addrs for peer {p['id']}")
|
522 |
+
print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
|
523 |
+
except Exception as e:
|
524 |
+
print(f"[PeerExchange] Decode error from {host}:{port}: {e}")
|
525 |
+
continue
|
526 |
+
|
527 |
+
break
|
528 |
+
except Exception as e:
|
529 |
+
print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
|
530 |
+
continue
|
531 |
+
|
532 |
+
time.sleep(PEER_EXCHANGE_INTERVAL)
|
533 |
+
|
534 |
+
# ---------------------------
|
535 |
+
# TCP Listener (входящие)
|
536 |
+
# ---------------------------
|
537 |
+
def tcp_listener():
|
538 |
+
# получаем локальные порты в момент старта listener'а
|
539 |
+
listen_sockets = []
|
540 |
+
cfg_local_ports = storage.get_local_ports()
|
541 |
+
print(f"[TCP Listener] binding to local ports: {cfg_local_ports}")
|
542 |
+
for port in cfg_local_ports:
|
543 |
+
for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
|
544 |
+
try:
|
545 |
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
546 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
547 |
+
sock.bind((addr_str, port))
|
548 |
+
sock.listen(5)
|
549 |
+
listen_sockets.append(sock)
|
550 |
+
proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
|
551 |
+
print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
|
552 |
+
except Exception as e:
|
553 |
+
print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
|
554 |
+
|
555 |
+
while True:
|
556 |
+
if not listen_sockets:
|
557 |
+
time.sleep(1)
|
558 |
+
continue
|
559 |
+
|
560 |
+
rlist, _, _ = select.select(listen_sockets, [], [], 1)
|
561 |
+
for s in rlist:
|
562 |
+
try:
|
563 |
+
conn, addr = s.accept()
|
564 |
+
data = conn.recv(64 * 1024)
|
565 |
+
if not data:
|
566 |
+
print(f"[TCP Listener] Empty data from {addr}, closing")
|
567 |
+
conn.close()
|
568 |
+
continue
|
569 |
+
|
570 |
+
print(f"[TCP Listener] RAW recv from {addr}: {data!r}")
|
571 |
+
|
572 |
+
try:
|
573 |
+
msg = json.loads(data.decode("utf-8"))
|
574 |
+
print(f"[TCP Listener] Decoded JSON from {addr}: {msg}")
|
575 |
+
except Exception as e:
|
576 |
+
print(f"[TCP Listener] JSON decode error from {addr}: {e}")
|
577 |
+
conn.close()
|
578 |
+
continue
|
579 |
+
|
580 |
+
if msg.get("type") == "PEER_EXCHANGE_REQUEST":
|
581 |
+
peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
|
582 |
+
peer_name = msg.get("name", "unknown")
|
583 |
+
peer_addrs = msg.get("addresses", []) or []
|
584 |
+
|
585 |
+
valid_addrs = []
|
586 |
+
for a in peer_addrs:
|
587 |
+
addr_value = a.get("addr")
|
588 |
+
nonce = a.get("nonce")
|
589 |
+
pow_hash = a.get("pow_hash")
|
590 |
+
difficulty = a.get("difficulty")
|
591 |
+
dt = a.get("datetime")
|
592 |
+
pubkey = a.get("pubkey")
|
593 |
+
|
594 |
+
if not addr_value:
|
595 |
+
print(f"[TCP Listener] Skip addr (no addr field): {a}")
|
596 |
+
continue
|
597 |
+
|
598 |
+
addr_norm = storage.normalize_address(addr_value)
|
599 |
+
if not addr_norm:
|
600 |
+
print(f"[TCP Listener] Can't normalize {addr_value}, skip")
|
601 |
+
continue
|
602 |
+
|
603 |
+
if nonce is None or not pow_hash or not pubkey:
|
604 |
+
print(f"[TCP Listener] Skip addr (incomplete): {a}")
|
605 |
+
continue
|
606 |
+
|
607 |
+
ok = storage.verify_pow(peer_id, pubkey, addr_norm, nonce, pow_hash, dt, difficulty)
|
608 |
+
print(f"[TCP Listener] Verify PoW for {addr_norm} = {ok}")
|
609 |
+
if not ok:
|
610 |
+
continue
|
611 |
+
|
612 |
+
existing = storage.get_peer_address(peer_id, addr_norm)
|
613 |
+
try:
|
614 |
+
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
615 |
+
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
616 |
+
except Exception as e:
|
617 |
+
print(f"[TCP Listener] datetime parse error for {addr_norm}: {e}")
|
618 |
+
continue
|
619 |
+
|
620 |
+
if existing_dt and dt_obj and existing_dt >= dt_obj:
|
621 |
+
print(f"[TCP Listener] Skip old addr {addr_norm} (dt={dt})")
|
622 |
+
continue
|
623 |
+
|
624 |
+
a_copy = dict(a)
|
625 |
+
a_copy["addr"] = addr_norm
|
626 |
+
valid_addrs.append(a_copy)
|
627 |
+
|
628 |
+
if valid_addrs:
|
629 |
+
storage.add_or_update_peer(
|
630 |
+
peer_id=peer_id,
|
631 |
+
name=peer_name,
|
632 |
+
addresses=valid_addrs,
|
633 |
+
source="incoming",
|
634 |
+
status="online"
|
635 |
+
)
|
636 |
+
print(f"[TCP Listener] Stored {len(valid_addrs)} addrs for peer {peer_id}")
|
637 |
+
else:
|
638 |
+
print(f"[TCP Listener] No valid addrs from {peer_id}")
|
639 |
+
|
640 |
+
print(f"[TCP Listener] Handshake from {peer_id} ({addr}) -> name={peer_name}")
|
641 |
+
|
642 |
+
# Готовим список пиров для ответа
|
643 |
+
is_lan = storage.is_private(addr[0])
|
644 |
+
peers_list = []
|
645 |
+
|
646 |
+
for peer in storage.get_known_peers(my_id, limit=50):
|
647 |
+
peer_id_local = peer["id"]
|
648 |
+
try:
|
649 |
+
addresses = json.loads(peer["addresses"])
|
650 |
+
except:
|
651 |
+
addresses = []
|
652 |
+
|
653 |
+
updated_addresses = []
|
654 |
+
for a in addresses:
|
655 |
+
try:
|
656 |
+
proto, hostport = a["addr"].split("://", 1)
|
657 |
+
host, port = storage.parse_hostport(hostport)
|
658 |
+
if not host or not port:
|
659 |
+
continue
|
660 |
+
|
661 |
+
if not is_lan and not is_public(host):
|
662 |
+
continue
|
663 |
+
|
664 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
665 |
+
scope_id = storage.get_ipv6_scope(host)
|
666 |
+
if scope_id:
|
667 |
+
host = f"{host}%{scope_id}"
|
668 |
+
|
669 |
+
updated_addresses.append({
|
670 |
+
"addr": f"{proto}://{host}:{port}"
|
671 |
+
})
|
672 |
+
except Exception:
|
673 |
+
continue
|
674 |
+
|
675 |
+
peers_list.append({
|
676 |
+
"id": peer_id_local,
|
677 |
+
"addresses": updated_addresses
|
678 |
+
})
|
679 |
+
|
680 |
+
print(f"[TCP Listener] Sending {len(peers_list)} peers back to {peer_id}")
|
681 |
+
conn.sendall(json.dumps(peers_list).encode("utf-8"))
|
682 |
+
|
683 |
+
conn.close()
|
684 |
+
except Exception as e:
|
685 |
+
print(f"[TCP Listener] Connection handling error: {e}")
|
686 |
+
|
687 |
+
# ---------------------------
|
688 |
+
# Запуск потоков
|
689 |
+
# ---------------------------
|
690 |
+
def start_sync(bootstrap_file="bootstrap.txt"):
|
691 |
+
# 1) загрузка bootstrap
|
692 |
+
load_bootstrap_peers(bootstrap_file)
|
693 |
+
|
694 |
+
# 2) пересчитать локальные config-derived переменные (теперь bootstrap и config загружены)
|
695 |
+
global local_addresses, global_addresses, all_addresses, local_ports
|
696 |
+
local_addresses = storage.get_config_value("local_addresses", [])
|
697 |
+
global_addresses = storage.get_config_value("global_addresses", [])
|
698 |
+
all_addresses = local_addresses + global_addresses
|
699 |
+
local_ports = storage.get_local_ports()
|
700 |
+
print(f"[PeerSync] Local ports (after bootstrap): {local_ports}")
|
701 |
+
print(f"[PeerSync] Local addresses (after bootstrap): {local_addresses}")
|
702 |
+
|
703 |
+
# 3) добавить себя в таблицу peers (чтобы pubkey, адреса были в БД и др. части кода могли их читать)
|
704 |
+
agent_pubkey = storage.get_config_value("agent_pubkey")
|
705 |
+
# подготавливаем адреса в том же формате, который add_or_update_peer ожидает
|
706 |
+
self_addrs = []
|
707 |
+
for a in all_addresses:
|
708 |
+
if isinstance(a, dict):
|
709 |
+
self_addrs.append(a)
|
710 |
+
else:
|
711 |
+
self_addrs.append({"addr": a, "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""})
|
712 |
+
|
713 |
+
try:
|
714 |
+
storage.add_or_update_peer(
|
715 |
+
peer_id=my_id,
|
716 |
+
name=agent_name,
|
717 |
+
addresses=self_addrs,
|
718 |
+
source="self",
|
719 |
+
status="online",
|
720 |
+
pubkey=agent_pubkey,
|
721 |
+
capabilities=None,
|
722 |
+
heard_from=None
|
723 |
+
)
|
724 |
+
print(f"[PeerSync] Registered self {my_id} in agent_peers (pubkey present: {bool(agent_pubkey)})")
|
725 |
+
except Exception as e:
|
726 |
+
print(f"[PeerSync] Failed to register self in agent_peers: {e}")
|
727 |
+
|
728 |
+
# 4) старт потоков
|
729 |
+
threading.Thread(target=udp_discovery, daemon=True).start()
|
730 |
+
threading.Thread(target=tcp_peer_exchange, daemon=True).start()
|
731 |
+
threading.Thread(target=tcp_listener, daemon=True).start()
|
agents/_not_used/peer_sync.py.old
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Давай! Вот старые версии:
|
2 |
+
|
3 |
+
```
|
4 |
+
def udp_discovery():
|
5 |
+
DISCOVERY_INTERVAL = 30
|
6 |
+
local_addresses = storage.get_config_value("local_addresses", [])
|
7 |
+
msg_data = json.dumps({
|
8 |
+
"id": my_id,
|
9 |
+
"name": agent_name,
|
10 |
+
"addresses": local_addresses
|
11 |
+
}).encode("utf-8")
|
12 |
+
|
13 |
+
# Создаём UDP сокеты для прослушки
|
14 |
+
listen_sockets = []
|
15 |
+
for port in local_ports:
|
16 |
+
# IPv4
|
17 |
+
try:
|
18 |
+
sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
19 |
+
sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
20 |
+
sock4.bind(("", port))
|
21 |
+
listen_sockets.append(sock4)
|
22 |
+
print(f"[UDP Discovery] Listening IPv4 on *:{port}")
|
23 |
+
except Exception as e:
|
24 |
+
print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
|
25 |
+
|
26 |
+
# IPv6
|
27 |
+
try:
|
28 |
+
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
29 |
+
sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
30 |
+
sock6.bind(("::", port))
|
31 |
+
listen_sockets.append(sock6)
|
32 |
+
print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
|
33 |
+
except Exception as e:
|
34 |
+
print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
|
35 |
+
|
36 |
+
while True:
|
37 |
+
# Приём сообщений
|
38 |
+
rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
|
39 |
+
for sock in rlist:
|
40 |
+
try:
|
41 |
+
data, addr = sock.recvfrom(2048)
|
42 |
+
msg = json.loads(data.decode("utf-8"))
|
43 |
+
peer_id = msg.get("id")
|
44 |
+
if peer_id == my_id:
|
45 |
+
continue
|
46 |
+
name = msg.get("name", "unknown")
|
47 |
+
addresses = msg.get("addresses", [])
|
48 |
+
storage.add_or_update_peer(peer_id, name, addresses, "discovery", "online")
|
49 |
+
print(f"[UDP Discovery] peer={peer_id} from {addr}")
|
50 |
+
except Exception as e:
|
51 |
+
print(f"[UDP Discovery] receive error: {e}")
|
52 |
+
|
53 |
+
# Отправка broadcast/multicast
|
54 |
+
for port in local_ports:
|
55 |
+
# IPv4 broadcast
|
56 |
+
for iface in netifaces.interfaces():
|
57 |
+
addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
|
58 |
+
for a in addrs:
|
59 |
+
if "broadcast" in a:
|
60 |
+
try:
|
61 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
62 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
63 |
+
sock.sendto(msg_data, (a["broadcast"], port))
|
64 |
+
sock.close()
|
65 |
+
except Exception:
|
66 |
+
continue
|
67 |
+
# IPv6 multicast ff02::1
|
68 |
+
for iface in netifaces.interfaces():
|
69 |
+
ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
|
70 |
+
for a in ifaddrs:
|
71 |
+
addr = a.get("addr")
|
72 |
+
if not addr:
|
73 |
+
continue
|
74 |
+
multicast_addr = f"ff02::1%{iface}" if addr.startswith("fe80:") else "ff02::1"
|
75 |
+
try:
|
76 |
+
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
77 |
+
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
|
78 |
+
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
|
79 |
+
sock6.sendto(msg_data, (multicast_addr, port))
|
80 |
+
sock6.close()
|
81 |
+
except Exception:
|
82 |
+
continue
|
83 |
+
|
84 |
+
time.sleep(DISCOVERY_INTERVAL)
|
85 |
+
|
86 |
+
---
|
87 |
+
|
88 |
+
def tcp_peer_exchange():
|
89 |
+
PEER_EXCHANGE_INTERVAL = 20 # для отладки
|
90 |
+
while True:
|
91 |
+
peers = storage.get_known_peers(my_id, limit=50)
|
92 |
+
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
93 |
+
|
94 |
+
for peer in peers:
|
95 |
+
peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
|
96 |
+
addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
|
97 |
+
|
98 |
+
if peer_id == my_id:
|
99 |
+
continue
|
100 |
+
|
101 |
+
try:
|
102 |
+
addr_list = json.loads(addresses_json)
|
103 |
+
except Exception as e:
|
104 |
+
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
105 |
+
addr_list = []
|
106 |
+
|
107 |
+
for addr in addr_list:
|
108 |
+
norm = storage.normalize_address(addr)
|
109 |
+
if not norm:
|
110 |
+
continue
|
111 |
+
proto, hostport = norm.split("://", 1)
|
112 |
+
if proto not in ["tcp", "any"]:
|
113 |
+
continue
|
114 |
+
host, port = storage.parse_hostport(hostport)
|
115 |
+
if not host or not port:
|
116 |
+
continue
|
117 |
+
|
118 |
+
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
119 |
+
try:
|
120 |
+
# IPv6 link-local
|
121 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
122 |
+
scope_id = storage.get_ipv6_scope(host)
|
123 |
+
if scope_id is None:
|
124 |
+
print(f"[PeerExchange] Skipping {host}, no scope_id")
|
125 |
+
continue
|
126 |
+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
127 |
+
sock.settimeout(3)
|
128 |
+
sock.connect((host, port, 0, scope_id))
|
129 |
+
else:
|
130 |
+
sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
|
131 |
+
socket.SOCK_STREAM)
|
132 |
+
sock.settimeout(3)
|
133 |
+
sock.connect((host, port))
|
134 |
+
|
135 |
+
# LAN или Интернет
|
136 |
+
if storage.is_private(host):
|
137 |
+
send_addresses = all_addresses
|
138 |
+
else:
|
139 |
+
send_addresses = [a for a in all_addresses
|
140 |
+
if is_public(stprage.parse_hostport(a.split("://", 1)[1])[0])]
|
141 |
+
|
142 |
+
handshake = {
|
143 |
+
"type": "PEER_EXCHANGE_REQUEST",
|
144 |
+
"id": my_id,
|
145 |
+
"name": agent_name,
|
146 |
+
"addresses": send_addresses,
|
147 |
+
}
|
148 |
+
sock.sendall(json.dumps(handshake).encode("utf-8"))
|
149 |
+
|
150 |
+
data = sock.recv(64 * 1024)
|
151 |
+
sock.close()
|
152 |
+
|
153 |
+
if not data:
|
154 |
+
print(f"[PeerExchange] No data from {host}:{port}")
|
155 |
+
continue
|
156 |
+
|
157 |
+
try:
|
158 |
+
peers_recv = json.loads(data.decode("utf-8"))
|
159 |
+
for p in peers_recv:
|
160 |
+
if p.get("id") and p["id"] != my_id:
|
161 |
+
storage.add_or_update_peer(
|
162 |
+
p["id"], p.get("name", "unknown"), p.get("addresses", []),
|
163 |
+
"peer_exchange", "online"
|
164 |
+
)
|
165 |
+
print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
|
166 |
+
except Exception as e:
|
167 |
+
print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
|
168 |
+
continue
|
169 |
+
|
170 |
+
break
|
171 |
+
except Exception as e:
|
172 |
+
print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
|
173 |
+
continue
|
174 |
+
|
175 |
+
time.sleep(PEER_EXCHANGE_INTERVAL)
|
176 |
+
|
177 |
+
---
|
178 |
+
|
179 |
+
def tcp_listener():
|
180 |
+
listen_sockets = []
|
181 |
+
for port in local_ports:
|
182 |
+
for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
|
183 |
+
try:
|
184 |
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
185 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
186 |
+
sock.bind((addr_str, port))
|
187 |
+
sock.listen(5)
|
188 |
+
listen_sockets.append(sock)
|
189 |
+
proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
|
190 |
+
print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
|
191 |
+
except Exception as e:
|
192 |
+
print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
|
193 |
+
|
194 |
+
while True:
|
195 |
+
if not listen_sockets:
|
196 |
+
time.sleep(1)
|
197 |
+
continue
|
198 |
+
|
199 |
+
rlist, _, _ = select.select(listen_sockets, [], [], 1)
|
200 |
+
for s in rlist:
|
201 |
+
try:
|
202 |
+
conn, addr = s.accept()
|
203 |
+
data = conn.recv(64 * 1024)
|
204 |
+
if not data:
|
205 |
+
conn.close()
|
206 |
+
continue
|
207 |
+
|
208 |
+
try:
|
209 |
+
msg = json.loads(data.decode("utf-8"))
|
210 |
+
except Exception as e:
|
211 |
+
print(f"[TCP Listener] JSON decode error from {addr}: {e}")
|
212 |
+
conn.close()
|
213 |
+
continue
|
214 |
+
|
215 |
+
if msg.get("type") == "PEER_EXCHANGE_REQUEST":
|
216 |
+
peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
|
217 |
+
peer_name = msg.get("name", "unknown")
|
218 |
+
peer_addrs = msg.get("addresses", [])
|
219 |
+
|
220 |
+
storage.add_or_update_peer(peer_id, peer_name, peer_addrs,
|
221 |
+
source="incoming", status="online")
|
222 |
+
print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
|
223 |
+
|
224 |
+
# LAN или Интернет
|
225 |
+
is_lan = storage.is_private(addr[0])
|
226 |
+
|
227 |
+
peers_list = []
|
228 |
+
for peer in storage.get_known_peers(my_id, limit=50):
|
229 |
+
peer_id = peer["id"]
|
230 |
+
try:
|
231 |
+
addresses = json.loads(peer["addresses"])
|
232 |
+
except:
|
233 |
+
addresses = []
|
234 |
+
|
235 |
+
updated_addresses = []
|
236 |
+
for a in addresses:
|
237 |
+
proto, hostport = a.split("://")
|
238 |
+
host, port = storage.parse_hostport(hostport)
|
239 |
+
|
240 |
+
# Фильтруем по LAN/Internet
|
241 |
+
if not is_lan and not is_public(host):
|
242 |
+
continue
|
243 |
+
|
244 |
+
# IPv6 link-local
|
245 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
246 |
+
scope_id = storage.get_ipv6_scope(host)
|
247 |
+
if scope_id:
|
248 |
+
host = f"{host}%{scope_id}"
|
249 |
+
|
250 |
+
updated_addresses.append(f"{proto}://{host}:{port}")
|
251 |
+
|
252 |
+
peers_list.append({"id": peer_id, "addresses": updated_addresses})
|
253 |
+
|
254 |
+
conn.sendall(json.dumps(peers_list).encode("utf-8"))
|
255 |
+
|
256 |
+
conn.close()
|
257 |
+
except Exception as e:
|
258 |
+
print(f"[TCP Listener] Connection handling error: {e}")
|
259 |
+
```
|
agents/peer_sync.py
CHANGED
@@ -6,28 +6,30 @@ import time
|
|
6 |
import threading
|
7 |
import select
|
8 |
import netifaces
|
9 |
-
import re
|
10 |
import ipaddress
|
11 |
-
import asyncio
|
12 |
-
import dateutil.parser
|
13 |
|
14 |
-
|
15 |
-
from datetime import datetime, timezone as UTC
|
16 |
from tools.storage import Storage
|
17 |
|
|
|
|
|
18 |
storage = Storage()
|
19 |
|
20 |
# ---------------------------
|
21 |
# Конфигурация
|
22 |
# ---------------------------
|
23 |
my_id = storage.get_config_value("agent_id")
|
|
|
24 |
agent_name = storage.get_config_value("agent_name", "unknown")
|
25 |
-
local_addresses = storage.get_config_value("local_addresses", [])
|
26 |
-
global_addresses = storage.get_config_value("global_addresses", [])
|
27 |
-
all_addresses = local_addresses + global_addresses # один раз
|
28 |
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
# ---------------------------
|
33 |
# Загрузка bootstrap
|
@@ -75,7 +77,7 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
|
|
75 |
print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
|
76 |
continue
|
77 |
|
78 |
-
# Расширяем any:// в tcp/udp и приводим к
|
79 |
expanded_addresses = []
|
80 |
for addr in addresses:
|
81 |
if isinstance(addr, dict):
|
@@ -128,268 +130,175 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
|
|
128 |
status="offline",
|
129 |
pubkey=pubkey,
|
130 |
capabilities=None,
|
131 |
-
heard_from=None
|
|
|
132 |
)
|
133 |
|
134 |
print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
|
135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
# ---------------------------
|
137 |
# UDP Discovery
|
138 |
# ---------------------------
|
139 |
-
def udp_discovery():
|
140 |
-
|
141 |
DISCOVERY_INTERVAL = 30
|
142 |
|
143 |
-
try:
|
144 |
-
# --- Создаём слушающие сокеты один раз ---
|
145 |
-
listen_sockets = []
|
146 |
-
local_addresses = storage.get_config_value("local_addresses", [])
|
147 |
-
print(f"[UDP Discovery] Local addresses (init): {local_addresses}")
|
148 |
-
|
149 |
-
for entry in local_addresses:
|
150 |
-
addr_str = entry.get("addr") if isinstance(entry, dict) else entry
|
151 |
-
if not addr_str:
|
152 |
-
continue
|
153 |
-
|
154 |
-
proto, hostport = addr_str.split("://", 1)
|
155 |
-
host, port = storage.parse_hostport(hostport)
|
156 |
-
if not port or proto.lower() != "udp":
|
157 |
-
continue
|
158 |
-
|
159 |
-
# IPv4
|
160 |
-
if not host.startswith("["):
|
161 |
-
try:
|
162 |
-
sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
163 |
-
sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
164 |
-
sock4.bind(("", port))
|
165 |
-
listen_sockets.append(sock4)
|
166 |
-
print(f"[UDP Discovery] Listening IPv4 on *:{port}")
|
167 |
-
except Exception as e:
|
168 |
-
print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
|
169 |
-
|
170 |
-
# IPv6
|
171 |
-
else:
|
172 |
-
try:
|
173 |
-
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
174 |
-
sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
175 |
-
sock6.bind(("::", port))
|
176 |
-
listen_sockets.append(sock6)
|
177 |
-
print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
|
178 |
-
except Exception as e:
|
179 |
-
print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
|
180 |
-
|
181 |
-
except Exception as init_e:
|
182 |
-
print(f"[UDP Discovery] init error: {init_e}")
|
183 |
-
return
|
184 |
-
|
185 |
-
# --- Основной цикл ---
|
186 |
-
agent_pubkey = storage.get_config_value("agent_pubkey")
|
187 |
-
|
188 |
while True:
|
|
|
189 |
try:
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
try:
|
199 |
-
|
|
|
|
|
|
|
200 |
except Exception as e:
|
201 |
-
print(f"[UDP Discovery]
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
valid_addresses = []
|
211 |
-
for a in addresses:
|
212 |
-
addr_str = a.get("addr")
|
213 |
-
nonce = a.get("nonce")
|
214 |
-
pow_hash = a.get("pow_hash")
|
215 |
-
difficulty = a.get("difficulty")
|
216 |
-
dt = a.get("datetime")
|
217 |
-
pubkey = a.get("pubkey")
|
218 |
-
|
219 |
-
if not addr_str or not pubkey:
|
220 |
-
continue
|
221 |
-
|
222 |
-
# Проверка PoW
|
223 |
-
if nonce is not None and pow_hash and difficulty is not None:
|
224 |
-
ok = storage.verify_pow(peer_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
|
225 |
-
print(f"[UDP Discovery] Verify PoW for {addr_str} = {ok}")
|
226 |
-
if not ok:
|
227 |
-
continue
|
228 |
-
|
229 |
-
# Проверка datetime
|
230 |
-
existing = storage.get_peer_address(peer_id, addr_str)
|
231 |
-
try:
|
232 |
-
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
233 |
-
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
234 |
-
except Exception as e:
|
235 |
-
print(f"[UDP Discovery] datetime parse error: {e}")
|
236 |
-
continue
|
237 |
-
|
238 |
-
if existing_dt and dt_obj and existing_dt >= dt_obj:
|
239 |
-
print(f"[UDP Discovery] Skip {addr_str}: old datetime {dt}")
|
240 |
-
continue
|
241 |
-
|
242 |
-
valid_addresses.append(a)
|
243 |
-
|
244 |
-
if valid_addresses:
|
245 |
-
storage.add_or_update_peer(
|
246 |
-
peer_id=peer_id,
|
247 |
-
name=name,
|
248 |
-
addresses=valid_addresses,
|
249 |
-
source="discovery",
|
250 |
-
status="online"
|
251 |
-
)
|
252 |
-
print(f"[UDP Discovery] Accepted peer {peer_id} ({addr}), {len(valid_addresses)} addresses")
|
253 |
-
|
254 |
-
except Exception as e:
|
255 |
-
print(f"[UDP Discovery] receive error: {e}")
|
256 |
-
|
257 |
-
# --- Отправка broadcast/multicast ---
|
258 |
-
local_addresses = storage.get_config_value("local_addresses", [])
|
259 |
-
valid_local_addresses = []
|
260 |
-
|
261 |
-
for a in local_addresses:
|
262 |
-
addr_str = a.get("addr") if isinstance(a, dict) else a
|
263 |
-
nonce = a.get("nonce")
|
264 |
-
pow_hash = a.get("pow_hash")
|
265 |
-
difficulty = a.get("difficulty")
|
266 |
-
dt = a.get("datetime")
|
267 |
-
pubkey = a.get("pubkey") if isinstance(a, dict) else agent_pubkey # self-check
|
268 |
-
|
269 |
-
if not addr_str:
|
270 |
-
continue
|
271 |
-
|
272 |
-
# Проверка PoW только если есть необходимые поля
|
273 |
-
if nonce is not None and pow_hash and difficulty is not None:
|
274 |
-
ok = storage.verify_pow(my_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
|
275 |
-
print(f"[UDP Discovery] Self-check PoW for {addr_str} = {ok}")
|
276 |
-
if not ok:
|
277 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
|
279 |
-
|
280 |
-
|
281 |
-
msg_data = json.dumps({
|
282 |
-
"id": my_id,
|
283 |
-
"name": agent_name,
|
284 |
-
"addresses": valid_local_addresses
|
285 |
-
}).encode("utf-8")
|
286 |
-
|
287 |
-
print(f"[UDP Discovery] Broadcasting: {msg_data}")
|
288 |
-
|
289 |
-
for entry in valid_local_addresses:
|
290 |
-
addr_str = entry.get("addr")
|
291 |
-
proto, hostport = addr_str.split("://", 1)
|
292 |
-
host, port = storage.parse_hostport(hostport)
|
293 |
-
if not port or proto.lower() != "udp":
|
294 |
-
continue
|
295 |
-
|
296 |
-
# IPv4 broadcast
|
297 |
-
if not host.startswith("["):
|
298 |
-
for iface in netifaces.interfaces():
|
299 |
-
addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
|
300 |
-
for a in addrs:
|
301 |
-
if "broadcast" in a:
|
302 |
-
try:
|
303 |
-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
304 |
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
305 |
-
print(f"[UDP Discovery] Sending broadcast -> {a['broadcast']}:{port}")
|
306 |
-
sock.sendto(msg_data, (a["broadcast"], port))
|
307 |
-
sock.close()
|
308 |
-
except Exception as e:
|
309 |
-
print(f"[UDP Discovery] Broadcast error {a['broadcast']}:{port}: {e}")
|
310 |
-
|
311 |
-
# IPv6 multicast ff02::1
|
312 |
-
else:
|
313 |
-
for iface in netifaces.interfaces():
|
314 |
-
ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
|
315 |
-
for a in ifaddrs:
|
316 |
-
addr_ipv6 = a.get("addr")
|
317 |
-
if not addr_ipv6:
|
318 |
-
continue
|
319 |
-
multicast_addr = f"ff02::1%{iface}" if addr_ipv6.startswith("fe80:") else "ff02::1"
|
320 |
-
try:
|
321 |
-
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
322 |
-
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
|
323 |
-
sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
|
324 |
-
print(f"[UDP Discovery] Sending multicast -> {multicast_addr}:{port}")
|
325 |
-
sock6.sendto(msg_data, (multicast_addr, port))
|
326 |
-
sock6.close()
|
327 |
-
except Exception as e:
|
328 |
-
print(f"[UDP Discovery] Multicast error {multicast_addr}:{port}: {e}")
|
329 |
-
|
330 |
-
time.sleep(DISCOVERY_INTERVAL)
|
331 |
-
|
332 |
-
except Exception as main_e:
|
333 |
-
print(f"[UDP Discovery] main loop error: {main_e}")
|
334 |
-
time.sleep(DISCOVERY_INTERVAL)
|
335 |
|
336 |
# ---------------------------
|
337 |
# TCP Peer Exchange (исходящие)
|
338 |
# ---------------------------
|
339 |
def tcp_peer_exchange():
|
340 |
-
|
341 |
-
PEER_EXCHANGE_INTERVAL = 20 # секунды для отладки
|
342 |
-
|
343 |
while True:
|
344 |
peers = storage.get_known_peers(my_id, limit=50)
|
345 |
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
346 |
|
347 |
for peer in peers:
|
348 |
-
|
349 |
-
|
|
|
350 |
|
|
|
351 |
if peer_id == my_id:
|
352 |
continue
|
353 |
|
354 |
try:
|
355 |
-
addr_list = json.loads(
|
356 |
except Exception as e:
|
357 |
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
358 |
addr_list = []
|
359 |
|
360 |
-
for
|
361 |
-
|
362 |
-
nonce = addr_entry.get("nonce")
|
363 |
-
pow_hash = addr_entry.get("pow_hash")
|
364 |
-
difficulty = addr_entry.get("difficulty")
|
365 |
-
dt = addr_entry.get("datetime")
|
366 |
-
pubkey = addr_entry.get("pubkey")
|
367 |
-
|
368 |
-
norm = storage.normalize_address(addr_str)
|
369 |
if not norm:
|
370 |
continue
|
371 |
-
|
372 |
-
# Проверка PoW
|
373 |
-
if nonce is not None and pow_hash and difficulty is not None and pubkey:
|
374 |
-
ok = storage.verify_pow(peer_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
|
375 |
-
print(f"[PeerExchange] Verify PoW for {peer_id}@{addr_str} = {ok}")
|
376 |
-
if not ok:
|
377 |
-
continue
|
378 |
-
|
379 |
-
# Проверка datetime с использованием dateutil
|
380 |
-
existing = storage.get_peer_address(peer_id, addr_str)
|
381 |
-
try:
|
382 |
-
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
383 |
-
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
384 |
-
except Exception as e:
|
385 |
-
print(f"[PeerExchange] datetime parse error for {addr_str}: {e}")
|
386 |
-
continue
|
387 |
-
|
388 |
-
if existing_dt and dt_obj and existing_dt >= dt_obj:
|
389 |
-
print(f"[PeerExchange] Skip {addr_str}: old datetime {dt}")
|
390 |
-
continue
|
391 |
-
|
392 |
-
# Парсим host и port
|
393 |
proto, hostport = norm.split("://", 1)
|
394 |
if proto not in ["tcp", "any"]:
|
395 |
continue
|
@@ -398,30 +307,21 @@ def tcp_peer_exchange():
|
|
398 |
continue
|
399 |
|
400 |
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
401 |
-
|
402 |
try:
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
sock.connect((host, port, 0, scope_id))
|
412 |
-
else:
|
413 |
-
sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
|
414 |
-
socket.SOCK_STREAM)
|
415 |
-
sock.settimeout(3)
|
416 |
-
sock.connect((host, port))
|
417 |
-
|
418 |
-
# Отправляем handshake
|
419 |
if storage.is_private(host):
|
420 |
send_addresses = all_addresses
|
421 |
else:
|
422 |
send_addresses = [
|
423 |
a for a in all_addresses
|
424 |
-
if
|
425 |
]
|
426 |
|
427 |
handshake = {
|
@@ -430,11 +330,8 @@ def tcp_peer_exchange():
|
|
430 |
"name": agent_name,
|
431 |
"addresses": send_addresses,
|
432 |
}
|
433 |
-
|
434 |
-
print(f"[PeerExchange] Sending handshake -> {host}:{port}: {raw_handshake}")
|
435 |
-
sock.sendall(raw_handshake)
|
436 |
|
437 |
-
# Читаем ответ
|
438 |
data = sock.recv(64 * 1024)
|
439 |
sock.close()
|
440 |
|
@@ -442,38 +339,21 @@ def tcp_peer_exchange():
|
|
442 |
print(f"[PeerExchange] No data from {host}:{port}")
|
443 |
continue
|
444 |
|
445 |
-
print(f"[PeerExchange] RAW recv from {host}:{port}: {data!r}")
|
446 |
-
|
447 |
try:
|
448 |
peers_recv = json.loads(data.decode("utf-8"))
|
449 |
-
print(f"[PeerExchange] Parsed recv from {host}:{port}: {peers_recv}")
|
450 |
for p in peers_recv:
|
451 |
-
|
452 |
-
for a in p.get("addresses", []):
|
453 |
-
try:
|
454 |
-
existing_addr = storage.get_peer_address(p["id"], a.get("addr"))
|
455 |
-
existing_dt = dateutil.parser.isoparse(existing_addr.get("datetime")) if existing_addr else None
|
456 |
-
dt_obj = dateutil.parser.isoparse(a.get("datetime")) if a.get("datetime") else None
|
457 |
-
if existing_addr is None or (existing_dt and dt_obj and existing_dt < dt_obj) or existing_dt is None:
|
458 |
-
new_addrs.append(a)
|
459 |
-
else:
|
460 |
-
print(f"[PeerExchange] Ignored old {a.get('addr')} from {p['id']}")
|
461 |
-
except Exception as e:
|
462 |
-
print(f"[PeerExchange] Error parsing datetime for {a.get('addr')}: {e}")
|
463 |
-
continue
|
464 |
-
|
465 |
-
if new_addrs:
|
466 |
storage.add_or_update_peer(
|
467 |
p["id"],
|
468 |
p.get("name", "unknown"),
|
469 |
-
|
470 |
-
|
471 |
-
|
|
|
472 |
)
|
473 |
-
print(f"[PeerExchange] Stored {len(new_addrs)} new addrs for peer {p['id']}")
|
474 |
print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
|
475 |
except Exception as e:
|
476 |
-
print(f"[PeerExchange] Decode error from {host}:{port}
|
477 |
continue
|
478 |
|
479 |
break
|
@@ -486,149 +366,116 @@ def tcp_peer_exchange():
|
|
486 |
# ---------------------------
|
487 |
# TCP Listener (входящие)
|
488 |
# ---------------------------
|
489 |
-
def tcp_listener():
|
490 |
-
|
491 |
-
for port in local_ports:
|
492 |
-
for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
|
493 |
-
try:
|
494 |
-
sock = socket.socket(family, socket.SOCK_STREAM)
|
495 |
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
496 |
-
sock.bind((addr_str, port))
|
497 |
-
sock.listen(5)
|
498 |
-
listen_sockets.append(sock)
|
499 |
-
proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
|
500 |
-
print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
|
501 |
-
except Exception as e:
|
502 |
-
print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
|
503 |
-
|
504 |
while True:
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
rlist, _, _ = select.select(listen_sockets, [], [], 1)
|
510 |
-
for s in rlist:
|
511 |
-
try:
|
512 |
-
conn, addr = s.accept()
|
513 |
-
data = conn.recv(64 * 1024)
|
514 |
-
if not data:
|
515 |
-
print(f"[TCP Listener] Empty data from {addr}, closing")
|
516 |
-
conn.close()
|
517 |
-
continue
|
518 |
-
|
519 |
-
print(f"[TCP Listener] RAW recv from {addr}: {data!r}")
|
520 |
-
|
521 |
try:
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
continue
|
528 |
-
|
529 |
-
if msg.get("type") == "PEER_EXCHANGE_REQUEST":
|
530 |
-
peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
|
531 |
-
peer_name = msg.get("name", "unknown")
|
532 |
-
peer_addrs = msg.get("addresses", [])
|
533 |
-
|
534 |
-
valid_addrs = []
|
535 |
-
for a in peer_addrs:
|
536 |
-
addr_value = a.get("addr")
|
537 |
-
nonce = a.get("nonce")
|
538 |
-
pow_hash = a.get("pow_hash")
|
539 |
-
difficulty = a.get("difficulty")
|
540 |
-
dt = a.get("datetime")
|
541 |
-
pubkey = a.get("pubkey")
|
542 |
-
|
543 |
-
if not addr_value or nonce is None or not pow_hash or not pubkey:
|
544 |
-
print(f"[TCP Listener] Skip addr (incomplete): {a}")
|
545 |
-
continue
|
546 |
-
|
547 |
-
ok = storage.verify_pow(peer_id, pubkey, addr_value, nonce, pow_hash, dt, difficulty)
|
548 |
-
print(f"[TCP Listener] Verify PoW for {addr_value} = {ok}")
|
549 |
-
if not ok:
|
550 |
-
continue
|
551 |
-
|
552 |
-
existing = storage.get_peer_address(peer_id, addr_value)
|
553 |
-
try:
|
554 |
-
existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
|
555 |
-
dt_obj = dateutil.parser.isoparse(dt) if dt else None
|
556 |
-
except Exception as e:
|
557 |
-
print(f"[TCP Listener] datetime parse error for {addr_value}: {e}")
|
558 |
-
continue
|
559 |
|
560 |
-
|
561 |
-
|
562 |
-
|
|
|
|
|
|
|
563 |
|
564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
565 |
|
566 |
-
if valid_addrs:
|
567 |
storage.add_or_update_peer(
|
568 |
-
peer_id
|
569 |
-
|
570 |
-
|
571 |
-
source="incoming",
|
572 |
-
status="online"
|
573 |
)
|
574 |
-
print(f"[TCP Listener]
|
575 |
-
else:
|
576 |
-
print(f"[TCP Listener] No valid addrs from {peer_id}")
|
577 |
-
|
578 |
-
print(f"[TCP Listener] Handshake from {peer_id} ({addr}) -> name={peer_name}")
|
579 |
|
580 |
-
|
581 |
-
|
582 |
-
peers_list = []
|
583 |
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
except:
|
589 |
-
addresses = []
|
590 |
-
|
591 |
-
updated_addresses = []
|
592 |
-
for a in addresses:
|
593 |
try:
|
594 |
-
|
595 |
-
|
596 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
597 |
continue
|
|
|
|
|
598 |
|
599 |
-
|
|
|
600 |
continue
|
601 |
|
602 |
-
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
603 |
-
scope_id = storage.get_ipv6_scope(host)
|
604 |
-
if scope_id:
|
605 |
-
host = f"{host}%{scope_id}"
|
606 |
-
|
607 |
updated_addresses.append({
|
608 |
-
"addr": f"{proto}://{host}:{port}"
|
|
|
|
|
|
|
609 |
})
|
610 |
-
except Exception:
|
611 |
-
continue
|
612 |
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
|
|
617 |
|
618 |
-
|
619 |
-
conn.sendall(json.dumps(peers_list).encode("utf-8"))
|
620 |
|
621 |
-
|
622 |
-
|
623 |
-
|
|
|
|
|
624 |
|
625 |
# ---------------------------
|
626 |
# Запуск потоков
|
627 |
# ---------------------------
|
628 |
def start_sync(bootstrap_file="bootstrap.txt"):
|
629 |
load_bootstrap_peers(bootstrap_file)
|
|
|
|
|
630 |
print(f"[PeerSync] Local ports: {local_ports}")
|
631 |
|
632 |
-
|
633 |
-
|
634 |
-
|
|
|
|
|
|
|
|
|
|
6 |
import threading
|
7 |
import select
|
8 |
import netifaces
|
|
|
9 |
import ipaddress
|
|
|
|
|
10 |
|
11 |
+
from datetime import datetime, timezone
|
|
|
12 |
from tools.storage import Storage
|
13 |
|
14 |
+
UTC = timezone.utc
|
15 |
+
|
16 |
storage = Storage()
|
17 |
|
18 |
# ---------------------------
|
19 |
# Конфигурация
|
20 |
# ---------------------------
|
21 |
my_id = storage.get_config_value("agent_id")
|
22 |
+
my_pubkey = storage.get_config_value("pubkay")
|
23 |
agent_name = storage.get_config_value("agent_name", "unknown")
|
|
|
|
|
|
|
24 |
|
25 |
+
local_addresses = storage.get_addresses("local")
|
26 |
+
global_addresses = storage.get_addresses("global")
|
27 |
+
all_addresses = local_addresses + global_addresses # один раз
|
28 |
+
|
29 |
+
#local_ports = list(set(storage.get_local_ports()))
|
30 |
+
#print(f"[PeerSync] Local ports: {local_ports}")
|
31 |
+
|
32 |
+
#print(f"[INFO] ID: {my_id}, NAME: {agent_name}: ADDRESS: {local_addresses} + {global_addresses} = {all_addresses}; Local ports: {local_ports}")
|
33 |
|
34 |
# ---------------------------
|
35 |
# Загрузка bootstrap
|
|
|
77 |
print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
|
78 |
continue
|
79 |
|
80 |
+
# Расширяем any:// в tcp/udp и приводим к формату адресов
|
81 |
expanded_addresses = []
|
82 |
for addr in addresses:
|
83 |
if isinstance(addr, dict):
|
|
|
130 |
status="offline",
|
131 |
pubkey=pubkey,
|
132 |
capabilities=None,
|
133 |
+
heard_from=None,
|
134 |
+
strict=False
|
135 |
)
|
136 |
|
137 |
print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
|
138 |
|
139 |
+
# ---------------------------
|
140 |
+
# start_peer_services
|
141 |
+
# ---------------------------
|
142 |
+
def start_peer_services(port):
|
143 |
+
"""Запускаем UDP и TCP слушатели на всех интерфейсах сразу"""
|
144 |
+
|
145 |
+
# UDP (один сокет для IPv4 и IPv6)
|
146 |
+
udp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
147 |
+
udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
148 |
+
udp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # слушаем и IPv4, и IPv6
|
149 |
+
udp_sock.bind(("::", port))
|
150 |
+
print(f"[UDP Discovery] Listening on [::]:{port} (IPv4+IPv6)")
|
151 |
+
|
152 |
+
# TCP (один сокет для IPv4 и IPv6)
|
153 |
+
tcp_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
154 |
+
tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
155 |
+
tcp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # слушаем и IPv4, и IPv6
|
156 |
+
tcp_sock.bind(("::", port))
|
157 |
+
tcp_sock.listen()
|
158 |
+
print(f"[TCP Listener] Listening on [::]:{port} (IPv4+IPv6)")
|
159 |
+
|
160 |
+
return udp_sock, tcp_sock
|
161 |
+
|
162 |
# ---------------------------
|
163 |
# UDP Discovery
|
164 |
# ---------------------------
|
165 |
+
def udp_discovery(sock, local_ports):
|
166 |
+
"""Приём и рассылка discovery через один сокет (IPv4+IPv6)."""
|
167 |
DISCOVERY_INTERVAL = 30
|
168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
while True:
|
170 |
+
# --- Приём сообщений ---
|
171 |
try:
|
172 |
+
rlist, _, _ = select.select([sock], [], [], 0.5)
|
173 |
+
for s in rlist:
|
174 |
+
try:
|
175 |
+
data, addr = s.recvfrom(2048)
|
176 |
+
msg = json.loads(data.decode("utf-8"))
|
177 |
+
peer_id = msg.get("id")
|
178 |
+
if peer_id == my_id:
|
179 |
+
continue
|
180 |
+
name = msg.get("name", "unknown")
|
181 |
+
raw_addresses = msg.get("addresses", [])
|
182 |
+
pubkey = msg.get("pubkey")
|
183 |
+
|
184 |
+
addresses = []
|
185 |
+
for a in raw_addresses:
|
186 |
+
if isinstance(a, dict) and "addr" in a:
|
187 |
+
addresses.append({
|
188 |
+
"addr": storage.normalize_address(a["addr"]),
|
189 |
+
"nonce": a.get("nonce"),
|
190 |
+
"pow_hash": a.get("pow_hash"),
|
191 |
+
"datetime": a.get("datetime")
|
192 |
+
})
|
193 |
+
elif isinstance(a, str):
|
194 |
+
addresses.append({
|
195 |
+
"addr": storage.normalize_address(a),
|
196 |
+
"nonce": None,
|
197 |
+
"pow_hash": None,
|
198 |
+
"datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
|
199 |
+
})
|
200 |
+
|
201 |
+
storage.add_or_update_peer(
|
202 |
+
peer_id, name, addresses,
|
203 |
+
source="discovery", status="online",
|
204 |
+
pubkey=pubkey, strict=False
|
205 |
+
)
|
206 |
+
print(f"[UDP Discovery] peer={peer_id} from {addr}")
|
207 |
+
except Exception as e:
|
208 |
+
print(f"[UDP Discovery] receive error: {e}")
|
209 |
+
except Exception as e:
|
210 |
+
print(f"[UDP Discovery] select() error: {e}")
|
211 |
+
|
212 |
+
# --- Формируем локальные адреса для рассылки ---
|
213 |
+
local_addresses = []
|
214 |
+
for iface in netifaces.interfaces():
|
215 |
+
for a in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
|
216 |
+
ip = a.get("addr")
|
217 |
+
if ip:
|
218 |
+
local_addresses.append({
|
219 |
+
"addr": storage.normalize_address(f"any://{ip}:{local_ports[0]}"),
|
220 |
+
"nonce": 0,
|
221 |
+
"pow_hash": "0"*64,
|
222 |
+
"datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
|
223 |
+
})
|
224 |
+
for a in netifaces.ifaddresses(iface).get(netifaces.AF_INET6, []):
|
225 |
+
ip = a.get("addr")
|
226 |
+
if ip:
|
227 |
+
local_addresses.append({
|
228 |
+
"addr": storage.normalize_address(f"any://[{ip}]:{local_ports[0]}"),
|
229 |
+
"nonce": 0,
|
230 |
+
"pow_hash": "0"*64,
|
231 |
+
"datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
|
232 |
+
})
|
233 |
|
234 |
+
msg_data = json.dumps({
|
235 |
+
"id": my_id,
|
236 |
+
"name": agent_name,
|
237 |
+
"addresses": local_addresses,
|
238 |
+
"pubkey": my_pubkey
|
239 |
+
}).encode("utf-8")
|
240 |
+
|
241 |
+
for port in local_ports:
|
242 |
+
# IPv4 broadcast
|
243 |
+
for iface in netifaces.interfaces():
|
244 |
+
addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
|
245 |
+
for a in addrs:
|
246 |
+
if "broadcast" in a:
|
247 |
try:
|
248 |
+
b_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
249 |
+
b_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
250 |
+
b_sock.sendto(msg_data, (a["broadcast"], port))
|
251 |
+
b_sock.close()
|
252 |
except Exception as e:
|
253 |
+
print(f"[UDP Discovery] IPv4 send error on {iface}:{port} -> {e}")
|
254 |
+
|
255 |
+
# IPv6 multicast ff02::1
|
256 |
+
for iface in netifaces.interfaces():
|
257 |
+
ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
|
258 |
+
for a in ifaddrs:
|
259 |
+
ip = a.get("addr")
|
260 |
+
if not ip:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
continue
|
262 |
+
multicast_addr = f"ff02::1%{iface}" if ip.startswith("fe80:") else "ff02::1"
|
263 |
+
try:
|
264 |
+
m_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
265 |
+
m_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
|
266 |
+
m_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
|
267 |
+
m_sock.sendto(msg_data, (multicast_addr, port))
|
268 |
+
m_sock.close()
|
269 |
+
except Exception as e:
|
270 |
+
print(f"[UDP Discovery] IPv6 send error on {iface}:{port} -> {e}")
|
271 |
|
272 |
+
time.sleep(DISCOVERY_INTERVAL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
|
274 |
# ---------------------------
|
275 |
# TCP Peer Exchange (исходящие)
|
276 |
# ---------------------------
|
277 |
def tcp_peer_exchange():
|
278 |
+
PEER_EXCHANGE_INTERVAL = 20
|
|
|
|
|
279 |
while True:
|
280 |
peers = storage.get_known_peers(my_id, limit=50)
|
281 |
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
282 |
|
283 |
for peer in peers:
|
284 |
+
# sqlite3.Row → dict
|
285 |
+
if not isinstance(peer, dict):
|
286 |
+
peer = dict(peer)
|
287 |
|
288 |
+
peer_id = peer.get("id")
|
289 |
if peer_id == my_id:
|
290 |
continue
|
291 |
|
292 |
try:
|
293 |
+
addr_list = json.loads(peer.get("addresses", "[]"))
|
294 |
except Exception as e:
|
295 |
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
296 |
addr_list = []
|
297 |
|
298 |
+
for addr in addr_list:
|
299 |
+
norm = storage.normalize_address(addr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
if not norm:
|
301 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
302 |
proto, hostport = norm.split("://", 1)
|
303 |
if proto not in ["tcp", "any"]:
|
304 |
continue
|
|
|
307 |
continue
|
308 |
|
309 |
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
|
|
310 |
try:
|
311 |
+
sock = socket.socket(
|
312 |
+
socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
|
313 |
+
socket.SOCK_STREAM
|
314 |
+
)
|
315 |
+
sock.settimeout(3)
|
316 |
+
sock.connect((host, port))
|
317 |
+
|
318 |
+
# LAN или Интернет
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
319 |
if storage.is_private(host):
|
320 |
send_addresses = all_addresses
|
321 |
else:
|
322 |
send_addresses = [
|
323 |
a for a in all_addresses
|
324 |
+
if not storage.is_private(storage.parse_hostport(a.split("://", 1)[1])[0])
|
325 |
]
|
326 |
|
327 |
handshake = {
|
|
|
330 |
"name": agent_name,
|
331 |
"addresses": send_addresses,
|
332 |
}
|
333 |
+
sock.sendall(json.dumps(handshake).encode("utf-8"))
|
|
|
|
|
334 |
|
|
|
335 |
data = sock.recv(64 * 1024)
|
336 |
sock.close()
|
337 |
|
|
|
339 |
print(f"[PeerExchange] No data from {host}:{port}")
|
340 |
continue
|
341 |
|
|
|
|
|
342 |
try:
|
343 |
peers_recv = json.loads(data.decode("utf-8"))
|
|
|
344 |
for p in peers_recv:
|
345 |
+
if p.get("id") and p["id"] != my_id:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
storage.add_or_update_peer(
|
347 |
p["id"],
|
348 |
p.get("name", "unknown"),
|
349 |
+
p.get("addresses", []),
|
350 |
+
"peer_exchange",
|
351 |
+
"online",
|
352 |
+
strict=False
|
353 |
)
|
|
|
354 |
print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
|
355 |
except Exception as e:
|
356 |
+
print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
|
357 |
continue
|
358 |
|
359 |
break
|
|
|
366 |
# ---------------------------
|
367 |
# TCP Listener (входящие)
|
368 |
# ---------------------------
|
369 |
+
def tcp_listener(sock):
|
370 |
+
"""Слушатель TCP (один сокет на IPv6, работает и для IPv4)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
while True:
|
372 |
+
try:
|
373 |
+
rlist, _, _ = select.select([sock], [], [], 1)
|
374 |
+
for s in rlist:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
375 |
try:
|
376 |
+
conn, addr = s.accept()
|
377 |
+
data = conn.recv(64 * 1024)
|
378 |
+
if not data:
|
379 |
+
conn.close()
|
380 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
381 |
|
382 |
+
try:
|
383 |
+
msg = json.loads(data.decode("utf-8"))
|
384 |
+
except Exception as e:
|
385 |
+
print(f"[TCP Listener] JSON decode error from {addr}: {e}")
|
386 |
+
conn.close()
|
387 |
+
continue
|
388 |
|
389 |
+
if msg.get("type") == "PEER_EXCHANGE_REQUEST":
|
390 |
+
peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
|
391 |
+
peer_name = msg.get("name", "unknown")
|
392 |
+
raw_addrs = msg.get("addresses", [])
|
393 |
+
pubkey = msg.get("pubkey")
|
394 |
+
|
395 |
+
# Нормализация и подготовка адресов
|
396 |
+
addresses = []
|
397 |
+
for a in raw_addrs:
|
398 |
+
if isinstance(a, dict) and "addr" in a:
|
399 |
+
addresses.append({
|
400 |
+
"addr": storage.normalize_address(a["addr"]),
|
401 |
+
"nonce": a.get("nonce"),
|
402 |
+
"pow_hash": a.get("pow_hash"),
|
403 |
+
"datetime": a.get("datetime")
|
404 |
+
})
|
405 |
+
elif isinstance(a, str):
|
406 |
+
addresses.append({
|
407 |
+
"addr": storage.normalize_address(a),
|
408 |
+
"nonce": None,
|
409 |
+
"pow_hash": None,
|
410 |
+
"datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
|
411 |
+
})
|
412 |
|
|
|
413 |
storage.add_or_update_peer(
|
414 |
+
peer_id, peer_name, addresses,
|
415 |
+
source="incoming", status="online",
|
416 |
+
pubkey=pubkey, strict=False
|
|
|
|
|
417 |
)
|
418 |
+
print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
|
|
|
|
|
|
|
|
|
419 |
|
420 |
+
# LAN или Интернет
|
421 |
+
is_lan = storage.is_private(addr[0])
|
|
|
422 |
|
423 |
+
# Формируем список пиров для отправки
|
424 |
+
peers_list = []
|
425 |
+
for peer in storage.get_known_peers(my_id, limit=50):
|
426 |
+
pid = peer["id"]
|
|
|
|
|
|
|
|
|
|
|
427 |
try:
|
428 |
+
peer_addrs = json.loads(peer.get("addresses", "[]"))
|
429 |
+
except:
|
430 |
+
peer_addrs = []
|
431 |
+
|
432 |
+
updated_addresses = []
|
433 |
+
for a in peer_addrs:
|
434 |
+
# Нормализация и проверка
|
435 |
+
addr_norm = storage.normalize_address(a.get("addr") if isinstance(a, dict) else a)
|
436 |
+
if not addr_norm:
|
437 |
continue
|
438 |
+
proto, hostport = addr_norm.split("://", 1)
|
439 |
+
host, port = storage.parse_hostport(hostport)
|
440 |
|
441 |
+
# Фильтруем приватные адреса при обмене с внешними пирами
|
442 |
+
if not is_lan and storage.is_private(host):
|
443 |
continue
|
444 |
|
|
|
|
|
|
|
|
|
|
|
445 |
updated_addresses.append({
|
446 |
+
"addr": f"{proto}://{host}:{port}",
|
447 |
+
"nonce": a.get("nonce") if isinstance(a, dict) else None,
|
448 |
+
"pow_hash": a.get("pow_hash") if isinstance(a, dict) else None,
|
449 |
+
"datetime": a.get("datetime") if isinstance(a, dict) else None
|
450 |
})
|
|
|
|
|
451 |
|
452 |
+
peers_list.append({
|
453 |
+
"id": pid,
|
454 |
+
"addresses": updated_addresses,
|
455 |
+
"pubkey": peer.get("pubkey")
|
456 |
+
})
|
457 |
|
458 |
+
conn.sendall(json.dumps(peers_list).encode("utf-8"))
|
|
|
459 |
|
460 |
+
conn.close()
|
461 |
+
except Exception as e:
|
462 |
+
print(f"[TCP Listener] Connection handling error: {e}")
|
463 |
+
except Exception as e:
|
464 |
+
print(f"[TCP Listener] select() error: {e}")
|
465 |
|
466 |
# ---------------------------
|
467 |
# Запуск потоков
|
468 |
# ---------------------------
|
469 |
def start_sync(bootstrap_file="bootstrap.txt"):
|
470 |
load_bootstrap_peers(bootstrap_file)
|
471 |
+
|
472 |
+
local_ports = list(set(storage.get_local_ports()))
|
473 |
print(f"[PeerSync] Local ports: {local_ports}")
|
474 |
|
475 |
+
for port in local_ports:
|
476 |
+
udp_sock, tcp_sock = start_peer_services(port)
|
477 |
+
|
478 |
+
threading.Thread(target=udp_discovery, args=(udp_sock, local_ports), daemon=True).start()
|
479 |
+
threading.Thread(target=tcp_listener, args=(tcp_sock,), daemon=True).start()
|
480 |
+
|
481 |
+
threading.Thread(target=tcp_peer_exchange, daemon=True).start()
|
agents/tools/storage.py
CHANGED
@@ -977,24 +977,17 @@ class Storage:
|
|
977 |
|
978 |
# Получаем уникальные локальные порты
|
979 |
def get_local_ports(self):
|
980 |
-
""
|
981 |
-
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
987 |
-
|
988 |
-
try:
|
989 |
-
local_addrs = json.loads(local_addrs_json)
|
990 |
-
except Exception:
|
991 |
-
print("[WARN] Не удалось разобрать local_addresses из БД")
|
992 |
-
return []
|
993 |
|
994 |
ports = []
|
995 |
for entry in local_addrs:
|
996 |
-
addr_str = entry
|
997 |
-
|
998 |
try:
|
999 |
proto, hostport = addr_str.split("://", 1)
|
1000 |
_, port = self.parse_hostport(hostport)
|
@@ -1004,6 +997,25 @@ class Storage:
|
|
1004 |
|
1005 |
return ports
|
1006 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1007 |
# Нормализация DID
|
1008 |
@staticmethod
|
1009 |
def normalize_did(did: str) -> str:
|
@@ -1025,15 +1037,14 @@ class Storage:
|
|
1025 |
self, peer_id, name, addresses,
|
1026 |
source="discovery", status="unknown",
|
1027 |
pubkey=None, capabilities=None,
|
1028 |
-
heard_from=None
|
1029 |
):
|
1030 |
c = self.conn.cursor()
|
1031 |
|
1032 |
-
# нормализуем входные адреса
|
1033 |
norm_addresses = []
|
1034 |
for a in (addresses or []):
|
1035 |
if isinstance(a, dict) and "addr" in a:
|
1036 |
-
# нормализация datetime: ISO 8601 без микросекунд
|
1037 |
dt_raw = a.get("datetime")
|
1038 |
if dt_raw:
|
1039 |
try:
|
@@ -1059,7 +1070,7 @@ class Storage:
|
|
1059 |
"datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
1060 |
})
|
1061 |
|
1062 |
-
# получаем существующую запись
|
1063 |
existing_addresses = []
|
1064 |
existing_pubkey = None
|
1065 |
existing_capabilities = {}
|
@@ -1086,38 +1097,48 @@ class Storage:
|
|
1086 |
final_capabilities = capabilities or existing_capabilities
|
1087 |
combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
|
1088 |
|
1089 |
-
#
|
1090 |
-
if
|
1091 |
-
|
1092 |
-
|
1093 |
-
|
1094 |
-
|
1095 |
-
|
1096 |
-
|
1097 |
-
|
1098 |
-
|
1099 |
-
|
1100 |
-
|
1101 |
-
|
1102 |
-
|
1103 |
-
|
1104 |
-
|
1105 |
-
|
1106 |
-
|
1107 |
-
|
1108 |
-
|
1109 |
-
|
1110 |
-
|
1111 |
-
|
1112 |
-
if
|
1113 |
-
|
1114 |
-
|
1115 |
-
|
1116 |
-
|
1117 |
-
|
1118 |
-
|
1119 |
-
|
1120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1121 |
c.execute("""
|
1122 |
INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
|
1123 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
977 |
|
978 |
# Получаем уникальные локальные порты
|
979 |
def get_local_ports(self):
|
980 |
+
local_addrs = self.get_config_value("local_addresses", [])
|
981 |
+
if not isinstance(local_addrs, list):
|
982 |
+
try:
|
983 |
+
local_addrs = json.loads(local_addrs)
|
984 |
+
except Exception:
|
985 |
+
print("[WARN] Не удалось разобрать local_addresses из БД")
|
986 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
987 |
|
988 |
ports = []
|
989 |
for entry in local_addrs:
|
990 |
+
addr_str = entry.get("addr") or entry.get("address") if isinstance(entry, dict) else entry
|
|
|
991 |
try:
|
992 |
proto, hostport = addr_str.split("://", 1)
|
993 |
_, port = self.parse_hostport(hostport)
|
|
|
997 |
|
998 |
return ports
|
999 |
|
1000 |
+
# Получить локальные или глобальные адреса
|
1001 |
+
def get_addresses(self, which="local"):
|
1002 |
+
key = f"{which}_addresses"
|
1003 |
+
addrs = self.get_config_value(key, [])
|
1004 |
+
if not isinstance(addrs, list):
|
1005 |
+
try:
|
1006 |
+
addrs = json.loads(addrs)
|
1007 |
+
except Exception:
|
1008 |
+
print(f"[WARN] Не удалось разобрать {key} из БД")
|
1009 |
+
return []
|
1010 |
+
|
1011 |
+
result = []
|
1012 |
+
for entry in addrs:
|
1013 |
+
if isinstance(entry, dict):
|
1014 |
+
result.append(entry.get("addr") or entry.get("address"))
|
1015 |
+
elif isinstance(entry, str):
|
1016 |
+
result.append(entry)
|
1017 |
+
return result
|
1018 |
+
|
1019 |
# Нормализация DID
|
1020 |
@staticmethod
|
1021 |
def normalize_did(did: str) -> str:
|
|
|
1037 |
self, peer_id, name, addresses,
|
1038 |
source="discovery", status="unknown",
|
1039 |
pubkey=None, capabilities=None,
|
1040 |
+
heard_from=None, strict: bool = True
|
1041 |
):
|
1042 |
c = self.conn.cursor()
|
1043 |
|
1044 |
+
# --- нормализуем входные адреса ---
|
1045 |
norm_addresses = []
|
1046 |
for a in (addresses or []):
|
1047 |
if isinstance(a, dict) and "addr" in a:
|
|
|
1048 |
dt_raw = a.get("datetime")
|
1049 |
if dt_raw:
|
1050 |
try:
|
|
|
1070 |
"datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
1071 |
})
|
1072 |
|
1073 |
+
# --- получаем существующую запись ---
|
1074 |
existing_addresses = []
|
1075 |
existing_pubkey = None
|
1076 |
existing_capabilities = {}
|
|
|
1097 |
final_capabilities = capabilities or existing_capabilities
|
1098 |
combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
|
1099 |
|
1100 |
+
# --- строгий режим ---
|
1101 |
+
if strict:
|
1102 |
+
# Проверка pubkey
|
1103 |
+
if existing_pubkey and pubkey and existing_pubkey != pubkey:
|
1104 |
+
print(f"[WARN] Peer {peer_id} pubkey mismatch! Possible impersonation attempt.")
|
1105 |
+
return
|
1106 |
+
final_pubkey = existing_pubkey or pubkey
|
1107 |
+
|
1108 |
+
# Объединяем адреса по addr, проверяем PoW и datetime
|
1109 |
+
addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
|
1110 |
+
for a in norm_addresses:
|
1111 |
+
addr = a["addr"]
|
1112 |
+
nonce = a.get("nonce")
|
1113 |
+
pow_hash = a.get("pow_hash")
|
1114 |
+
dt = a.get("datetime")
|
1115 |
+
|
1116 |
+
# проверка PoW
|
1117 |
+
if nonce is not None and pow_hash is not None:
|
1118 |
+
if not self.verify_pow(peer_id, final_pubkey, addr, nonce, pow_hash, dt):
|
1119 |
+
print(f"[WARN] Peer {peer_id} address {addr} failed PoW validation")
|
1120 |
+
continue
|
1121 |
+
|
1122 |
+
# проверка актуальности datetime
|
1123 |
+
if addr in addr_map:
|
1124 |
+
old_dt = addr_map[addr].get("datetime")
|
1125 |
+
if old_dt and dt <= old_dt:
|
1126 |
+
continue
|
1127 |
+
|
1128 |
+
addr_map[addr] = {"addr": addr, "nonce": nonce, "pow_hash": pow_hash, "datetime": dt}
|
1129 |
+
|
1130 |
+
combined_addresses = list(addr_map.values())
|
1131 |
+
|
1132 |
+
# --- упрощённый режим ---
|
1133 |
+
else:
|
1134 |
+
final_pubkey = existing_pubkey or pubkey
|
1135 |
+
addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
|
1136 |
+
for a in norm_addresses:
|
1137 |
+
# просто перезаписываем адреса без PoW и datetime проверки
|
1138 |
+
addr_map[a["addr"]] = a
|
1139 |
+
combined_addresses = list(addr_map.values())
|
1140 |
+
|
1141 |
+
# --- запись в БД ---
|
1142 |
c.execute("""
|
1143 |
INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
|
1144 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
assets/logo-hand-small.png
ADDED
![]() |
Git LFS Details
|