A Python port of the Invisible Internet Project (I2P)
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()