diff --git a/README.md b/README.md index 243f238..4e1ab20 100755 --- a/README.md +++ b/README.md @@ -102,22 +102,22 @@ specified host and port. Returns the I2P b32 destination address for the server `tunnel-control.sh server.destroy ` -#### Create a tunnel that listens for connections on localhost and forwards connections over I2P to the specified destination public key. Returns a newly created localhost port number. +#### Create a tunnel that listens for connections on localhost on the specified port and forwards connections over I2P to the specified destination public key. -`tunnel-control.sh client.create ` +`tunnel-control.sh client.create ` #### Close the tunnel listening for connections on the specified port. Returns "OK". -`tunnel-control.sh client.destroy ` +`tunnel-control.sh client.destroy ` #### Create a socks tunnel, listening on the specified port -`tunnel-control.sh socks.create ` +`tunnel-control.sh socks.create ` #### Destroy the socks tunnel listening on the specified port -`tunnel-control.sh socks.destroy ` +`tunnel-control.sh socks.destroy ` #### Start a SAM listener on port 7656. Returns "OK" diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java index fe30c7e..fa0ff46 100644 --- a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java @@ -50,19 +50,16 @@ public class AddTunnelController { try { var controller = Gui.instance.getController(); var tunnelControl = controller.getRouterWrapper().getTunnelControl(); - var tunnels = tunnelControl.getTunnels(); + var tunnelList = tunnelControl.getTunnelList(); if (tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) { Tunnel t = new TunnelControl.ClientTunnel(clientDestAddrField.getText(), Integer.parseInt(clientPortField.getText())); - tunnels.add(t); - controller.tunnelTableList.add(t); + tunnelList.addTunnel(t); } else if (tunnelType.getSelectedToggle().equals(serverTunnelRadioButton)) { Tunnel t = new TunnelControl.ServerTunnel(serverHostField.getText(), Integer.parseInt(serverPortField.getText()), new TunnelControl.KeyPair(serverKeyField.getText()), tunnelControl.getTunnelControlTempDir()); - tunnels.add(t); - controller.tunnelTableList.add(t); + tunnelList.addTunnel(t); } else if (tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) { Tunnel t = new TunnelControl.SocksTunnel(Integer.parseInt(socksPortField.getText())); - tunnels.add(t); - controller.tunnelTableList.add(t); + tunnelList.addTunnel(t); } clientTunnelConfigPane.getScene().getWindow().hide(); } diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java index b4c03b0..9472d00 100644 --- a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java @@ -21,6 +21,7 @@ import org.getmonero.i2p.zero.RouterWrapper; import static org.getmonero.i2p.zero.TunnelControl.Tunnel; import java.text.DecimalFormat; +import java.util.List; import java.util.Properties; public class Controller { @@ -56,7 +57,7 @@ public class Controller { DecimalFormat format2dp = new DecimalFormat("0.00"); private boolean masterState = true; - public final ObservableList tunnelTableList = FXCollections.observableArrayList(); + private final ObservableList tunnelTableList = FXCollections.observableArrayList(); private Stage getStage() { return (Stage) rootBorderPane.getScene().getWindow(); @@ -86,8 +87,7 @@ public class Controller { tunnelRemoveButton.setOnAction(e->{ Tunnel t = tunnelsTableView.getSelectionModel().getSelectedItem(); - getRouterWrapper().getTunnelControl().getTunnels().remove(t); - tunnelTableList.remove(t); + getRouterWrapper().getTunnelControl().getTunnelList().removeTunnel(t); t.destroy(); tunnelRemoveButton.setDisable(true); }); @@ -148,6 +148,18 @@ public class Controller { startRouter(); + new Thread(()->{ + while(getRouterWrapper()==null || getRouterWrapper().getTunnelControl()==null) { + try { Thread.sleep(100); } catch (InterruptedException e) {} + } + var tunnelList = getRouterWrapper().getTunnelControl().getTunnelList(); + tunnelList.addPropertyChangeListener(event->{ + tunnelTableList.clear(); + tunnelTableList.addAll((List) event.getNewValue()); + }); + }).start(); + + var bandwidthUpdateThread = new Thread(()->{ while(!Gui.instance.isStopping()) { if (routerWrapper.isStarted()) { diff --git a/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml index 0c99e01..239d680 100644 --- a/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml +++ b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml @@ -61,5 +61,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/org.getmonero.i2p.zero/src/module-info.java b/org.getmonero.i2p.zero/src/module-info.java index 04f69f2..9f0905a 100644 --- a/org.getmonero.i2p.zero/src/module-info.java +++ b/org.getmonero.i2p.zero/src/module-info.java @@ -6,5 +6,6 @@ module org.getmonero.i2p.zero { requires sam; requires i2ptunnel; requires jdk.crypto.ec; + requires java.desktop; exports org.getmonero.i2p.zero; } \ No newline at end of file diff --git a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java index f8f1876..cbedec9 100644 --- a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java +++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java @@ -81,7 +81,7 @@ public class RouterWrapper { } } - tunnelControl = new TunnelControl(router, new File(i2PConfigDir, "tunnelTemp")); + tunnelControl = new TunnelControl(router, i2PConfigDir, new File(i2PConfigDir, "tunnelTemp")); new Thread(tunnelControl).start(); } diff --git a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java index 7fce263..55004d1 100644 --- a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java +++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java @@ -10,7 +10,13 @@ import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.router.Router; import net.i2p.sam.SAMBridge; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; import java.io.*; import java.math.BigInteger; import java.net.InetAddress; @@ -21,24 +27,122 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Random; +import java.util.stream.Stream; 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 tunnelControlConfigDir; private File tunnelControlTempDir; + private TunnelList tunnelList; + public static class TunnelList { + private File tunnelControlConfigDir; + private File tunnelControlTempDir; + private List tunnels = new ArrayList<>(); + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); - public TunnelControl(Router router, File tunnelControlTempDir) { + public TunnelList(File tunnelControlConfigDir, File tunnelControlTempDir) { + this.tunnelControlConfigDir = tunnelControlConfigDir; + this.tunnelControlTempDir = tunnelControlTempDir; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + private void fireChangeEvent() { + pcs.firePropertyChange(new PropertyChangeEvent(this, "list", null, tunnels)); + } + + public void addTunnel(Tunnel t) { + tunnels.add(t); + save(); + fireChangeEvent(); + } + public void removeTunnel(Tunnel t) { + tunnels.remove(t); + save(); + fireChangeEvent(); + } + public Stream getTunnelsCopyStream() { + return new ArrayList<>(tunnels).stream(); + } + public void load() { + try { + File tunnelControlConfigFile = new File(tunnelControlConfigDir, "tunnels.json"); + if (tunnelControlConfigFile.exists()) { + tunnels.clear(); + JSONObject root = (JSONObject) new JSONParser().parse(Files.readString(tunnelControlConfigFile.toPath())); + JSONArray list = (JSONArray) root.get("tunnels"); + list.stream().forEach((o)->{ + JSONObject obj = (JSONObject) o; + String type = (String) obj.get("type"); + switch (type) { + case "server": + tunnels.add(new ServerTunnel((String) obj.get("host"), Integer.parseInt((String) obj.get("port")), new KeyPair((String) obj.get("keypair")), tunnelControlTempDir)); + break; + case "client": + tunnels.add(new ClientTunnel((String) obj.get("dest"), Integer.parseInt((String) obj.get("port")))); + break; + case "socks": + tunnels.add(new SocksTunnel(Integer.parseInt((String) obj.get("port")))); + break; + } + }); + fireChangeEvent(); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public void save() { + try { + JSONObject root = new JSONObject(); + JSONArray tunnelsArray = new JSONArray(); + root.put("tunnels", tunnelsArray); + for(Tunnel t : tunnels) { + JSONObject entry = new JSONObject(); + entry.put("type", t.getType()); + tunnelsArray.add(entry); + switch (t.getType()) { + case "server": + entry.put("host", t.getHost()); + entry.put("port", t.getPort()); + entry.put("dest", t.getI2P()); + entry.put("keypair", ((ServerTunnel) t).keyPair.toString()); + break; + + case "client": + entry.put("dest", t.getI2P()); + entry.put("port", t.getPort()); + break; + + case "socks": + entry.put("port", t.getPort()); + break; + } + } + Files.writeString(new File(tunnelControlConfigDir, "tunnels.json").toPath(), root.toJSONString()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public TunnelControl(Router router, File tunnelControlConfigDir, File tunnelControlTempDir) { this.router = router; tunnelControlTempDir.delete(); tunnelControlTempDir.mkdir(); + this.tunnelControlConfigDir = tunnelControlConfigDir; this.tunnelControlTempDir = tunnelControlTempDir; Runtime.getRuntime().addShutdownHook(new Thread(()->this.tunnelControlTempDir.delete())); + + tunnelList = new TunnelList(tunnelControlConfigDir, tunnelControlTempDir); } public interface Tunnel { @@ -80,22 +184,27 @@ public class TunnelControl implements Runnable { 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 ServerTunnel(String host, int port, KeyPair keyPair, File tunnelControlTempDir) { + try { + 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(); + } + catch (Exception e) { + throw new RuntimeException(e); + } } public void destroy() { new Thread(()->{ @@ -132,8 +241,8 @@ public class TunnelControl implements Runnable { @Override public String getState() { return tunnel==null ? "opening..." : "open"; } } - public List getTunnels() { - return tunnels; + public TunnelList getTunnelList() { + return tunnelList; } public static class KeyPair { @@ -145,13 +254,24 @@ public class TunnelControl implements Runnable { 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(); + + @Override + public String toString() { + return seckey + "," + pubkey; + } + + public KeyPair(String base64EncodedCommaDelimitedPair) { + try { + 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(); + } + catch (Exception e) { + throw new RuntimeException(e); + } } public static KeyPair gen() throws Exception { ByteArrayOutputStream seckey = new ByteArrayOutputStream(); @@ -166,7 +286,7 @@ public class TunnelControl implements Runnable { return new KeyPair(Files.readString(Paths.get(path))); } public void write(String path) throws Exception { - Files.writeString(Paths.get(path), seckey + "," + pubkey); + Files.writeString(Paths.get(path), toString()); } } @@ -174,8 +294,10 @@ public class TunnelControl implements Runnable { @Override public void run() { + tunnelList.load(); + try { - controlServerSocket = new ServerSocket(clientPortSeq++, 0, InetAddress.getLoopbackAddress()); + controlServerSocket = new ServerSocket(30000, 0, InetAddress.getLoopbackAddress()); while (!stopping) { try (var socket = controlServerSocket.accept()) { var out = new PrintWriter(socket.getOutputStream(), true); @@ -185,48 +307,51 @@ public class TunnelControl implements Runnable { switch(args[0]) { - case "server.create": + case "server.create": { String destHost = args[1]; int destPort = Integer.parseInt(args[2]); File serverTunnelConfigDir = new File(args[3]); File serverKeyFile; KeyPair keyPair; - if(!serverTunnelConfigDir.exists() || serverTunnelConfigDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".keys")).length==0) { + if (!serverTunnelConfigDir.exists() || serverTunnelConfigDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".keys")).length == 0) { serverTunnelConfigDir.mkdir(); keyPair = KeyPair.gen(); serverKeyFile = new File(serverTunnelConfigDir, keyPair.b32Dest + ".keys"); keyPair.write(serverKeyFile.getPath()); - } - else { + } else { serverKeyFile = serverTunnelConfigDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".keys"))[0]; keyPair = KeyPair.read(serverKeyFile.getPath()); } var tunnel = new ServerTunnel(destHost, destPort, keyPair, getTunnelControlTempDir()); - tunnels.add(tunnel); + tunnelList.addTunnel(tunnel); out.println(tunnel.dest); break; + } - case "server.destroy": + case "server.destroy": { String dest = args[1]; - new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("server") && ((ServerTunnel) t).dest.equals(dest)).forEach(t->{ + tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("server") && ((ServerTunnel) t).dest.equals(dest)).forEach(t -> { t.destroy(); - tunnels.remove(t); + tunnelList.removeTunnel(t); }); out.println("OK"); break; + } - case "client.create": + case "client.create": { String destPubKey = args[1]; - var clientTunnel = new ClientTunnel(destPubKey, clientPortSeq++); - tunnels.add(clientTunnel); + int port = Integer.parseInt(args[2]); + var clientTunnel = new ClientTunnel(destPubKey, port); + tunnelList.addTunnel(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->{ + tunnelList.getTunnelsCopyStream().filter(t->t.getType().equals("client") && ((ClientTunnel) t).port == port).forEach(t->{ t.destroy(); - tunnels.remove(t); + tunnelList.removeTunnel(t); }); out.println("OK"); break; @@ -234,21 +359,22 @@ public class TunnelControl implements Runnable { case "socks.create": { int port = Integer.parseInt(args[1]); - tunnels.add(new SocksTunnel(port)); + tunnelList.addTunnel(new SocksTunnel(port)); out.println("OK"); break; } - case "socks.destroy": + 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->{ + tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("socks") && ((SocksTunnel) t).port == port).forEach(t -> { t.destroy(); - tunnels.remove(t); + tunnelList.removeTunnel(t); }); out.println("OK"); break; + } - case "sam.create": + 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); @@ -256,6 +382,7 @@ public class TunnelControl implements Runnable { samBridge.startup(); out.println("OK"); break; + } }