GitHub Action commited on
Commit
f2f34e4
·
1 Parent(s): 402975a

Sync from GitHub with Git LFS

Browse files
agents/bootstrap.txt CHANGED
@@ -1,3 +1,4 @@
1
- tcp://node1.mesh.local:8000
2
- udp://node2.mesh.local:8000
3
- any://node3.mesh.local:8000
 
 
1
+ did:hmp:ac0e063e-8609-4ef9-84ed-d7dcafc65977 ["tcp://node1.mesh.local:8000","udp://node1.mesh.local:8030"]
2
+ did:hmp:ac0e063e-8709-4ef9-84ed-d7dcafc65977 ["tcp://node2.mesh.local:8010"]
3
+ did:hmp:ac0e063e-8609-4ff9-84ed-d7dc46c65977 ["tcp://node1.mesh.local:8020"]
4
+ did:hmp:ac0f053e-8609-4ef9-84ed-d7dcafc65977 ["any://node3.mesh.local:8000"]
agents/peer_sync.py CHANGED
@@ -1,177 +1,209 @@
1
- # agents/peer_sync.py
2
 
3
  import socket
4
- import threading
5
- import time
6
  import json
7
- import uuid
8
- import ipaddress
9
- import re
10
- import netifaces
11
  import select
 
 
12
 
 
13
  from tools.storage import Storage
14
- from datetime import datetime
15
 
16
  storage = Storage()
17
- my_id = storage.get_config_value("agent_id", str(uuid.uuid4()))
18
- agent_name = storage.get_config_value("agent_name", "HMP-Agent")
19
-
20
- # ======================
21
- # Парсер host:port
22
- # ======================
23
- def parse_hostport(hostport):
24
- if hostport.startswith("["): # IPv6
25
- m = re.match(r"\[(.+)\]:(\d+)$", hostport)
26
- if m:
27
- return m.group(1), int(m.group(2))
28
- else: # IPv4
29
- if ":" in hostport:
30
- h, p = hostport.rsplit(":", 1)
31
- return h, int(p)
32
- return None, None
33
-
34
- def is_ipv6(host: str) -> bool:
35
- return ":" in host
36
-
37
- def is_loopback(host: str) -> bool:
38
- try:
39
- # IPv4 127.0.0.0/8
40
- if re.match(r"^127\.\d+\.\d+\.\d+$", host):
41
- return True
42
- # IPv6 ::1
43
- if host == "::1":
44
- return True
45
- except:
46
- pass
47
- return False
48
-
49
- # ======================
50
- # Сбор TCP/UDP портов для прослушивания
51
- # ======================
52
- def get_listening_ports():
53
- tcp_ports = set()
54
- udp_ports = set()
55
- for key in ["global_addresses", "local_addresses"]:
56
- addresses = storage.get_config_value(key, [])
57
- for a in addresses:
58
- try:
59
- proto, hostport = a.split("://", 1)
60
- host, port = parse_hostport(hostport)
61
- if host is None or port is None:
62
- continue
63
- if proto == "tcp":
64
- tcp_ports.add((host, port))
65
- elif proto in ["udp", "utp"]:
66
- udp_ports.add((host, port))
67
- elif proto == "any":
68
- tcp_ports.add((host, port))
69
- udp_ports.add((host, port))
70
- except Exception as e:
71
- print(f"[PeerSync] Ошибка разбора адреса {a}: {e}")
72
- continue
73
- return sorted(tcp_ports), sorted(udp_ports)
74
-
75
- tcp_ports, udp_ports = get_listening_ports()
76
-
77
- # ======================
78
- # Локальные IP (IPv4 + IPv6 link-local/global)
79
- # ======================
80
- def get_all_local_ips_v4():
81
- ips = []
82
- for iface in netifaces.interfaces():
83
- for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
84
- ip = addr.get("addr")
85
- if ip and not ip.startswith("127.") and not ip.startswith("0."):
86
- ips.append(ip)
87
- return ips
88
-
89
- def get_all_local_ips_v6():
90
- ips = []
91
- for iface in netifaces.interfaces():
92
- for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET6, []):
93
- ip = addr.get("addr")
94
- # обрезаем суффикс %ifname у Windows/Linux
95
- if ip:
96
- ip = ip.split("%")[0]
97
- if ip != "::1":
98
- ips.append(ip)
99
- return ips
100
-
101
- # ======================
102
- # Нормализация/расширение адресов для БД
103
- # ======================
104
- def expand_and_filter(addresses):
105
- out = []
106
- seen = set()
107
- for a in addresses:
108
- norm = storage.normalize_address(a)
109
- if not norm:
110
- continue
111
  try:
