| | |
| |
|
| | import socket |
| | import json |
| | import time |
| | import threading |
| | import select |
| | import netifaces |
| | import ipaddress |
| |
|
| | from datetime import datetime, timezone |
| | from tools.storage import Storage |
| |
|
| | UTC = timezone.utc |
| |
|
| | storage = Storage() |
| |
|
| | |
| | |
| | |
| | my_id = storage.get_config_value("agent_id") |
| | my_pubkey = storage.get_config_value("pubkey") |
| | agent_name = storage.get_config_value("agent_name", "unknown") |
| |
|
| | local_addresses = storage.get_addresses("local") |
| | global_addresses = storage.get_addresses("global") |
| | all_addresses = local_addresses + global_addresses |
| |
|
| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | def load_bootstrap_peers(filename="bootstrap.txt"): |
| | try: |
| | with open(filename, "r", encoding="utf-8") as f: |
| | lines = f.readlines() |
| | except FileNotFoundError: |
| | print(f"[Bootstrap] File {filename} not found") |
| | return |
| |
|
| | for line in lines: |
| | line = line.strip() |
| | if not line or line.startswith("#"): |
| | continue |
| |
|
| | |
| | parts = [p.strip() for p in line.split(";") if p.strip()] |
| | data = {} |
| | for part in parts: |
| | if ":" not in part: |
| | continue |
| | key, val = part.split(":", 1) |
| | key = key.strip().upper() |
| | val = val.strip() |
| | if val.startswith('"') and val.endswith('"'): |
| | val = val[1:-1].replace("\\n", "\n") |
| | data[key] = val |
| |
|
| | |
| | did = data.get("DID") |
| | pubkey = data.get("KEY") |
| | addresses_json = data.get("ADDRESS") |
| | name = data.get("NAME") |
| |
|
| | if not did or not pubkey or not addresses_json: |
| | print(f"[Bootstrap] Missing required fields in line: {line}") |
| | continue |
| |
|
| | |
| | try: |
| | addresses = json.loads(addresses_json) |
| | except Exception as e: |
| | print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})") |
| | continue |
| |
|
| | |
| | expanded_addresses = [] |
| | for addr in addresses: |
| | if isinstance(addr, dict): |
| | |
| | if "address" in addr: |
| | addr_str = addr["address"] |
| | if addr_str.startswith("any://"): |
| | hostport = addr_str[len("any://"):] |
| | variants = [f"tcp://{hostport}", f"udp://{hostport}"] |
| | else: |
| | variants = [addr_str] |
| |
|
| | for v in variants: |
| | expanded_addresses.append({ |
| | "addr": v, |
| | "nonce": addr.get("pow", {}).get("nonce"), |
| | "pow_hash": addr.get("pow", {}).get("hash"), |
| | "difficulty": addr.get("pow", {}).get("difficulty"), |
| | "datetime": addr.get("datetime", "") |
| | }) |
| | |
| | elif "addr" in addr: |
| | expanded_addresses.append(addr) |
| |
|
| | elif isinstance(addr, str): |
| | if addr.startswith("any://"): |
| | hostport = addr[len("any://"):] |
| | expanded_addresses.extend([ |
| | {"addr": f"tcp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""}, |
| | {"addr": f"udp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""} |
| | ]) |
| | else: |
| | expanded_addresses.append({ |
| | "addr": addr, |
| | "nonce": None, |
| | "pow_hash": None, |
| | "difficulty": None, |
| | "datetime": "" |
| | }) |
| |
|
| | |
| | print(f"[DEBUG] Saving peer {did} with addresses:") |
| | for a in expanded_addresses: |
| | print(a) |
| | storage.add_or_update_peer( |
| | peer_id=did, |
| | name=name, |
| | addresses=expanded_addresses, |
| | source="bootstrap", |
| | status="offline", |
| | pubkey=pubkey, |
| | capabilities=None, |
| | heard_from=None, |
| | strict=False |
| | ) |
| |
|
| | print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}") |
| |
|
| | |
| | |
| | |
| | def start_peer_services(port): |
| | """Запускаем UDP и TCP слушатели на всех интерфейсах сразу""" |
| |
|
| | |
| | udp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) |
| | udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| | udp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) |
| | udp_sock.bind(("::", port)) |
| | print(f"[UDP Discovery] Listening on [::]:{port} (IPv4+IPv6)") |
| |
|
| | |
| | tcp_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
| | tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| | tcp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) |
| | tcp_sock.bind(("::", port)) |
| | tcp_sock.listen() |
| | print(f"[TCP Listener] Listening on [::]:{port} (IPv4+IPv6)") |
| |
|
| | return udp_sock, tcp_sock |
| |
|
| | |
| | |
| | |
| | def udp_discovery(sock, local_ports): |
| | """Приём и рассылка discovery через один сокет (IPv4+IPv6).""" |
| | DISCOVERY_INTERVAL = 30 |
| | MAX_PACKET_SIZE = 1200 |
| | chunks_buffer = {} |
| |
|
| | def send_discovery_packets(msg_dict, dest, port): |
| | """Разбивка JSON на чанки и отправка по UDP.""" |
| | msg_json = json.dumps(msg_dict) |
| | chunks = [msg_json[i:i + MAX_PACKET_SIZE] for i in range(0, len(msg_json), MAX_PACKET_SIZE)] |
| | total = len(chunks) |
| | for idx, chunk in enumerate(chunks): |
| | pkt = json.dumps({ |
| | "chunk": idx, |
| | "total": total, |
| | "data": chunk |
| | }).encode("utf-8") |
| | try: |
| | if ":" not in dest: |
| | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) |
| | else: |
| | s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) |
| |
|
| | s.sendto(pkt, (dest, port)) |
| | s.close() |
| | except Exception as e: |
| | print(f"[UDP Discovery] send error to {dest}:{port} -> {e}") |
| |
|
| | while True: |
| | |
| | try: |
| | rlist, _, _ = select.select([sock], [], [], 0.5) |
| | for s in rlist: |
| | try: |
| | data, addr = s.recvfrom(4096) |
| | pkt = json.loads(data.decode("utf-8")) |
| |
|
| | if "chunk" in pkt and "total" in pkt and "data" in pkt: |
| | buf = chunks_buffer.setdefault(addr, {"chunks": {}, "total": pkt["total"]}) |
| | buf["chunks"][pkt["chunk"]] = pkt["data"] |
| | if len(buf["chunks"]) == buf["total"]: |
| | full_msg_json = "".join(buf["chunks"][i] for i in sorted(buf["chunks"])) |
| | msg = json.loads(full_msg_json) |
| | print(f"[UDP Discovery] received full msg (with pubkey={bool(msg.get('pubkey'))}) from {addr}") |
| | del chunks_buffer[addr] |
| | else: |
| | continue |
| | else: |
| | msg = pkt |
| | print(f"[UDP Discovery] received short msg (with pubkey={bool(msg.get('pubkey'))}) from {addr}") |
| |
|
| | peer_id = msg.get("id") |
| | if peer_id == my_id: |
| | continue |
| | name = msg.get("name", "unknown") |
| | raw_addresses = msg.get("addresses", []) |
| | pubkey = msg.get("pubkey") |
| |
|
| | addresses = [] |
| | for a in raw_addresses: |
| | if isinstance(a, dict) and "addr" in a: |
| | addresses.append({ |
| | "addr": storage.normalize_address(a["addr"]), |
| | "nonce": a.get("nonce"), |
| | "pow_hash": a.get("pow_hash"), |
| | "datetime": a.get("datetime") |
| | }) |
| | elif isinstance(a, str): |
| | addresses.append({ |
| | "addr": storage.normalize_address(a), |
| | "nonce": None, |
| | "pow_hash": None, |
| | "datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat() |
| | }) |
| |
|
| | storage.add_or_update_peer( |
| | peer_id, name, addresses, |
| | source="discovery", status="online", |
| | pubkey=pubkey, strict=False |
| | ) |
| | print(f"[UDP Discovery] peer={peer_id} from {addr} (pubkey={bool(pubkey)})") |
| | except Exception as e: |
| | print(f"[UDP Discovery] receive error: {e}") |
| | except Exception as e: |
| | print(f"[UDP Discovery] select() error: {e}") |
| |
|
| | |
| | print("[UDP Discovery] Interfaces:") |
| | for iface in netifaces.interfaces(): |
| | addrs = netifaces.ifaddresses(iface) |
| | ipv4_list = addrs.get(netifaces.AF_INET, []) |
| | ipv6_list = addrs.get(netifaces.AF_INET6, []) |
| | try: |
| | if_idx = socket.if_nametoindex(iface) |
| | except Exception: |
| | if_idx = None |
| | print(f" {iface} (idx={if_idx}) - IPv4: {ipv4_list}, IPv6: {ipv6_list}") |
| |
|
| | |
| | local_addresses = [] |
| | for iface in netifaces.interfaces(): |
| | for a in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []): |
| | ip = a.get("addr") |
| | if ip: |
| | local_addresses.append({ |
| | "addr": storage.normalize_address(f"any://{ip}:{local_ports[0]}"), |
| | "nonce": 0, |
| | "pow_hash": "0"*64, |
| | "datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat() |
| | }) |
| |
|
| | |
| | peers = storage.get_known_peers(my_id) |
| | print("[UDP Discovery] Known peers:", [p["id"] for p in peers]) |
| |
|
| | msg_dict = { |
| | "id": my_id, |
| | "name": agent_name, |
| | "addresses": local_addresses, |
| | "pubkey": my_pubkey |
| | } |
| | print(f"[UDP Discovery] sending msg (with pubkey={bool(my_pubkey)}): {msg_dict}") |
| |
|
| | for port in local_ports: |
| | |
| | for iface in netifaces.interfaces(): |
| | addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, []) |
| | for a in addrs: |
| | if "broadcast" in a: |
| | send_discovery_packets(msg_dict, a["broadcast"], port) |
| | send_discovery_packets(msg_dict, "255.255.255.255", port) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | time.sleep(DISCOVERY_INTERVAL) |
| |
|
| | |
| | |
| | |
| | def tcp_peer_exchange(): |
| | PEER_EXCHANGE_INTERVAL = 20 |
| | while True: |
| | peers = storage.get_known_peers(my_id, limit=50) |
| | print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...") |
| |
|
| | for peer in peers: |
| | |
| | if not isinstance(peer, dict): |
| | peer = dict(peer) |
| |
|
| | peer_id = peer.get("id") |
| | if peer_id == my_id: |
| | continue |
| |
|
| | try: |
| | addr_list = json.loads(peer.get("addresses", "[]")) |
| | except Exception as e: |
| | print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}") |
| | addr_list = [] |
| |
|
| | for addr in addr_list: |
| | norm = storage.normalize_address(addr) |
| | if not norm: |
| | continue |
| | proto, hostport = norm.split("://", 1) |
| | if proto not in ["tcp", "any"]: |
| | continue |
| | host, port = storage.parse_hostport(hostport) |
| | if not host or not port: |
| | continue |
| |
|
| | print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})") |
| | try: |
| | sock = socket.socket( |
| | socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET, |
| | socket.SOCK_STREAM |
| | ) |
| | sock.settimeout(3) |
| | sock.connect((host, port)) |
| |
|
| | |
| | if storage.is_private(host): |
| | send_addresses = all_addresses |
| | else: |
| | send_addresses = [ |
| | a for a in all_addresses |
| | if not storage.is_private(storage.parse_hostport(a.split("://", 1)[1])[0]) |
| | ] |
| |
|
| | handshake = { |
| | "type": "PEER_EXCHANGE_REQUEST", |
| | "id": my_id, |
| | "name": agent_name, |
| | "addresses": send_addresses, |
| | } |
| | sock.sendall(json.dumps(handshake).encode("utf-8")) |
| |
|
| | data = sock.recv(64 * 1024) |
| | sock.close() |
| |
|
| | if not data: |
| | print(f"[PeerExchange] No data from {host}:{port}") |
| | continue |
| |
|
| | try: |
| | peers_recv = json.loads(data.decode("utf-8")) |
| | for p in peers_recv: |
| | if p.get("id") and p["id"] != my_id: |
| | storage.add_or_update_peer( |
| | p["id"], |
| | p.get("name", "unknown"), |
| | p.get("addresses", []), |
| | "peer_exchange", |
| | "online", |
| | strict=False |
| | ) |
| | print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}") |
| | except Exception as e: |
| | print(f"[PeerExchange] Decode error from {host}:{port} -> {e}") |
| | continue |
| |
|
| | break |
| | except Exception as e: |
| | print(f"[PeerExchange] Connection to {host}:{port} failed: {e}") |
| | continue |
| |
|
| | time.sleep(PEER_EXCHANGE_INTERVAL) |
| |
|
| | |
| | |
| | |
| | def tcp_listener(sock): |
| | """Слушатель TCP (один сокет на IPv6, работает и для IPv4).""" |
| | while True: |
| | try: |
| | rlist, _, _ = select.select([sock], [], [], 1) |
| | for s in rlist: |
| | try: |
| | conn, addr = s.accept() |
| | data = conn.recv(64 * 1024) |
| | if not data: |
| | conn.close() |
| | continue |
| |
|
| | try: |
| | msg = json.loads(data.decode("utf-8")) |
| | except Exception as e: |
| | print(f"[TCP Listener] JSON decode error from {addr}: {e}") |
| | conn.close() |
| | continue |
| |
|
| | if msg.get("type") == "PEER_EXCHANGE_REQUEST": |
| | peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}" |
| | peer_name = msg.get("name", "unknown") |
| | raw_addrs = msg.get("addresses", []) |
| | pubkey = msg.get("pubkey") |
| |
|
| | |
| | addresses = [] |
| | for a in raw_addrs: |
| | if isinstance(a, dict) and "addr" in a: |
| | addresses.append({ |
| | "addr": storage.normalize_address(a["addr"]), |
| | "nonce": a.get("nonce"), |
| | "pow_hash": a.get("pow_hash"), |
| | "datetime": a.get("datetime") |
| | }) |
| | elif isinstance(a, str): |
| | addresses.append({ |
| | "addr": storage.normalize_address(a), |
| | "nonce": None, |
| | "pow_hash": None, |
| | "datetime": datetime.now(UTC).replace(microsecond=0).isoformat() |
| | }) |
| |
|
| | storage.add_or_update_peer( |
| | peer_id, peer_name, addresses, |
| | source="incoming", status="online", |
| | pubkey=pubkey, strict=False |
| | ) |
| | print(f"[TCP Listener] Handshake from {peer_id} ({addr})") |
| |
|
| | |
| | is_lan = storage.is_private(addr[0]) |
| |
|
| | |
| | peers_list = [] |
| | for peer in storage.get_known_peers(my_id, limit=50): |
| | pid = peer["id"] |
| | try: |
| | peer_addrs = json.loads(peer.get("addresses", "[]")) |
| | except: |
| | peer_addrs = [] |
| |
|
| | updated_addresses = [] |
| | for a in peer_addrs: |
| | |
| | addr_norm = storage.normalize_address(a.get("addr") if isinstance(a, dict) else a) |
| | if not addr_norm: |
| | continue |
| | proto, hostport = addr_norm.split("://", 1) |
| | host, port = storage.parse_hostport(hostport) |
| |
|
| | |
| | if not is_lan and storage.is_private(host): |
| | continue |
| |
|
| | updated_addresses.append({ |
| | "addr": f"{proto}://{host}:{port}", |
| | "nonce": a.get("nonce") if isinstance(a, dict) else None, |
| | "pow_hash": a.get("pow_hash") if isinstance(a, dict) else None, |
| | "datetime": a.get("datetime") if isinstance(a, dict) else None |
| | }) |
| |
|
| | peers_list.append({ |
| | "id": pid, |
| | "addresses": updated_addresses, |
| | "pubkey": peer.get("pubkey") |
| | }) |
| |
|
| | conn.sendall(json.dumps(peers_list).encode("utf-8")) |
| |
|
| | conn.close() |
| | except Exception as e: |
| | print(f"[TCP Listener] Connection handling error: {e}") |
| | except Exception as e: |
| | print(f"[TCP Listener] select() error: {e}") |
| |
|
| | |
| | |
| | |
| | def start_sync(bootstrap_file="bootstrap.txt"): |
| | load_bootstrap_peers(bootstrap_file) |
| |
|
| | local_ports = list(set(storage.get_local_ports())) |
| | print(f"[PeerSync] Local ports: {local_ports}") |
| |
|
| | for port in local_ports: |
| | udp_sock, tcp_sock = start_peer_services(port) |
| |
|
| | threading.Thread(target=udp_discovery, args=(udp_sock, local_ports), daemon=True).start() |
| | threading.Thread(target=tcp_listener, args=(tcp_sock,), daemon=True).start() |
| |
|
| | threading.Thread(target=tcp_peer_exchange, daemon=True).start() |