GitHub Action commited on
Commit
4d94786
·
1 Parent(s): 1e1ee41

Sync from GitHub with Git LFS

Browse files
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
- local_ports = storage.get_local_ports()
30
- print(f"[PeerSync] Local ports: {local_ports}")
 
 
 
 
 
 
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
- import dateutil.parser # для парсинга ISO datetime
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
- 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", [])
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
- valid_local_addresses.append(a)
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
- import dateutil.parser # для корректного парсинга ISO datetime
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
- peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
349
- addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
 
350
 
 
351
  if peer_id == my_id:
352
  continue
353
 
354
  try:
355
- addr_list = json.loads(addresses_json)
356
  except Exception as e:
357
  print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
358
  addr_list = []
359
 
360
- for addr_entry in addr_list:
361
- addr_str = addr_entry.get("addr")
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
- # IPv6 link-local
404
- if storage.is_ipv6(host) and host.startswith("fe80:"):
405
- scope_id = storage.get_ipv6_scope(host)
406
- if scope_id is None:
407
- print(f"[PeerExchange] Skipping {host}, no scope_id")
408
- continue
409
- sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
410
- sock.settimeout(3)
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 is_public(storage.parse_hostport(a.split("://", 1)[1])[0])
425
  ]
426
 
427
  handshake = {
@@ -430,11 +330,8 @@ def tcp_peer_exchange():
430
  "name": agent_name,
431
  "addresses": send_addresses,
432
  }
433
- raw_handshake = json.dumps(handshake).encode("utf-8")
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
- new_addrs = []
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
- new_addrs,
470
- source="peer_exchange",
471
- status="online"
 
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}: {e}")
477
  continue
478
 
479
  break
@@ -486,149 +366,116 @@ def tcp_peer_exchange():
486
  # ---------------------------
487
  # TCP Listener (входящие)
488
  # ---------------------------
489
- def tcp_listener():
490
- listen_sockets = []
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
- if not listen_sockets:
506
- time.sleep(1)
507
- continue
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
- msg = json.loads(data.decode("utf-8"))
523
- print(f"[TCP Listener] Decoded JSON from {addr}: {msg}")
524
- except Exception as e:
525
- print(f"[TCP Listener] JSON decode error from {addr}: {e}")
526
- conn.close()
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
- if existing_dt and dt_obj and existing_dt >= dt_obj:
561
- print(f"[TCP Listener] Skip old addr {addr_value} (dt={dt})")
562
- continue
 
 
 
563
 
564
- valid_addrs.append(a)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
- if valid_addrs:
567
  storage.add_or_update_peer(
568
- peer_id=peer_id,
569
- name=peer_name,
570
- addresses=valid_addrs,
571
- source="incoming",
572
- status="online"
573
  )
574
- print(f"[TCP Listener] Stored {len(valid_addrs)} addrs for peer {peer_id}")
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
- is_lan = storage.is_private(addr[0])
582
- peers_list = []
583
 
584
- for peer in storage.get_known_peers(my_id, limit=50):
585
- peer_id_local = peer["id"]
586
- try:
587
- addresses = json.loads(peer["addresses"])
588
- except:
589
- addresses = []
590
-
591
- updated_addresses = []
592
- for a in addresses:
593
  try:
594
- proto, hostport = a["addr"].split("://", 1)
595
- host, port = storage.parse_hostport(hostport)
596
- if not host or not port:
 
 
 
 
 
 
597
  continue
 
 
598
 
599
- if not is_lan and not is_public(host):
 
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
- peers_list.append({
614
- "id": peer_id_local,
615
- "addresses": updated_addresses
616
- })
 
617
 
618
- print(f"[TCP Listener] Sending {len(peers_list)} peers back to {peer_id}")
619
- conn.sendall(json.dumps(peers_list).encode("utf-8"))
620
 
621
- conn.close()
622
- except Exception as e:
623
- print(f"[TCP Listener] Connection handling error: {e}")
 
 
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
- threading.Thread(target=udp_discovery, daemon=True).start()
633
- threading.Thread(target=tcp_peer_exchange, daemon=True).start()
634
- threading.Thread(target=tcp_listener, daemon=True).start()
 
 
 
 
 
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
- Формат конфигурации: список dict {"address": str, "pow": {...}, "datetime": str, "difficulty": int}
983
- """
984
- local_addrs_json = self.get_config_value("local_addresses")
985
- if not local_addrs_json:
986
- return []
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["address"] if isinstance(entry, dict) else 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
- # Проверка неизменности pubkey
1090
- if existing_pubkey and pubkey and existing_pubkey != pubkey:
1091
- print(f"[WARN] Peer {peer_id} pubkey mismatch! Possible impersonation attempt.")
1092
- return
1093
- final_pubkey = existing_pubkey or pubkey
1094
-
1095
- # Объединяем адреса по addr, проверяем PoW и актуальность
1096
- addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
1097
- for a in norm_addresses:
1098
- addr = a["addr"]
1099
- nonce = a.get("nonce")
1100
- pow_hash = a.get("pow_hash")
1101
- dt = a.get("datetime")
1102
-
1103
- # проверка PoW
1104
- if nonce is not None and pow_hash is not None:
1105
- if not self.verify_pow(peer_id, final_pubkey, addr, nonce, pow_hash, dt):
1106
- print(f"[WARN] Peer {peer_id} address {addr} failed PoW validation")
1107
- continue
1108
-
1109
- # проверка актуальности по datetime
1110
- if addr in addr_map:
1111
- old_dt = addr_map[addr].get("datetime")
1112
- if old_dt and dt <= old_dt:
1113
- continue # оставляем более свежий адрес
1114
-
1115
- # обновляем запись
1116
- addr_map[addr] = {"addr": addr, "nonce": nonce, "pow_hash": pow_hash, "datetime": dt}
1117
-
1118
- combined_addresses = list(addr_map.values())
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

  • SHA256: b0b3fc8bff3e82b281a3be375c3c4152b416f1b361108e64a76076bb6022177e
  • Pointer size: 129 Bytes
  • Size of remote file: 7.5 kB