GitHub Action commited on
Commit
86183a9
·
1 Parent(s): 34c9342

Sync from GitHub with Git LFS

Browse files
Files changed (1) hide show
  1. agents/peer_sync.py +288 -110
agents/peer_sync.py CHANGED
@@ -21,17 +21,30 @@ agent_name = storage.get_config_value("agent_name", "HMP-Agent")
21
  # ======================
22
  def parse_hostport(hostport):
23
  if hostport.startswith("["): # IPv6
24
- match = re.match(r"\[(.+)\]:(\d+)", hostport)
25
- if match:
26
- host = match.group(1)
27
- port = int(match.group(2))
28
- return host, port
29
  else: # IPv4
30
  if ":" in hostport:
31
- host, port = hostport.rsplit(":", 1)
32
- return host, int(port)
33
  return None, None
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # ======================
36
  # Сбор TCP/UDP портов для прослушивания
37
  # ======================
@@ -61,29 +74,75 @@ def get_listening_ports():
61
  tcp_ports, udp_ports = get_listening_ports()
62
 
63
  # ======================
64
- # Получение всех локальных IP
65
  # ======================
66
- def get_all_local_ips():
67
  ips = []
68
  for iface in netifaces.interfaces():
69
  for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
70
- ips.append(addr['addr'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return ips
72
 
73
  # ======================
74
- # Объединённый UDP/LAN Discovery
75
  # ======================
76
- def udp_lan_discovery():
77
- DISCOVERY_INTERVAL = 30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
 
 
 
 
 
79
  local_addresses = storage.get_config_value("local_addresses", [])
 
80
  udp_port_set = set()
81
  for a in local_addresses:
82
- _, port = parse_hostport(a.split("://", 1)[1])
83
- if port:
84
- udp_port_set.add(port)
 
 
 
 
85
 
86
- # Сокеты для приёма
87
  listen_sockets = []
88
  for port in udp_port_set:
89
  try:
@@ -91,62 +150,40 @@ def udp_lan_discovery():
91
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
92
  sock.bind(("", port))
93
  listen_sockets.append(sock)
 
94
  except Exception as e:
95
- print(f"[UDP/LAN Discovery] Не удалось создать сокет на порту {port}: {e}")
96
 
97
  send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
98
  send_sock.settimeout(0.5)
99
 
100
  while True:
101
- # ------------------ Приём ------------------
102
  for sock in listen_sockets:
103
  try:
104
- sock.settimeout(0.1)
105
- data, addr = sock.recvfrom(2048)
106
  msg = json.loads(data.decode("utf-8"))
107
  peer_id = msg.get("id")
108
- if peer_id == my_id:
109
  continue
110
-
111
  name = msg.get("name", "unknown")
112
- addresses = msg.get("addresses", [f"{addr[0]}:{sock.getsockname()[1]}"])
113
- expanded_addresses = []
114
-
115
- for a in addresses:
116
- norm = storage.normalize_address(a)
117
- if norm is None:
118
- continue
119
- proto, rest = norm.split("://", 1)
120
- host_port = rest.split(":")[0]
121
- if host_port.startswith("127.") or host_port.startswith("0."):
122
- continue
123
- if proto in ["udp", "any"]:
124
- expanded_addresses.append(f"udp://{rest}")
125
- expanded_addresses.append(f"tcp://{rest}")
126
- elif proto == "tcp":
127
- expanded_addresses.append(f"tcp://{rest}")
128
-
129
- if expanded_addresses:
130
- print(f"[UDP/LAN Discovery] получен пакет от {addr} через {sock.getsockname()}, "
131
- f"id={peer_id}, addresses={expanded_addresses}")
132
- storage.add_or_update_peer(peer_id, name, expanded_addresses, "discovery", "online")
133
  except socket.timeout:
134
- continue
135
  except Exception as e:
136
- print(f"[UDP/LAN Discovery] ошибка при обработке пакета: {e}")
137
- continue
138
 
139
- # ------------------ Отправка ------------------
140
- local_ips = get_all_local_ips()
141
- msg_data = json.dumps({
142
- "id": my_id,
143
- "name": agent_name,
144
- "addresses": local_addresses
145
- }).encode("utf-8")
146
-
147
- for local_ip in local_ips:
148
  try:
149
- net = ipaddress.ip_network(local_ip + '/24', strict=False)
150
  except Exception:
151
  continue
152
  for ip in net.hosts():
@@ -156,17 +193,96 @@ def udp_lan_discovery():
156
  for port in udp_port_set:
157
  try:
158
  send_sock.sendto(msg_data, (ip_str, port))
159
- except Exception:
160
- continue
161
 
162
  time.sleep(DISCOVERY_INTERVAL)
163
 
164
  # ======================
165
- # UDP Discovery Sender (для global_addresses)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  # ======================
167
  def udp_discovery_sender():
168
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
169
- sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
 
 
 
 
 
 
 
170
 
171
  global_addresses = storage.get_config_value("global_addresses", [])
172
  msg = {"id": my_id, "name": agent_name, "addresses": global_addresses}
@@ -185,30 +301,52 @@ def udp_discovery_sender():
185
  now = time.time()
186
  if int(now) % DISCOVERY_INTERVAL == 0:
187
  for port in udp_port_set:
188
- sock.sendto(json.dumps(msg).encode("utf-8"), ("239.255.0.1", port))
 
 
 
 
 
 
 
 
 
189
  if now - last_broadcast > BROADCAST_INTERVAL:
190
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
191
- for port in udp_port_set:
192
- sock.sendto(json.dumps(msg).encode("utf-8"), ("255.255.255.255", port))
 
 
 
193
  last_broadcast = now
 
194
  time.sleep(1)
195
 
196
  # ======================
197
- # TCP Listener (Peer Exchange)
198
  # ======================
199
  def tcp_listener():
200
  sockets = []
201
  for host, port in tcp_ports:
202
  try:
203
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
204
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
205
- bind_host = "" if host in ["0.0.0.0", "any"] else host
206
- sock.bind((bind_host, port))
207
- sock.listen(5)
208
- sockets.append(sock)
209
- print(f"[TCP Listener] Слушаем на {bind_host}:{port}")
 
 
 
 
 
 
 
 
 
210
  except Exception as e:
211
- print(f"[TCP Listener] Ошибка bind/listen {host}:{port} -> {e}")
212
 
213
  while True:
214
  if not sockets:
@@ -218,9 +356,15 @@ def tcp_listener():
218
  for s in readable:
219
  try:
220
  conn, addr = s.accept()
221
- data = conn.recv(1024)
 
 
 
 
 
 
222
  if data == b"PEER_EXCHANGE_REQUEST":
223
- print(f"[TCP Listener] Получен PEER_EXCHANGE_REQUEST от {addr}")
224
  try:
225
  peers = []
226
  for pid, addresses_json in storage.get_online_peers(limit=50):
@@ -228,54 +372,86 @@ def tcp_listener():
228
  addresses = json.loads(addresses_json)
229
  except Exception:
230
  addresses = []
231
- peers.append({
232
- "id": pid,
233
- "addresses": addresses
234
- })
235
  payload = json.dumps(peers).encode("utf-8")
236
  conn.sendall(payload)
237
- print(f"[TCP Listener] Отправлен список пиров ({len(peers)}) в {addr}")
238
  except Exception as e:
239
- print(f"[TCP Listener] Ошибка при отправке списка пиров: {e}")
240
  conn.close()
241
  except Exception as e:
242
- print(f"[TCP Listener] Ошибка при обработке соединения: {e}")
243
- continue
244
 
245
  # ======================
246
- # Peer Exchange (TCP)
247
  # ======================
248
  def peer_exchange():
249
- PEER_EXCHANGE_INTERVAL = 120
250
  while True:
251
- peers = storage.get_online_peers(limit=50)
252
- for peer in peers:
253
- peer_id, addresses = peer["id"], peer["addresses"]
254
- if peer_id == my_id:
255
  continue
256
-
257
  try:
258
- addr_list = json.loads(addresses)
259
- for addr in addr_list:
260
- norm = storage.normalize_address(addr)
261
- if norm is None:
262
- continue
263
- proto, hostport = norm.split("://")
264
- if proto not in ["tcp", "any"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  continue
266
 
267
  try:
268
- host, port = parse_hostport(hostport)
269
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
270
- s.settimeout(2)
271
- s.connect((host, port))
272
- s.sendall(b"PEER_EXCHANGE_REQUEST")
273
- s.close()
274
- break
275
- except:
276
  continue
277
- except:
278
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  time.sleep(PEER_EXCHANGE_INTERVAL)
280
 
281
  # ======================
@@ -284,7 +460,9 @@ def peer_exchange():
284
  def start_sync():
285
  print("[PeerSync] Запуск фоновой синхронизации")
286
  storage.load_bootstrap()
287
- threading.Thread(target=udp_lan_discovery, daemon=True).start()
 
 
288
  threading.Thread(target=udp_discovery_sender, daemon=True).start()
289
  threading.Thread(target=peer_exchange, daemon=True).start()
290
  threading.Thread(target=tcp_listener, daemon=True).start()
 
21
  # ======================
22
  def parse_hostport(hostport):
23
  if hostport.startswith("["): # IPv6
24
+ m = re.match(r"\[(.+)\]:(\d+)$", hostport)
25
+ if m:
26
+ return m.group(1), int(m.group(2))
 
 
27
  else: # IPv4
28
  if ":" in hostport:
29
+ h, p = hostport.rsplit(":", 1)
30
+ return h, int(p)
31
  return None, None
32
 
33
+ def is_ipv6(host: str) -> bool:
34
+ return ":" in host
35
+
36
+ def is_loopback(host: str) -> bool:
37
+ try:
38
+ # IPv4 127.0.0.0/8
39
+ if re.match(r"^127\.\d+\.\d+\.\d+$", host):
40
+ return True
41
+ # IPv6 ::1
42
+ if host == "::1":
43
+ return True
44
+ except:
45
+ pass
46
+ return False
47
+
48
  # ======================
49
  # Сбор TCP/UDP портов для прослушивания
50
  # ======================
 
74
  tcp_ports, udp_ports = get_listening_ports()
75
 
76
  # ======================
77
+ # Локальные IP (IPv4 + IPv6 link-local/global)
78
  # ======================
79
+ def get_all_local_ips_v4():
80
  ips = []
81
  for iface in netifaces.interfaces():
82
  for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
83
+ ip = addr.get("addr")
84
+ if ip and not ip.startswith("127.") and not ip.startswith("0."):
85
+ ips.append(ip)
86
+ return ips
87
+
88
+ def get_all_local_ips_v6():
89
+ ips = []
90
+ for iface in netifaces.interfaces():
91
+ for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET6, []):
92
+ ip = addr.get("addr")
93
+ # обрезаем суффикс %ifname у Windows/Linux
94
+ if ip:
95
+ ip = ip.split("%")[0]
96
+ if ip != "::1":
97
+ ips.append(ip)
98
  return ips
99
 
100
  # ======================
101
+ # Нормализация/расширение адресов для БД
102
  # ======================
103
+ def expand_and_filter(addresses):
104
+ out = []
105
+ seen = set()
106
+ for a in addresses:
107
+ norm = storage.normalize_address(a)
108
+ if not norm:
109
+ continue
110
+ try:
111
+ proto, rest = norm.split("://", 1)
112
+ except:
113
+ continue
114
+ host, _port = parse_hostport(rest)
115
+ if not host or is_loopback(host):
116
+ continue
117
+
118
+ if proto == "tcp":
119
+ cand = f"tcp://{rest}"
120
+ if cand not in seen:
121
+ out.append(cand); seen.add(cand)
122
+ elif proto in ["udp", "utp", "any"]:
123
+ for cand in (f"udp://{rest}", f"tcp://{rest}"):
124
+ if cand not in seen:
125
+ out.append(cand); seen.add(cand)
126
+ return out
127
 
128
+ # ======================
129
+ # IPv4 UDP LAN Discovery (приём + рассылка по /24)
130
+ # ======================
131
+ def udp_lan_discovery_v4():
132
+ DISCOVERY_INTERVAL = 30
133
  local_addresses = storage.get_config_value("local_addresses", [])
134
+ # порты, на которые слать/listen
135
  udp_port_set = set()
136
  for a in local_addresses:
137
+ try:
138
+ _proto, hostport = a.split("://", 1)
139
+ _h, p = parse_hostport(hostport)
140
+ if p:
141
+ udp_port_set.add(p)
142
+ except:
143
+ continue
144
 
145
+ # слушатели IPv4
146
  listen_sockets = []
147
  for port in udp_port_set:
148
  try:
 
150
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
151
  sock.bind(("", port))
152
  listen_sockets.append(sock)
153
+ print(f"[UDPv4] Слушаем UDP на 0.0.0.0:{port}")
154
  except Exception as e:
155
+ print(f"[UDPv4] Не удалось слушать порт {port}: {e}")
156
 
157
  send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
158
  send_sock.settimeout(0.5)
159
 
160
  while True:
161
+ # --- приём ---
162
  for sock in listen_sockets:
163
  try:
164
+ sock.settimeout(0.05)
165
+ data, addr = sock.recvfrom(4096)
166
  msg = json.loads(data.decode("utf-8"))
167
  peer_id = msg.get("id")
168
+ if not peer_id or peer_id == my_id:
169
  continue
 
170
  name = msg.get("name", "unknown")
171
+ addresses = msg.get("addresses", [])
172
+ expanded = expand_and_filter(addresses)
173
+ if expanded:
174
+ print(f"[UDPv4] RX от {addr} -> {expanded}")
175
+ storage.add_or_update_peer(peer_id, name, expanded, "discovery", "online")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  except socket.timeout:
177
+ pass
178
  except Exception as e:
179
+ print(f"[UDPv4] Ошибка при обработке пакета: {e}")
 
180
 
181
+ # --- рассылка ---
182
+ v4s = get_all_local_ips_v4()
183
+ msg_data = json.dumps({"id": my_id, "name": agent_name, "addresses": local_addresses}).encode("utf-8")
184
+ for local_ip in v4s:
 
 
 
 
 
185
  try:
186
+ net = ipaddress.ip_network(local_ip + "/24", strict=False)
187
  except Exception:
188
  continue
189
  for ip in net.hosts():
 
193
  for port in udp_port_set:
194
  try:
195
  send_sock.sendto(msg_data, (ip_str, port))
196
+ except:
197
+ pass
198
 
199
  time.sleep(DISCOVERY_INTERVAL)
200
 
201
  # ======================
202
+ # IPv6 UDP Discovery (приём + мультикаст ff02::1)
203
+ # ======================
204
+ def udp_lan_discovery_v6():
205
+ DISCOVERY_INTERVAL = 30
206
+ local_addresses = storage.get_config_value("local_addresses", [])
207
+ udp_port_set = set()
208
+ for a in local_addresses:
209
+ try:
210
+ _proto, hostport = a.split("://", 1)
211
+ h, p = parse_hostport(hostport)
212
+ if p:
213
+ udp_port_set.add(p)
214
+ except:
215
+ continue
216
+
217
+ # слушатели IPv6
218
+ listen_sockets = []
219
+ for port in udp_port_set:
220
+ try:
221
+ sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
222
+ sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
223
+ sock6.bind(("::", port))
224
+ listen_sockets.append(sock6)
225
+ print(f"[UDPv6] Слушаем UDP на [::]:{port}")
226
+ except Exception as e:
227
+ print(f"[UDPv6] Не удалось слушать порт {port}: {e}")
228
+
229
+ # отправитель IPv6 (мультикаст link-local all-nodes)
230
+ try:
231
+ send_sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
232
+ # hop limit 1
233
+ send_sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
234
+ except Exception as e:
235
+ print(f"[UDPv6] Не удалось создать отправитель: {e}")
236
+ send_sock6 = None
237
+
238
+ maddr = "ff02::1" # all-nodes on-link
239
+ msg_data = json.dumps({"id": my_id, "name": agent_name, "addresses": local_addresses}).encode("utf-8")
240
+
241
+ while True:
242
+ # --- приём ---
243
+ for sock in listen_sockets:
244
+ try:
245
+ sock.settimeout(0.05)
246
+ data, addr = sock.recvfrom(8192)
247
+ msg = json.loads(data.decode("utf-8"))
248
+ peer_id = msg.get("id")
249
+ if not peer_id or peer_id == my_id:
250
+ continue
251
+ name = msg.get("name", "unknown")
252
+ addresses = msg.get("addresses", [])
253
+ expanded = expand_and_filter(addresses)
254
+ if expanded:
255
+ print(f"[UDPv6] RX от {addr} -> {expanded}")
256
+ storage.add_or_update_peer(peer_id, name, expanded, "discovery", "online")
257
+ except socket.timeout:
258
+ pass
259
+ except Exception as e:
260
+ print(f"[UDPv6] Ошибка при обработке пакета: {e}")
261
+
262
+ # --- мультикаст рассылка ---
263
+ if send_sock6:
264
+ for port in udp_port_set:
265
+ try:
266
+ # iface scope index не задаём — ОС выберет подходящий on-link интерфейс
267
+ send_sock6.sendto(msg_data, (maddr, port))
268
+ except:
269
+ pass
270
+
271
+ time.sleep(DISCOVERY_INTERVAL)
272
+
273
+ # ======================
274
+ # UDP Discovery Sender (глобальный v4/v6 мультикаст/бродкаст как было)
275
  # ======================
276
  def udp_discovery_sender():
277
+ # IPv4 multicast
278
+ sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
279
+ sock4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
280
+ # IPv6 multicast
281
+ try:
282
+ sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
283
+ sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 2)
284
+ except Exception:
285
+ sock6 = None
286
 
287
  global_addresses = storage.get_config_value("global_addresses", [])
288
  msg = {"id": my_id, "name": agent_name, "addresses": global_addresses}
 
301
  now = time.time()
302
  if int(now) % DISCOVERY_INTERVAL == 0:
303
  for port in udp_port_set:
304
+ try:
305
+ sock4.sendto(json.dumps(msg).encode("utf-8"), ("239.255.0.1", port))
306
+ except:
307
+ pass
308
+ if sock6:
309
+ try:
310
+ sock6.sendto(json.dumps(msg).encode("utf-8"), ("ff02::1", port))
311
+ except:
312
+ pass
313
+
314
  if now - last_broadcast > BROADCAST_INTERVAL:
315
+ try:
316
+ sock4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
317
+ for port in udp_port_set:
318
+ sock4.sendto(json.dumps(msg).encode("utf-8"), ("255.255.255.255", port))
319
+ except:
320
+ pass
321
  last_broadcast = now
322
+
323
  time.sleep(1)
324
 
325
  # ======================
326
+ # TCP Listener (v4+v6) для Peer Exchange
327
  # ======================
328
  def tcp_listener():
329
  sockets = []
330
  for host, port in tcp_ports:
331
  try:
332
+ if is_ipv6(host):
333
+ s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
334
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
335
+ bind_host = "::" if host in ["::", "any", "0.0.0.0"] else host
336
+ s.bind((bind_host, port))
337
+ s.listen(5)
338
+ sockets.append(s)
339
+ print(f"[TCP] Слушаем на [{bind_host}]:{port}")
340
+ else:
341
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
342
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
343
+ bind_host = "" if host in ["0.0.0.0", "any"] else host
344
+ s.bind((bind_host, port))
345
+ s.listen(5)
346
+ sockets.append(s)
347
+ print(f"[TCP] Слушаем на {bind_host}:{port}")
348
  except Exception as e:
349
+ print(f"[TCP] Ошибка bind/listen {host}:{port} -> {e}")
350
 
351
  while True:
352
  if not sockets:
 
356
  for s in readable:
357
  try:
358
  conn, addr = s.accept()
359
+ data = b""
360
+ try:
361
+ chunk = conn.recv(1024)
362
+ data = chunk or b""
363
+ except:
364
+ data = b""
365
+
366
  if data == b"PEER_EXCHANGE_REQUEST":
367
+ print(f"[TCP] PEER_EXCHANGE_REQUEST от {addr}")
368
  try:
369
  peers = []
370
  for pid, addresses_json in storage.get_online_peers(limit=50):
 
372
  addresses = json.loads(addresses_json)
373
  except Exception:
374
  addresses = []
375
+ # фильтруем/расширяем перед отдачей
376
+ addresses = expand_and_filter(addresses)
377
+ peers.append({"id": pid, "addresses": addresses})
 
378
  payload = json.dumps(peers).encode("utf-8")
379
  conn.sendall(payload)
380
+ print(f"[TCP] Отправлен список пиров ({len(peers)}) в {addr}")
381
  except Exception as e:
382
+ print(f"[TCP] Ошибка при отправке списка пиров: {e}")
383
  conn.close()
384
  except Exception as e:
385
+ print(f"[TCP] Ошибка при обработке соединения: {e}")
 
386
 
387
  # ======================
388
+ # Peer Exchange (TCP инициатор, читает ответ и пишет в БД)
389
  # ======================
390
  def peer_exchange():
391
+ PEER_EXCHANGE_INTERVAL = 30 # почаще, чтобы быстрее раскидать пиров
392
  while True:
393
+ rows = storage.get_online_peers(limit=50)
394
+ for pid, addresses_json in rows:
395
+ if pid == my_id:
 
396
  continue
 
397
  try:
398
+ addr_list = json.loads(addresses_json)
399
+ except:
400
+ addr_list = []
401
+ for addr in addr_list:
402
+ norm = storage.normalize_address(addr)
403
+ if not norm:
404
+ continue
405
+ proto, hostport = norm.split("://", 1)
406
+ if proto not in ["tcp", "any"]:
407
+ continue
408
+ host, port = parse_hostport(hostport)
409
+ if not host or not port or is_loopback(host):
410
+ continue
411
+
412
+ try:
413
+ family = socket.AF_INET6 if is_ipv6(host) else socket.AF_INET
414
+ s = socket.socket(family, socket.SOCK_STREAM)
415
+ s.settimeout(3)
416
+ # IPv6 connect tuple принимает (host, port, flowinfo, scope_id), но flow/scope 0 по умолчанию ок
417
+ s.connect((host, port))
418
+ s.sendall(b"PEER_EXCHANGE_REQUEST")
419
+
420
+ # читаем весь ответ (до закрытия сокета/таймаута)
421
+ chunks = []
422
+ try:
423
+ while True:
424
+ b = s.recv(8192)
425
+ if not b:
426
+ break
427
+ chunks.append(b)
428
+ except socket.timeout:
429
+ pass
430
+ s.close()
431
+
432
+ if not chunks:
433
  continue
434
 
435
  try:
436
+ peers_list = json.loads(b"".join(chunks).decode("utf-8"))
437
+ except Exception as e:
438
+ print(f"[PeerExchange] ошибка парсинга ответа: {e}")
 
 
 
 
 
439
  continue
440
+
441
+ for p in peers_list:
442
+ p_id = p.get("id")
443
+ if not p_id or p_id == my_id:
444
+ continue
445
+ addrs = expand_and_filter(p.get("addresses", []))
446
+ if addrs:
447
+ storage.add_or_update_peer(p_id, "unknown", addrs, "exchange", "online")
448
+ print(f"[PeerExchange] получили {p_id} -> {addrs}")
449
+
450
+ # если успешно отработали по одному TCP-адресу — дальше по этому пиру можно не перебирать
451
+ break
452
+ except Exception as e:
453
+ # пробуем следующий адрес пира
454
+ continue
455
  time.sleep(PEER_EXCHANGE_INTERVAL)
456
 
457
  # ======================
 
460
  def start_sync():
461
  print("[PeerSync] Запуск фоновой синхронизации")
462
  storage.load_bootstrap()
463
+
464
+ threading.Thread(target=udp_lan_discovery_v4, daemon=True).start()
465
+ threading.Thread(target=udp_lan_discovery_v6, daemon=True).start()
466
  threading.Thread(target=udp_discovery_sender, daemon=True).start()
467
  threading.Thread(target=peer_exchange, daemon=True).start()
468
  threading.Thread(target=tcp_listener, daemon=True).start()