A Python port of the Invisible Internet Project (I2P)
1"""NTCP2 connector script for podman integration tests.
2
3Waits for the listener's key file, connects to it, performs the
4Noise_XK handshake, exchanges frames, writes results to a JSON file.
5
6Usage:
7 python router_connector.py --host 127.0.0.1 --port 6000 \
8 --key-file /shared/listener_key.bin \
9 --result-file /shared/connector_result.json
10"""
11
12import argparse
13import asyncio
14import json
15import os
16import struct
17import sys
18import time
19import traceback
20
21sys.path.insert(0, "/app/src")
22
23from i2p_crypto.x25519 import X25519DH
24from i2p_transport.ntcp2 import NTCP2Frame, FrameType
25from i2p_transport.ntcp2_connection import NTCP2Connection
26from i2p_transport.ntcp2_handshake import NTCP2Handshake
27
28
29async def _send_hs(writer, msg):
30 writer.write(struct.pack("!H", len(msg)) + msg)
31 await writer.drain()
32
33
34async def _recv_hs(reader):
35 lb = await reader.readexactly(2)
36 length = struct.unpack("!H", lb)[0]
37 return await reader.readexactly(length)
38
39
40def wait_for_key_file(path, timeout=30):
41 """Block until the listener's key file appears."""
42 deadline = time.time() + timeout
43 while time.time() < deadline:
44 if os.path.exists(path) and os.path.getsize(path) == 32:
45 with open(path, "rb") as f:
46 return f.read()
47 time.sleep(0.2)
48 raise TimeoutError(f"Key file {path} not found after {timeout}s")
49
50
51async def run_connector(host, port, key_file, result_file):
52 result = {"status": "error", "error": "unknown"}
53
54 try:
55 peer_static_pub = wait_for_key_file(key_file)
56 print(f"Got listener key, connecting to {host}:{port}", flush=True)
57
58 static = X25519DH.generate_keypair()
59 reader, writer = await asyncio.wait_for(
60 asyncio.open_connection(host, port), timeout=10.0
61 )
62
63 hs = NTCP2Handshake(
64 our_static=static, peer_static_pub=peer_static_pub, initiator=True
65 )
66 msg1 = hs.create_message_1()
67 await _send_hs(writer, msg1)
68 msg2 = await asyncio.wait_for(_recv_hs(reader), timeout=10.0)
69 msg3 = hs.process_message_2(msg2)
70 await _send_hs(writer, msg3)
71
72 send_cipher, recv_cipher = hs.split()
73 conn = NTCP2Connection(
74 reader=reader, writer=writer,
75 cipher_send=send_cipher, cipher_recv=recv_cipher,
76 remote_hash=peer_static_pub,
77 )
78 print("Handshake complete", flush=True)
79
80 # Send a frame to the listener
81 frame = NTCP2Frame(FrameType.I2NP, b"hello from connector")
82 await conn.send_frame(frame)
83
84 # Receive the listener's reply
85 received = await asyncio.wait_for(conn.recv_frame(), timeout=10.0)
86 print(f"Received: type={received.frame_type}, payload={received.payload!r}", flush=True)
87
88 result = {
89 "status": "ok",
90 "handshake": "complete",
91 "received_type": received.frame_type.value,
92 "received_payload": received.payload.decode("utf-8", errors="replace"),
93 "sent_frame": True,
94 }
95
96 conn._writer.close()
97 except Exception as e:
98 result = {"status": "error", "error": str(e)}
99 traceback.print_exc()
100 finally:
101 with open(result_file, "w") as f:
102 json.dump(result, f)
103 print(f"Result: {result}", flush=True)
104
105
106def main():
107 parser = argparse.ArgumentParser()
108 parser.add_argument("--host", default="127.0.0.1")
109 parser.add_argument("--port", type=int, required=True)
110 parser.add_argument("--key-file", required=True)
111 parser.add_argument("--result-file", required=True)
112 args = parser.parse_args()
113 asyncio.run(run_connector(args.host, args.port, args.key_file, args.result_file))
114
115
116if __name__ == "__main__":
117 main()