112
- proto, rest = norm.split("://", 1)
113
  except:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  continue
115
- host, _port = parse_hostport(rest)
116
- if not host or is_loopback(host):
 
 
117
  continue
118
 
119
- if proto == "tcp":
120
- cand = f"tcp://{rest}"
121
- if cand not in seen:
122
- out.append(cand); seen.add(cand)
123
- elif proto in ["udp", "utp", "any"]:
124
- for cand in (f"udp://{rest}", f"tcp://{rest}"):
125
- if cand not in seen:
126
- out.append(cand); seen.add(cand)
127
- return out
128
-
129
- # ======================
130
- # UDP LAN Discovery (IPv4 broadcast + IPv6 multicast)
131
- # ======================
132
- def udp_lan_discovery():
133
- DISCOVERY_INTERVAL = 30
134
 
135
- local_addresses = storage.get_config_value("local_addresses", [])
 
 
 
 
 
 
 
 
136
 
137
- # Получаем уникальные порты из local_addresses
138
- udp_port_set = set()
139
- for a in local_addresses:
140
- _, port = parse_hostport(a.split("://", 1)[1])
141
- if port:
142
- udp_port_set.add(port)
143
 
144
- # ------------------ Слушатели ------------------
 
 
 
145
  listen_sockets = []
146
- for port in udp_port_set:
147
- # IPv4
 
148
  try:
149
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
150
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
151
- sock.bind(("", port))
152
- listen_sockets.append(sock)
153
- print(f"[UDP/LAN Discovery] слушаем IPv4 на *:{port}")
 
154
  except Exception as e:
155
- print(f"[UDP/LAN Discovery] Не удалось создать IPv4 сокет {port}: {e}")
156
 
157
- # IPv6
158
  try:
159
- sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
160
  sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
161
  sock6.bind(("::", port))
 
162
  listen_sockets.append(sock6)
163
- print(f"[UDP/LAN Discovery] слушаем IPv6 на [::]:{port}")
164
  except Exception as e:
165
- print(f"[UDP/LAN Discovery] Не удалось создать IPv6 сокет {port}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
 
 
 
 
 
 
167
  msg_data = json.dumps({
168
  "id": my_id,
169
  "name": agent_name,
170
  "addresses": local_addresses
171
  }).encode("utf-8")
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  while True:
174
- # ------------------ Приём ------------------
175
  rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
176
  for sock in rlist:
177
  try:
@@ -180,193 +212,79 @@ def udp_lan_discovery():
180
  peer_id = msg.get("id")
181
  if peer_id == my_id:
182
  continue
183
-
184
  name = msg.get("name", "unknown")
185
  addresses = msg.get("addresses", [])
186
  storage.add_or_update_peer(peer_id, name, addresses, "discovery", "online")
187
- print(f"[UDP/LAN Discovery] peer={peer_id} name={name} addresses={addresses} from {addr}")
188
  except Exception as e:
189
- print(f"[UDP/LAN Discovery] ошибка при приёме: {e}")
190
 
191
- # ------------------ Отправка ------------------
192
- for port in udp_port_set:
193
- # ---------------- IPv4 Broadcast ----------------
194
  for iface in netifaces.interfaces():
195
  addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
196
  for a in addrs:
197
  if "broadcast" in a:
198
- bcast = a["broadcast"]
199
  try:
200
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
201
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
202
- sock.sendto(msg_data, (bcast, port))
203
  sock.close()
204
- print(f"[UDP/LAN Discovery] -> IPv4 broadcast {bcast}:{port}")
205
- except Exception as e:
206
- print(f"[UDP/LAN Discovery] ошибка IPv4 broadcast {bcast}:{port}: {e}")
207
-
208
- # ---------------- IPv6 Multicast ----------------
209
  for iface in netifaces.interfaces():
210
  ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
211
  for a in ifaddrs:
212
  addr = a.get("addr")
213
  if not addr:
214
  continue
215
-
216
- # Если link-local, добавляем scope_id
217
- if addr.startswith("fe80:"):
218
- multicast_addr = f"ff02::1%{iface}"
219
- else:
220
- multicast_addr = "ff02::1"
221
-
222
  try:
223
  sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
224
  sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
225
  sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
226
  sock6.sendto(msg_data, (multicast_addr, port))
227
  sock6.close()
228
- print(f"[UDP/LAN Discovery] -> IPv6 multicast {multicast_addr}:{port}")
229
  except Exception:
230
  continue
231
 
232
  time.sleep(DISCOVERY_INTERVAL)
233
 
234
- # ======================
235
- # UDP Discovery Sender (глобальный v4/v6 мультикаст/бродкаст как было)
236
- # ======================
237
- def udp_discovery_sender():
238
- # IPv4 multicast
239
- sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
240
- sock4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
241
- # IPv6 multicast
242
- try:
243
- sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
244
- sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 2)
245
- except Exception:
246
- sock6 = None
247
-
248
- global_addresses = storage.get_config_value("global_addresses", [])
249
- msg = {"id": my_id, "name": agent_name, "addresses": global_addresses}
250
-
251
- udp_port_set = set()
252
- for a in global_addresses:
253
- _, port = parse_hostport(a.split("://", 1)[1])
254
- if port:
255
- udp_port_set.add(port)
256
-
257
- last_broadcast = 0
258
- DISCOVERY_INTERVAL = 60
259
- BROADCAST_INTERVAL = 600
260
-
261
  while True:
