A Python port of the Invisible Internet Project (I2P)
1"""Extract NTCP2 connection info from a Java I2P router's router.info file.
2
3Reads the binary RouterInfo, parses it, and writes the NTCP2 host, port,
4and static public key to a JSON file.
5
6Usage:
7 python extract_java_ri.py --ri-file /shared/router.info \
8 --output /shared/java_ntcp2_info.json
9"""
10
11import argparse
12import base64
13import json
14import sys
15import traceback
16
17sys.path.insert(0, "/app/src")
18
19from i2p_data.router import RouterInfo
20
21
22def extract_ntcp2_info(ri_path, output_path):
23 """Parse router.info and extract NTCP2 address details."""
24 try:
25 with open(ri_path, "rb") as f:
26 data = f.read()
27
28 print(f"RouterInfo file: {len(data)} bytes", flush=True)
29
30 ri = RouterInfo.from_bytes(data)
31 print(f"Parsed RouterInfo: {ri}", flush=True)
32 print(f" Published: {ri.published}", flush=True)
33 print(f" Addresses: {len(ri.addresses)}", flush=True)
34 print(f" Options: {ri.options}", flush=True)
35
36 # Find NTCP2 address
37 ntcp2_addr = None
38 for addr in ri.addresses:
39 print(f" Address: {addr} options={addr.options}", flush=True)
40 if addr.transport == "NTCP2":
41 ntcp2_addr = addr
42 break
43
44 if ntcp2_addr is None:
45 # Some newer Java routers use "NTCP" for NTCP2
46 for addr in ri.addresses:
47 if "NTCP" in addr.transport.upper():
48 ntcp2_addr = addr
49 break
50
51 if ntcp2_addr is None:
52 result = {
53 "status": "error",
54 "error": "No NTCP2 address found in RouterInfo",
55 "transports": [a.transport for a in ri.addresses],
56 }
57 else:
58 # The "s" option contains the X25519 static key (base64)
59 static_key_b64 = ntcp2_addr.options.get("s", "")
60 host = ntcp2_addr.get_host() or "127.0.0.1"
61 port = ntcp2_addr.get_port() or 12345
62
63 if static_key_b64:
64 # I2P uses modified base64: ~ instead of /, - instead of +
65 std_b64 = static_key_b64.replace("~", "/").replace("-", "+")
66 # Ensure proper padding
67 padding = 4 - len(std_b64) % 4
68 if padding != 4:
69 std_b64 += "=" * padding
70 static_key = base64.b64decode(std_b64)
71 else:
72 static_key = b""
73
74 result = {
75 "status": "ok",
76 "host": host,
77 "port": port,
78 "static_key_b64": static_key_b64,
79 "static_key_hex": static_key.hex() if static_key else "",
80 "static_key_len": len(static_key),
81 "transport": ntcp2_addr.transport,
82 "all_options": ntcp2_addr.options,
83 "signature_valid": ri.verify(),
84 }
85
86 with open(output_path, "w") as f:
87 json.dump(result, f, indent=2)
88 print(f"Result: {json.dumps(result, indent=2)}", flush=True)
89
90 except Exception as e:
91 result = {"status": "error", "error": str(e)}
92 traceback.print_exc()
93 with open(output_path, "w") as f:
94 json.dump(result, f)
95 print(f"Error: {e}", flush=True)
96
97
98def main():
99 parser = argparse.ArgumentParser()
100 parser.add_argument("--ri-file", required=True)
101 parser.add_argument("--output", required=True)
102 args = parser.parse_args()
103 extract_ntcp2_info(args.ri_file, args.output)
104
105
106if __name__ == "__main__":
107 main()