package org.getmonero.i2p.zero; import net.i2p.I2PAppContext; import net.i2p.app.ClientAppManager; import net.i2p.app.ClientAppManagerImpl; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.data.Base64; import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.router.Router; import net.i2p.sam.SAMBridge; import java.io.*; import java.math.BigInteger; import java.net.InetAddress; import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Random; public class TunnelControl implements Runnable { private List tunnels = new ArrayList<>(); private int clientPortSeq = 30000; private Router router; private boolean stopping = false; private ServerSocket controlServerSocket; private File tunnelControlTempDir; public TunnelControl(Router router, File tunnelControlTempDir) { this.router = router; tunnelControlTempDir.delete(); tunnelControlTempDir.mkdir(); this.tunnelControlTempDir = tunnelControlTempDir; this.tunnelControlTempDir.deleteOnExit(); } public interface Tunnel { public String getType(); public String getHost(); public String getPort(); public String getI2P(); public String getState(); public void destroy(); } public static class ClientTunnel implements Tunnel { public String dest; public int port; public I2PTunnel tunnel; public ClientTunnel(String dest, int port) { this.dest = dest; this.port = port; new Thread(()->{ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "client " + port + " " + dest}); }).start(); } public void destroy() { new Thread(()->{ while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it tunnel.runClose(new String[]{"forced", "all"}, tunnel); }).start(); } @Override public String getType() { return "client"; } @Override public String getHost() { return "localhost"; } @Override public String getPort() { return port+""; } @Override public String getI2P() { return dest; } @Override public String getState() { return tunnel==null ? "opening..." : "open"; } } public static class ServerTunnel implements Tunnel { public String dest; public String host; public int port; public volatile I2PTunnel tunnel; public KeyPair keyPair; public ServerTunnel(String host, int port, KeyPair keyPair, File tunnelControlTempDir) throws Exception { this.host = host; this.port = port; this.keyPair = keyPair; this.dest = keyPair.b32Dest; String uuid = new BigInteger(128, new Random()).toString(16); String seckeyPath = tunnelControlTempDir.getAbsolutePath() + File.separator + "seckey."+uuid+".dat"; Files.write(Path.of(seckeyPath), Base64.decode(keyPair.seckey)); new File(seckeyPath).deleteOnExit(); // clean up temporary file that was only required because new I2PTunnel() requires it to be written to disk // listen using the I2P server keypair, and forward incoming connections to a destination and port new Thread(()->{ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "server "+host+" "+port+" " + seckeyPath}); }).start(); } public void destroy() { new Thread(()->{ while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it tunnel.runClose(new String[]{"forced", "all"}, tunnel); }).start(); } @Override public String getType() { return "server"; } @Override public String getHost() { return host; } @Override public String getPort() { return port+""; } @Override public String getI2P() { return dest; } @Override public String getState() { return tunnel==null ? "opening..." : "open"; } } public static class SocksTunnel implements Tunnel { public int port; public I2PTunnel tunnel; public SocksTunnel(int port) { this.port = port; new Thread(()->{ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "sockstunnel " + port}); }).start(); } public void destroy() { new Thread(()->{ while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it tunnel.runClose(new String[]{"forced", "all"}, tunnel); }).start(); } @Override public String getType() { return "socks"; } @Override public String getHost() { return "localhost"; } @Override public String getPort() { return port+""; } @Override public String getI2P() { return "n/a"; } @Override public String getState() { return tunnel==null ? "opening..." : "open"; } } public List getTunnels() { return tunnels; } public static class KeyPair { public String seckey; public String pubkey; public String b32Dest; public KeyPair(String seckey, String pubkey, String b32Dest) { this.seckey = seckey; this.pubkey = pubkey; this.b32Dest = b32Dest; } public KeyPair(String base64EncodedCommaDelimitedPair) throws Exception { String[] a = base64EncodedCommaDelimitedPair.split(","); this.seckey = a[0]; this.pubkey = a[1]; Destination d = new Destination(); d.readBytes(new ByteArrayInputStream(Base64.decode(this.seckey))); this.b32Dest = d.toBase32(); } public static KeyPair gen() throws Exception { ByteArrayOutputStream seckey = new ByteArrayOutputStream(); ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); I2PClient client = I2PClientFactory.createClient(); Destination d = client.createDestination(seckey); d.writeBytes(pubkey); String b32Dest = d.toBase32(); return new KeyPair(Base64.encode(seckey.toByteArray()), Base64.encode(pubkey.toByteArray()), b32Dest); } public static KeyPair read(String path) throws Exception { return new KeyPair(Files.readString(Paths.get(path))); } public void write(String path) throws Exception { Files.writeString(Paths.get(path), seckey + "," + pubkey); } } @Override public void run() { try { controlServerSocket = new ServerSocket(clientPortSeq++, 0, InetAddress.getLoopbackAddress()); while (!stopping) { try (var socket = controlServerSocket.accept()) { var out = new PrintWriter(socket.getOutputStream(), true); var in = new BufferedReader(new InputStreamReader(socket.getInputStream())); var args = in.readLine().split(" "); switch(args[0]) { case "server.create": String destHost = args[1]; int destPort = Integer.parseInt(args[2]); File serverTunnelConfigDir = new File(args[3]); if(!serverTunnelConfigDir.exists()) serverTunnelConfigDir.mkdir(); File serverKeyFile = new File(serverTunnelConfigDir, "serverTunnelSecretKey"); KeyPair keyPair; if(serverKeyFile.exists()) { keyPair = KeyPair.read(serverKeyFile.getPath()); } else { keyPair = KeyPair.gen(); keyPair.write(serverKeyFile.getPath()); } var tunnel = new ServerTunnel(destHost, destPort, keyPair, getTunnelControlTempDir()); tunnels.add(tunnel); out.println(tunnel.dest); break; case "server.destroy": String dest = args[1]; new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("server") && ((ServerTunnel) t).dest.equals(dest)).forEach(t->{ t.destroy(); tunnels.remove(t); }); out.println("OK"); break; case "client.create": String destPubKey = args[1]; var clientTunnel = new ClientTunnel(destPubKey, clientPortSeq++); tunnels.add(clientTunnel); out.println(clientTunnel.port); break; case "client.destroy": { int port = Integer.parseInt(args[1]); new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("client") && ((ClientTunnel) t).port == port).forEach(t->{ t.destroy(); tunnels.remove(t); }); out.println("OK"); break; } case "socks.create": { int port = Integer.parseInt(args[1]); tunnels.add(new SocksTunnel(port)); out.println("OK"); break; } case "socks.destroy": int port = Integer.parseInt(args[1]); new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("socks") && ((SocksTunnel) t).port == port).forEach(t->{ t.destroy(); tunnels.remove(t); }); out.println("OK"); break; case "sam.create": String[] samArgs = new String[]{"sam.keys", "127.0.0.1", "7656", "i2cp.tcp.host=127.0.0.1", "i2cp.tcp.port=7654"}; I2PAppContext context = router.getContext(); ClientAppManager mgr = new ClientAppManagerImpl(context); SAMBridge samBridge = new SAMBridge(context, mgr, samArgs); samBridge.startup(); out.println("OK"); break; } } catch (Exception e) { if(!e.getMessage().contains("Socket closed")) e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } finally { if(controlServerSocket!=null) try { controlServerSocket.close(); } catch (Exception e) { e.printStackTrace(); } } } public void stop() { stopping = true; try { controlServerSocket.close(); } catch (Exception e) { e.printStackTrace(); } } public File getTunnelControlTempDir() { return tunnelControlTempDir; } }