262
- now = time.time()
263
- if int(now) % DISCOVERY_INTERVAL == 0:
264
- for port in udp_port_set:
265
- try:
266
- sock4.sendto(json.dumps(msg).encode("utf-8"), ("239.255.0.1", port))
267
- except:
268
- pass
269
- if sock6:
270
- try:
271
- sock6.sendto(json.dumps(msg).encode("utf-8"), ("ff02::1", port))
272
- except:
273
- pass
274
-
275
- if now - last_broadcast > BROADCAST_INTERVAL:
276
- try:
277
- sock4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
278
- for port in udp_port_set:
279
- sock4.sendto(json.dumps(msg).encode("utf-8"), ("255.255.255.255", port))
280
- except:
281
- pass
282
- last_broadcast = now
283
-
284
- time.sleep(1)
285
 
286
- # ==========================
287
- # TCP Listener (IPv4 + IPv6, link-local + global)
288
- # ==========================
289
- def tcp_listener():
290
- sockets = []
291
- for host, port in tcp_ports:
292
- try:
293
- if is_ipv6(host):
294
- s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
295
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
296
- bind_host = "::" if host in ["::", "any", "0.0.0.0"] else host
297
- s.bind((bind_host, port))
298
- s.listen(5)
299
- sockets.append(s)
300
- print(f"[TCP] Слушаем на [{bind_host}]:{port}")
301
  else:
302
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
303
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
304
- bind_host = "" if host in ["0.0.0.0", "any"] else host
305
- s.bind((bind_host, port))
306
- s.listen(5)
307
- sockets.append(s)
308
- print(f"[TCP] Слушаем на {bind_host}:{port}")
309
- except Exception as e:
310
- print(f"[TCP] Ошибка bind/listen {host}:{port} -> {e}")
311
-
312
- while True:
313
- if not sockets:
314
- time.sleep(1)
315
- continue
316
-
317
- readable, _, _ = select.select(sockets, [], [], 1)
318
- for s in readable:
319
- try:
320
- conn, addr = s.accept()
321
- data = b""
322
- try:
323
- data = conn.recv(1024) or b""
324
- except:
325
- pass
326
 
327
- if data == b"PEER_EXCHANGE_REQUEST":
328
- print(f"[TCP] PEER_EXCHANGE_REQUEST от {addr}")
329
- try:
330
- peers_list = []
331
- for pid, addresses_json in storage.get_online_peers(limit=50):
332
- try:
333
- addresses = json.loads(addresses_json)
334
- except:
335
- addresses = []
336
- addresses = expand_and_filter(addresses)
337
- peers_list.append({"id": pid, "addresses": addresses})
338
- payload = json.dumps(peers_list).encode("utf-8")
339
- conn.sendall(payload)
340
- print(f"[TCP] Отправлен список пиров ({len(peers_list)}) в {addr}")
341
- except Exception as e:
342
- print(f"[TCP] Ошибка при отправке списка пиров: {e}")
343
- conn.close()
344
- except Exception as e:
345
- print(f"[TCP] Ошибка при обработке соединения: {e}")
346
-
347
- # ==========================
348
- # Peer Exchange (инициатор TCP)
349
- # ==========================
350
- def peer_exchange():
351
- PEER_EXCHANGE_INTERVAL = 120
352
- while True:
353
- peers = storage.get_online_peers(limit=50)
354
- for peer in peers:
355
- peer_id, addresses_json = peer["id"], peer["addresses"]
356
  if peer_id == my_id:
