A Python port of the Invisible Internet Project (I2P)
at main 145 lines 4.6 kB view raw
1"""Connect to a Java I2P router via NTCP2. 2 3Reads the NTCP2 connection info (host, port, static key) from a JSON 4file, attempts the Noise_XK handshake, and reports the result. 5 6Usage: 7 python connect_to_java.py --info-file /shared/java_ntcp2_info.json \ 8 --result-file /shared/interop_result.json 9""" 10 11import argparse 12import asyncio 13import json 14import struct 15import sys 16import traceback 17 18sys.path.insert(0, "/app/src") 19 20from i2p_crypto.x25519 import X25519DH 21from i2p_transport.ntcp2 import NTCP2Frame, FrameType 22from i2p_transport.ntcp2_connection import NTCP2Connection 23from i2p_transport.ntcp2_handshake import NTCP2Handshake 24 25 26async def _send_hs(writer, msg): 27 writer.write(struct.pack("!H", len(msg)) + msg) 28 await writer.drain() 29 30 31async def _recv_hs(reader): 32 lb = await reader.readexactly(2) 33 length = struct.unpack("!H", lb)[0] 34 return await reader.readexactly(length) 35 36 37async def attempt_handshake(host, port, peer_static_pub): 38 """Try to perform an NTCP2 handshake with the Java router.""" 39 our_static = X25519DH.generate_keypair() 40 41 print(f"Connecting to {host}:{port}...", flush=True) 42 reader, writer = await asyncio.wait_for( 43 asyncio.open_connection(host, port), timeout=10.0 44 ) 45 print("TCP connected", flush=True) 46 47 hs = NTCP2Handshake( 48 our_static=our_static, 49 peer_static_pub=peer_static_pub, 50 initiator=True, 51 ) 52 53 # Send message 1 54 msg1 = hs.create_message_1() 55 print(f"Sending msg1 ({len(msg1)} bytes)...", flush=True) 56 await _send_hs(writer, msg1) 57 58 # Read message 2 59 print("Waiting for msg2...", flush=True) 60 msg2 = await asyncio.wait_for(_recv_hs(reader), timeout=15.0) 61 print(f"Got msg2 ({len(msg2)} bytes)", flush=True) 62 63 # Process msg2, send msg3 64 msg3 = hs.process_message_2(msg2) 65 print(f"Sending msg3 ({len(msg3)} bytes)...", flush=True) 66 await _send_hs(writer, msg3) 67 68 print("Handshake complete!", flush=True) 69 70 # Try to receive a frame (Java router usually sends DateTime or RouterInfo) 71 send_cipher, recv_cipher = hs.split() 72 conn = NTCP2Connection( 73 reader=reader, writer=writer, 74 cipher_send=send_cipher, cipher_recv=recv_cipher, 75 remote_hash=peer_static_pub, 76 ) 77 78 received_frames = [] 79 try: 80 for _ in range(3): # Try to read up to 3 frames 81 frame = await asyncio.wait_for(conn.recv_frame(), timeout=5.0) 82 received_frames.append({ 83 "type": frame.frame_type.value, 84 "type_name": frame.frame_type.name, 85 "payload_len": len(frame.payload), 86 }) 87 print(f"Received frame: type={frame.frame_type.name}, " 88 f"payload_len={len(frame.payload)}", flush=True) 89 except asyncio.TimeoutError: 90 print("No more frames (timeout)", flush=True) 91 except Exception as e: 92 print(f"Frame read error: {e}", flush=True) 93 94 conn._writer.close() 95 96 return { 97 "handshake": "complete", 98 "frames_received": received_frames, 99 } 100 101 102async def run(info_file, result_file): 103 result = {"status": "error", "error": "unknown"} 104 105 try: 106 with open(info_file) as f: 107 info = json.load(f) 108 109 if info.get("status") != "ok": 110 result = {"status": "error", "error": f"Bad info file: {info}"} 111 else: 112 host = info["host"] 113 port = info["port"] 114 static_key = bytes.fromhex(info["static_key_hex"]) 115 116 if len(static_key) != 32: 117 result = {"status": "error", 118 "error": f"Invalid static key length: {len(static_key)}"} 119 else: 120 hs_result = await attempt_handshake(host, port, static_key) 121 result = {"status": "ok", **hs_result} 122 123 except asyncio.TimeoutError: 124 result = {"status": "error", "error": "timeout during handshake"} 125 except ConnectionRefusedError: 126 result = {"status": "error", "error": "connection refused"} 127 except Exception as e: 128 result = {"status": "error", "error": str(e), "type": type(e).__name__} 129 traceback.print_exc() 130 131 with open(result_file, "w") as f: 132 json.dump(result, f, indent=2) 133 print(f"Result: {json.dumps(result, indent=2)}", flush=True) 134 135 136def main(): 137 parser = argparse.ArgumentParser() 138 parser.add_argument("--info-file", required=True) 139 parser.add_argument("--result-file", required=True) 140 args = parser.parse_args() 141 asyncio.run(run(args.info_file, args.result_file)) 142 143 144if __name__ == "__main__": 145 main()