357
  continue
358
 
359
  try:
360
  addr_list = json.loads(addresses_json)
361
- except:
 
362
  addr_list = []
363
 
 
 
364
  for addr in addr_list:
365
  norm = storage.normalize_address(addr)
366
  if not norm:
367
  continue
368
 
369
- proto, hostport = norm.split("://")
370
  if proto not in ["tcp", "any"]:
371
  continue
372
 
@@ -374,17 +292,22 @@ def peer_exchange():
374
  if not host or not port:
375
  continue
376
 
 
 
377
  try:
378
- # Для link-local IPv6 автоматически добавляем scope_id
379
  if is_ipv6(host) and host.startswith("fe80:"):
380
- for iface in get_all_local_ips_v6(): # перебор интерфейсов
381
- if iface.endswith(host): # простой вариант сопоставления
 
382
  scope_id = socket.if_nametoindex(iface)
383
- sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
384
- sock.settimeout(3)
385
- sock.connect((host, port, 0, scope_id))
386
  break
 
 
 
 
387
  else:
 
388
  continue
389
  else:
390
  sock = socket.socket(socket.AF_INET6 if is_ipv6(host) else socket.AF_INET, socket.SOCK_STREAM)
@@ -396,6 +319,7 @@ def peer_exchange():
396
  sock.close()
397
 
398
  if not data:
 
399
  continue
400
 
401
  try:
@@ -403,33 +327,109 @@ def peer_exchange():
403
  for p in peers_recv:
404
  if p.get("id") and p["id"] != my_id:
405
  storage.add_or_update_peer(
406
- p["id"],
407
- p.get("name", "unknown"),
408
- p.get("addresses", []),
409
- "peer_exchange",
410
- "online"
411
  )
412
- print(f"[PeerExchange] Получено {len(peers_recv)} пиров от {host}:{port}")
413
  except Exception as e:
414
- print(f"[PeerExchange] Ошибка разбора ответа от {host}:{port} -> {e}")
415
- break # успешное соединение, не пробуем остальные адреса этого пира
416
 
417
- except Exception:
 
 
418
  continue
419
 
420
  time.sleep(PEER_EXCHANGE_INTERVAL)
421
 
422
- # ======================
423
- # Основной запуск
424
- # ======================
425
- def start_sync():
426
- print("[PeerSync] Запуск фоновой синхронизации")
427
- storage.load_bootstrap()
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
- threading.Thread(target=udp_lan_discovery, daemon=True).start()
430
- threading.Thread(target=udp_discovery_sender, daemon=True).start()
431
- threading.Thread(target=peer_exchange, daemon=True).start()
432
- threading.Thread(target=tcp_listener, daemon=True).start()
 
 
 
 
 
 
433
 
434
  while True:
435
- time.sleep(60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
11
+ from datetime import datetime, timezone as UTC
12
  from tools.storage import Storage
 
13
 
14
  storage = Storage()
15
+
16
+ # ---------------------------
17
+ # Вспомогательные функции
18
+ # ---------------------------
19
+ def parse_hostport(s: str):
20
+ """
21
+ Разбирает "IP:port" или "[IPv6]:port" и возвращает (host, port)
22
+ """
23
+ s = s.strip()
24
+ if s.startswith("["):
25
+ # IPv6 с портом: [addr]:port
26
+ host, _, port = s[1:].partition("]:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  try:
28
+ port = int(port)
29
  except:
30
+ port = None
31
+ return host, port
32
+ else:
33
+ # IPv4 или IPv6 без []
34
+ if ":" in s:
35
+ host, port = s.rsplit(":", 1)
36
+ try:
37
+ port = int(port)
38
+ except:
39
+ port = None
40
+ return host, port
41
+ return s, None
42
+
43
+ def is_ipv6(host: str):
44
+ try:
45
+ socket.inet_pton(socket.AF_INET6, host)
46
+ return True
47
+ except OSError:
48
+ return False
49
+
50
+ # ---------------------------
51
+ # Загрузка bootstrap
52
+ # ---------------------------
53
+
54
+ def load_bootstrap_peers(filename="bootstrap.txt"):
55
+ """
56
+ Читает bootstrap.txt и добавляет узлы в storage.
57
+ Формат строки: did [JSON-список адресов]
58
+ """
59
+ try:
60
+ with open(filename, "r", encoding="utf-8") as f:
61
+ lines = f.readlines()
62
+ except FileNotFoundError:
63
+ print(f"[Bootstrap] File {filename} not found")
64
+ return
65
+
66
+ for line in lines:
67
+ line = line.strip()
68
+ if not line or line.startswith("#"):
69
  continue
70
+
71
+ match = re.match(r"^(did:hmp:[\w-]+)\s+(.+)$", line)
72
+ if not match:
73
+ print(f"[Bootstrap] Invalid line format: {line}")
74
  continue
75
 
76
+ did, addresses_json = match.groups()
77
+ try:
78
+ addresses = json.loads(addresses_json)
79
+ except Exception as e:
80
+ print(f"[Bootstrap] Invalid JSON addresses for {did}: {e}")
81
+ continue
 
 
 
 
 
 
 
 
 
82
 
83
+ # Разворачиваем any:// в tcp:// и udp://
84
+ expanded_addresses = []
85
+ for addr in addresses:
86
+ if addr.startswith("any://"):
87
+ hostport = addr[len("any://"):]
88
+ expanded_addresses.append(f"tcp://{hostport}")
89
+ expanded_addresses.append(f"udp://{hostport}")
90
+ else:
91
+ expanded_addresses.append(addr)
92
 
93
+ storage.add_or_update_peer(did, name=None, addresses=expanded_addresses,
94
+ source="bootstrap", status="offline")
95
+ print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
 
 
 
96
 
97
+ # ---------------------------
98
+ # TCP Listener (обработка входящих PEER_EXCHANGE_REQUEST)
99
+ # ---------------------------
100
+ def tcp_listener():
101
  listen_sockets = []
102
+
103
+ # Создаём TCP сокеты на всех локальных портах
104
+ for port in local_ports:
105
  try:
106
+ sock4 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
107
+ sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
108
+ sock4.bind(("", port))
109
+ sock4.listen(5)
110
+ listen_sockets.append(sock4)
111
+ print(f"[TCP Listener] Listening IPv4 on *:{port}")
112
  except Exception as e:
113
+ print(f"[TCP Listener] IPv4 bind failed on port {port}: {e}")
114
 
 
115
  try:
116
+ sock6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
117
  sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
118
  sock6.bind(("::", port))
119
+ sock6.listen(5)
120
  listen_sockets.append(sock6)
121
+ print(f"[TCP Listener] Listening IPv6 on [::]:{port}")
122
  except Exception as e:
123
+ print(f"[TCP Listener] IPv6 bind failed on port {port}: {e}")
124
+
125
+ while True:
126
+ if not listen_sockets:
127
+ time.sleep(1)
128
+ continue
129
+ rlist, _, _ = select.select(listen_sockets, [], [], 1)
130
+ for s in rlist:
131
+ try:
132
+ conn, addr = s.accept()
133
+ data = conn.recv(1024)
134
+ if data == b"PEER_EXCHANGE_REQUEST":
135
+ print(f"[TCP Listener] PEER_EXCHANGE_REQUEST from {addr}")
136
+ peers_list = []
137
+ for peer in storage.get_online_peers(limit=50):
138
+ peer_id = peer["id"]
139
+ addresses_json = peer["addresses"]
140
+ try:
141
+ addresses = json.loads(addresses_json)
142
+ except:
143
+ addresses = []
144
+ peers_list.append({"id": peer_id, "addresses": addresses})
145
+ payload = json.dumps(peers_list).encode("utf-8")
146
+ conn.sendall(payload)
147
+ conn.close()
148
+ except Exception as e:
149
+ print(f"[TCP Listener] Connection handling error: {e}")
150
+
151
+ # ---------------------------
152
+ # Конфигурация
153
+ # ---------------------------
154
+ my_id = storage.get_config_value("agent_id")
155
+ agent_name = storage.get_config_value("agent_name", "unknown")
156
+
157
+ # Получаем уникальные локальные порты для прослушки TCP/UDP
158
+ def get_local_ports():
159
+ ports = set()
160
+ local_addresses = storage.get_config_value("local_addresses", [])
161
+ for addr in local_addresses:
162
+ _, port = parse_hostport(addr.split("://", 1)[1])
163
+ if port:
164
+ ports.add(port)
165
+ return sorted(ports)
166
+
167
+ local_ports = get_local_ports()
168
+ print(f"[PeerSync] Local ports: {local_ports}")
169
 
170
+ # ---------------------------
171
+ # UDP Discovery
172
+ # ---------------------------
173
+ def udp_discovery():
174
+ DISCOVERY_INTERVAL = 30
175
+ local_addresses = storage.get_config_value("local_addresses", [])
176
  msg_data = json.dumps({
177
  "id": my_id,
178
  "name": agent_name,
179
  "addresses": local_addresses
180
  }).encode("utf-8")
181
 
182
+ # Создаём UDP сокеты для прослушки
183
+ listen_sockets = []
184
+ for port in local_ports:
185
+ # IPv4
186
+ try:
187
+ sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
188
+ sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
189
+ sock4.bind(("", port))
190
+ listen_sockets.append(sock4)
191
+ print(f"[UDP Discovery] Listening IPv4 on *:{port}")
192
+ except Exception as e:
193
+ print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
194
+
195
+ # IPv6
196
+ try:
197
+ sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
198
+ sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
199
+ sock6.bind(("::", port))
200
+ listen_sockets.append(sock6)
201
+ print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
202
+ except Exception as e:
203
+ print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
204
+
205
  while True:
206
+ # Приём сообщений
207
  rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
208
  for sock in rlist:
209
  try:
 
212
  peer_id = msg.get("id")
213
  if peer_id == my_id:
214
  continue
 
215
  name = msg.get("name", "unknown")
216
  addresses = msg.get("addresses", [])
217
  storage.add_or_update_peer(peer_id, name, addresses, "discovery", "online")
218
+ print(f"[UDP Discovery] peer={peer_id} from {addr}")
219
  except Exception as e:
220
+ print(f"[UDP Discovery] receive error: {e}")
221
 
222
+ # Отправка broadcast/multicast
223
+ for port in local_ports:
224
+ # IPv4 broadcast
225
  for iface in netifaces.interfaces():
226
  addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
227
  for a in addrs:
228
  if "broadcast" in a:
 
229
  try:
230
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
231
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
232
+ sock.sendto(msg_data, (a["broadcast"], port))
233
  sock.close()
234
+ except Exception:
235
+ continue
236
+ # IPv6 multicast ff02::1
 
 
237
  for iface in netifaces.interfaces():
238
  ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
239
  for a in ifaddrs:
240
  addr = a.get("addr")
241
  if not addr:
242
  continue
243
+ multicast_addr = f"ff02::1%{iface}" if addr.startswith("fe80:") else "ff02::1"
 
 
 
 
 
 
244
  try:
245
  sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
246
  sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
247
  sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
248
  sock6.sendto(msg_data, (multicast_addr, port))
249
  sock6.close()
 
250
  except Exception:
251
  continue
252
 
253
  time.sleep(DISCOVERY_INTERVAL)
254
 
255
+ # ---------------------------
256
+ # TCP Peer Exchange
257
+ # ---------------------------
258
+ def tcp_peer_exchange():
259
+ PEER_EXCHANGE_INTERVAL = 20 # для отладки сделаем меньше
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  while True:
261
+ peers = storage.get_online_peers(limit=50)
262
+ print(f"[PeerExchange] Checking {len(peers)} peers...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ for peer in peers:
265
+ # peer может быть tuple (id, addresses) или dict
266
+ if isinstance(peer, dict):
267
+ peer_id, addresses_json = peer["id"], peer["addresses"]
 
 
 
 
 
 
 
 
 
 
 
268
  else:
269
+ peer_id, addresses_json = peer[0], peer[1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  if peer_id == my_id:
272
  continue
273
 
274
  try:
275
  addr_list = json.loads(addresses_json)
276
+ except Exception as e:
277
+ print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
278
  addr_list = []
279
 
280
+ print(f"[PeerExchange] Peer {peer_id} -> addresses={addr_list}")
281
+
282
  for addr in addr_list:
283
  norm = storage.normalize_address(addr)
284
  if not norm:
285
  continue
286
 
287
+ proto, hostport = norm.split("://", 1)
288
  if proto not in ["tcp", "any"]:
289
  continue
290
 
 
292
  if not host or not port:
293
  continue
294
 
295
+ print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
296
+
297
  try:
298
+ # IPv6 link-local
299
  if is_ipv6(host) and host.startswith("fe80:"):
300
+ scope_id = None
301
+ for iface in netifaces.interfaces():
302
+ if iface.endswith(host):
303
  scope_id = socket.if_nametoindex(iface)
 
 
 
304
  break
305
+ if scope_id is not None:
306
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
307
+ sock.settimeout(3)
308
+ sock.connect((host, port, 0, scope_id))
309
  else:
310
+ print(f"[PeerExchange] Skipping {host}, no scope_id found")
311
  continue
312
  else:
313
  sock = socket.socket(socket.AF_INET6 if is_ipv6(host) else socket.AF_INET, socket.SOCK_STREAM)
 
319
  sock.close()
320
 
321
  if not data:
322
+ print(f"[PeerExchange] No data from {host}:{port}")
323
  continue
324
 
325
  try:
 
327
  for p in peers_recv:
328
  if p.get("id") and p["id"] != my_id:
329
  storage.add_or_update_peer(
330
+ p["id"], p.get("name", "unknown"), p.get("addresses", []),
331
+ "peer_exchange", "online"
 
 
 
332
  )
333
+ print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
334
  except Exception as e:
335
+ print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
336
+ continue
337
 
338
+ break # успешное соединение — идём к следующему пиру
339
+ except Exception as e:
340
+ print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
341
  continue
342
 
343
  time.sleep(PEER_EXCHANGE_INTERVAL)
344
 
345
+ # ---------------------------
346
+ # TCP Listener (обработка входящих PEER_EXCHANGE_REQUEST)
347
+ # ---------------------------
348
+ def tcp_listener():
349
+ listen_sockets = []
350
+
351
+ # Создаём TCP сокеты на всех локальных портах
352
+ for port in local_ports:
353
+ # IPv4
354
+ try:
355
+ sock4 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
356
+ sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
357
+ sock4.bind(("", port))
358
+ sock4.listen(5)
359
+ listen_sockets.append(sock4)
360
+ print(f"[TCP Listener] Listening IPv4 on *:{port}")
361
+ except Exception as e:
362
+ print(f"[TCP Listener] IPv4 bind failed on port {port}: {e}")
363
 
364
+ # IPv6
365
+ try:
366
+ sock6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
367
+ sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
368
+ sock6.bind(("::", port))
369
+ sock6.listen(5)
370
+ listen_sockets.append(sock6)
371
+ print(f"[TCP Listener] Listening IPv6 on [::]:{port}")
372
+ except Exception as e:
373
+ print(f"[TCP Listener] IPv6 bind failed on port {port}: {e}")
374
 
375
  while True:
376
+ if not listen_sockets:
377
+ time.sleep(1)
378
+ continue
379
+ rlist, _, _ = select.select(listen_sockets, [], [], 1)
380
+ for s in rlist:
381
+ try:
382
+ conn, addr = s.accept()
383
+ data = conn.recv(1024)
384
+ if data == b"PEER_EXCHANGE_REQUEST":
385
+ print(f"[TCP Listener] PEER_EXCHANGE_REQUEST from {addr}")
386
+ peers_list = []
387
+
388
+ for peer in storage.get_online_peers(limit=50):
389
+ peer_id = peer["id"]
390
+ addresses_json = peer["addresses"]
391
+ try:
392
+ addresses = json.loads(addresses_json)
393
+ except:
394
+ addresses = []
395
+
396
+ # Обработка IPv6 link-local: добавить scope_id в адрес
397
+ updated_addresses = []
398
+ for a in addresses:
399
+ proto, hostport = a.split("://")
400
+ host, port = parse_hostport(hostport)
401
+ if is_ipv6(host) and host.startswith("fe80:"):
402
+ scope_id = None
403
+ for iface in netifaces.interfaces():
404
+ iface_addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
405
+ for addr_info in iface_addrs:
406
+ if addr_info.get("addr") == host:
407
+ scope_id = socket.if_nametoindex(iface)
408
+ break
409
+ if scope_id:
410
+ break
411
+ if scope_id:
412
+ host = f"{host}%{scope_id}"
413
+ updated_addresses.append(f"{proto}://{host}:{port}")
414
+ peers_list.append({"id": peer_id, "addresses": updated_addresses})
415
+
416
+ payload = json.dumps(peers_list).encode("utf-8")
417
+ conn.sendall(payload)
418
+ conn.close()
419
+ except Exception as e:
420
+ print(f"[TCP Listener] Connection handling error: {e}")
421
+
422
+ # ---------------------------
423
+ # Запуск потоков
424
+ # ---------------------------
425
+ def start_sync(bootstrap_file="bootstrap.txt"):
426
+ # Загружаем bootstrap-пиров перед запуском discovery/peer exchange
427
+ load_bootstrap_peers(bootstrap_file)
428
+
429
+ # Печать локальных портов для логов
430
+ print(f"[PeerSync] Local ports: {local_ports}")
431
+
432
+ # Запуск потоков
433
+ threading.Thread(target=udp_discovery, daemon=True).start()
434
+ threading.Thread(target=tcp_peer_exchange, daemon=True).start()
435
+ threading.Thread(target=tcp_listener, daemon=True).start()
agents/start_repl.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import sys
2
  import os
3
  import threading
 
1
+ # agent/start_repl.py
2
+
3
  import sys
4
  import os
5
  import threading
agents/tools/storage.py CHANGED
@@ -881,36 +881,36 @@ class Storage:
881
  return None
882
 
883
  # Работа с пирам (agent_peers)
884
- def add_or_update_peer(self, peer_id, name, addresses, source="discovery", status="unknown", pubkey=None, capabilities=None):
 
 
 
 
885
  c = self.conn.cursor()
886
 
887
- # ищем существующий peer по любому совпадающему адресу
888
- c.execute("SELECT id, addresses, pubkey, capabilities FROM agent_peers")
889
- rows = c.fetchall()
890
  existing_id = None
891
  existing_addresses = []
892
  existing_pubkey = None
893
  existing_capabilities = {}
894
 
895
- for row in rows:
896
- db_id, db_addresses_json, db_pubkey, db_capabilities_json = row
897
- try:
898
- db_addresses = json.loads(db_addresses_json)
899
- except Exception:
900
- db_addresses = []
901
-
902
- if any(addr in db_addresses for addr in addresses):
903
- existing_id = db_id
904
- existing_addresses = db_addresses
905
- existing_pubkey = db_pubkey
906
  try:
907
- existing_capabilities = json.loads(db_capabilities_json) if db_capabilities_json else {}
908
  except:
909
  existing_capabilities = {}
910
- break
911
 
912
- combined_addresses = list(set(existing_addresses) | set(addresses))
913
- final_peer_id = existing_id or peer_id
914
  final_pubkey = pubkey or existing_pubkey
915
  final_capabilities = capabilities or existing_capabilities
916
 
@@ -926,7 +926,7 @@ class Storage:
926
  pubkey=excluded.pubkey,
927
  capabilities=excluded.capabilities
928
  """, (
929
- final_peer_id,
930
  name,
931
  json.dumps(combined_addresses),
932
  source,
 
881
  return None
882
 
883
  # Работа с пирам (agent_peers)
884
+ def add_or_update_peer(
885
+ self, peer_id, name, addresses,
886
+ source="discovery", status="unknown",
887
+ pubkey=None, capabilities=None
888
+ ):
889
  c = self.conn.cursor()
890
 
891
+ # нормализация адресов
892
+ addresses = list({a.strip() for a in (addresses or []) if a and a.strip()})
893
+
894
  existing_id = None
895
  existing_addresses = []
896
  existing_pubkey = None
897
  existing_capabilities = {}
898
 
899
+ if peer_id:
900
+ c.execute("SELECT id, addresses, pubkey, capabilities FROM agent_peers WHERE id=?", (peer_id,))
901
+ row = c.fetchone()
902
+ if row:
903
+ existing_id, db_addresses_json, existing_pubkey, db_caps_json = row
904
+ try:
905
+ existing_addresses = json.loads(db_addresses_json) or []
906
+ except:
907
+ existing_addresses = []
 
 
908
  try:
909
+ existing_capabilities = json.loads(db_caps_json) if db_caps_json else {}
910
  except:
911
  existing_capabilities = {}
 
912
 
913
+ combined_addresses = list({*existing_addresses, *addresses})
 
914
  final_pubkey = pubkey or existing_pubkey
915
  final_capabilities = capabilities or existing_capabilities
916
 
 
926
  pubkey=excluded.pubkey,
927
  capabilities=excluded.capabilities
928
  """, (
929
+ peer_id,
930
  name,
931
  json.dumps(combined_addresses),
932
  source,