diff --git a/README.md b/README.md index c77f8fd..11cc502 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## Zero dependency, small footprint, cross-platform I2P Java Router with simple tunnel/socks controller and SAM interface +## Zero dependency, small footprint, cross-platform I2P Java Router with GUI and simple tunnel/socks controller and SAM interface ## @@ -47,14 +47,19 @@ the jlink tool to build zero-dependency platform-specific launchers. To run the Linux router, type: `dist/linux/router/bin/launch.sh` +or `dist/linux/router/bin/launch-gui.sh` To run the MacOS router, type: `dist/mac/router/bin/launch.sh` +or `dist/mac/router/bin/launch-gui.sh` For Windows, run: `dist/windows/router/bin/launch.bat` +or `dist/windows/router/bin/launch-gui.bat` + +Note that for the Windows GUI to run, you may need to install the latest Microsoft Visual C++ Redistributable If it launches successfully, you'll see the message: diff --git a/bin/build-launcher.sh b/bin/build-launcher.sh index 590bb6f..f696378 100755 --- a/bin/build-launcher.sh +++ b/bin/build-launcher.sh @@ -8,39 +8,61 @@ fi source $basedir/bin/java-config.sh +rm -fr target/org.getmonero.i2p.zero/classes +rm -fr target/org.getmonero.i2p.zero.gui/classes + # compile the Main class that starts the I2P router and SAM listener echo "*** Compiling Main class" -$JAVA_HOME/bin/javac --module-path import/lib -d target/classes $(find src -name '*.java') +$JAVA_HOME/bin/javac --module-path import/lib -d target/org.getmonero.i2p.zero/classes $(find org.getmonero.i2p.zero/src -name '*.java') +$JAVA_HOME/bin/javac --module-path import/lib:import/javafx-sdk-11.0.2/lib:target/org.getmonero.i2p.zero/classes -d target/org.getmonero.i2p.zero.gui/classes $(find org.getmonero.i2p.zero.gui/src -name '*.java') +cp -r org.getmonero.i2p.zero.gui/src/* target/org.getmonero.i2p.zero.gui/classes +find target -type f -name '*.java' -delete # package as a modular jar echo "*** Packaging as a modular jar" -$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.jar --main-class org.getmonero.i2p.zero.Main -C target/classes . +$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.jar --main-class org.getmonero.i2p.zero.Main -C target/org.getmonero.i2p.zero/classes . +$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.gui.jar --main-class org.getmonero.i2p.zero.gui.Gui -C target/org.getmonero.i2p.zero.gui/classes . rm -fr $basedir/dist -mkdir -p $basedir/dist/linux $basedir/dist/mac $basedir/dist/win +for i in linux mac win linux-gui mac-gui win-gui; do mkdir -p $basedir/dist/$i; done # create OS specific launchers which will bundle together the code and a minimal JVM -echo "*** Performing jlink (Linux)" -$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_LINUX}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/linux/router --strip-debug --compress 2 --no-header-files --no-man-pages - -echo "*** Performing jlink (Mac)" -$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_MAC}/Contents/Home/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/mac/router --strip-debug --compress 2 --no-header-files --no-man-pages - -echo "*** Performing jlink (Windows)" -$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_WIN}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/win/router --strip-debug --compress 2 --no-header-files --no-man-pages +for i in linux mac win; do + echo "*** Performing jlink ($i)" + case $i in + linux ) + JAVA_HOME_VARIANT=${JAVA_HOME_LINUX} ;; + mac ) + JAVA_HOME_VARIANT=${JAVA_HOME_MAC} ;; + win ) + JAVA_HOME_VARIANT=${JAVA_HOME_WIN} ;; + esac + echo "Using JAVA_HOME_VARIANT: $JAVA_HOME_VARIANT" + $JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_VARIANT}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/$i/router --strip-debug --compress 2 --no-header-files --no-man-pages + $JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_VARIANT}/jmods:import/javafx-jmods/$i/javafx-jmods-11.0.2:target/modules:target/org.getmonero.i2p.zero.jar:target/org.getmonero.i2p.zero.gui.jar --add-modules org.getmonero.i2p.zero,org.getmonero.i2p.zero.gui,javafx.controls,javafx.fxml,java.desktop --output dist/$i-gui/router --strip-debug --compress 2 --no-header-files --no-man-pages +done -for i in linux mac; do +for i in linux mac linux-gui mac-gui; do cp $basedir/resources/launch.sh $basedir/dist/$i/router/bin/ cp $basedir/resources/tunnel-control.sh $basedir/dist/$i/router/bin/ done -cp $basedir/resources/launch.bat $basedir/dist/win/router/bin/ +for i in win win-gui; do + cp $basedir/resources/launch.bat $basedir/dist/$i/router/bin/ +done -for i in linux mac win; do cp -r $basedir/import/i2p.base $basedir/dist/$i/router/; done -for i in linux mac win; do mkdir -p $basedir/dist/$i/router/i2p.config; done +for i in linux-gui mac-gui; do + cp $basedir/resources/launch-gui.sh $basedir/dist/$i/router/bin/ +done +for i in win-gui; do + cp $basedir/resources/launch-gui.bat $basedir/dist/$i/router/bin/ +done + +for i in linux mac win linux-gui mac-gui win-gui; do cp -r $basedir/import/i2p.base $basedir/dist/$i/router/; done +for i in linux mac win linux-gui mac-gui win-gui; do mkdir -p $basedir/dist/$i/router/i2p.config; done # remove unnecessary native libs from jbigi.jar -for i in linux mac win; do +for i in linux mac win linux-gui mac-gui win-gui; do for j in freebsd linux mac win; do if [ "$i" != "$j" ]; then if [ "$j" = "mac" ]; then j="osx"; fi @@ -54,3 +76,4 @@ du -sk dist/* | awk '{printf "%.1f MB %s\n",$1/1024,$2}' echo "*** Done ***" echo "To run, type: dist/linux/router/bin/launch.sh" +echo "To run the GUI, type: dist/linux-gui/router/bin/launch-gui.sh" diff --git a/bin/import-packages.sh b/bin/import-packages.sh index 9d6b569..196b311 100755 --- a/bin/import-packages.sh +++ b/bin/import-packages.sh @@ -37,4 +37,28 @@ if [ ! -d "$basedir/import/apache-ant-1.10.5" ]; then tar zxvf apache-ant-1.10.5-bin.tar.gz fi +if [ ! -d "$basedir/import/javafx-sdk-11.0.2" ]; then + if [ $(uname -s) = Darwin ]; then + wget JAVAFX_SDK_DOWNLOAD_URL_MAC + unzip openjfx-11.0.2_osx-x64_bin-sdk.zip + else + wget $JAVAFX_SDK_DOWNLOAD_URL_LINUX + unzip openjfx-11.0.2_linux-x64_bin-sdk.zip + fi +fi + +if [ ! -d "$basedir/import/javafx-jmods" ]; then + mkdir -p javafx-jmods + mkdir -p javafx-jmods/linux javafx-jmods/mac javafx-jmods/win + wget --directory-prefix=javafx-jmods/linux $JAVAFX_JMODS_DOWNLOAD_URL_LINUX + wget --directory-prefix=javafx-jmods/mac $JAVAFX_JMODS_DOWNLOAD_URL_MAC + wget --directory-prefix=javafx-jmods/win $JAVAFX_JMODS_DOWNLOAD_URL_WIN + + unzip jdks/linux/$JAVAFX_JMODS_DOWNLOAD_FILENAME_LINUX -d javafx-jmods/linux/ + unzip jdks/mac/$JAVAFX_JMODS_DOWNLOAD_FILENAME_MAC -d javafx-jmods/mac/ + unzip jdks/win/$JAVAFX_JMODS_DOWNLOAD_FILENAME_WIN -d javafx-jmods/win/ +fi + + + diff --git a/bin/java-config.sh b/bin/java-config.sh index c6535c9..9b83fd8 100644 --- a/bin/java-config.sh +++ b/bin/java-config.sh @@ -18,6 +18,17 @@ JAVA_HOME_LINUX=$basedir/import/jdks/linux/jdk-11.0.1+13 JAVA_HOME_MAC=$basedir/import/jdks/mac/jdk-11.0.1+13/Contents/Home JAVA_HOME_WIN=$basedir/import/jdks/win/jdk-11.0.1+13 +JAVAFX_SDK_DOWNLOAD_URL_LINUX=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_linux-x64_bin-sdk.zip +JAVAFX_SDK_DOWNLOAD_URL_MAC=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_osx-x64_bin-sdk.zip + +JAVAFX_JMODS_DOWNLOAD_URL_LINUX=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_linux-x64_bin-jmods.zip +JAVAFX_JMODS_DOWNLOAD_URL_MAC=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_osx-x64_bin-jmods.zip +JAVAFX_JMODS_DOWNLOAD_URL_WIN=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_windows-x64_bin-jmods.zip + +JAVAFX_JMODS_DOWNLOAD_FILENAME_LINUX=openjfx-11.0.2_linux-x64_bin-jmods.zip +JAVAFX_JMODS_DOWNLOAD_FILENAME_MAC=openjfx-11.0.2_osx-x64_bin-jmods.zip +JAVAFX_JMODS_DOWNLOAD_FILENAME_WIN=openjfx-11.0.2_windows-x64_bin-jmods.zip + OS=`uname -s` if [ $OS = "Darwin" ]; then export JAVA_HOME=$JAVA_HOME_MAC diff --git a/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml b/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml new file mode 100644 index 0000000..b1306c9 --- /dev/null +++ b/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.getmonero.i2p.zero.gui/src/module-info.java b/org.getmonero.i2p.zero.gui/src/module-info.java new file mode 100644 index 0000000..62ffc01 --- /dev/null +++ b/org.getmonero.i2p.zero.gui/src/module-info.java @@ -0,0 +1,8 @@ +module org.getmonero.i2p.zero.gui { + requires org.getmonero.i2p.zero; + requires java.desktop; + requires javafx.controls; + requires javafx.fxml; + exports org.getmonero.i2p.zero.gui; + opens org.getmonero.i2p.zero.gui; +} 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 new file mode 100644 index 0000000..d4a74a1 --- /dev/null +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java @@ -0,0 +1,124 @@ +package org.getmonero.i2p.zero.gui; + +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.layout.Pane; +import org.getmonero.i2p.zero.TunnelControl; + +import java.util.stream.Stream; +import static org.getmonero.i2p.zero.TunnelControl.Tunnel; + +public class AddTunnelController { + + @FXML Pane clientTunnelConfigPane; + @FXML Pane serverTunnelConfigPane; + @FXML Pane socksProxyConfigPane; + @FXML Button addButton; + @FXML Button cancelButton; + @FXML ToggleGroup tunnelType; + @FXML RadioButton clientTunnelRadioButton; + @FXML RadioButton serverTunnelRadioButton; + @FXML RadioButton socksProxyRadioButton; + @FXML TextField clientDestAddrField; + @FXML TextField clientPortField; + @FXML TextField serverHostField; + @FXML TextField serverPortField; + @FXML TextField serverKeyField; + @FXML TextField serverAddrField; + @FXML TextField socksPortField; + + + private void updateAddButtonState() { + if(tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) { + addButton.setDisable(Stream.of(clientDestAddrField, clientPortField).anyMatch(f->f.getText().isBlank())); + } + else if(tunnelType.getSelectedToggle().equals(serverTunnelRadioButton)) { + addButton.setDisable(Stream.of(serverHostField, serverPortField, serverKeyField, serverAddrField).anyMatch(f->f.getText().isBlank())); + } + else if(tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) { + addButton.setDisable(Stream.of(socksPortField).anyMatch(f->f.getText().isBlank())); + } + } + + @FXML + private void initialize() { + + serverTunnelConfigPane.setVisible(false); + socksProxyConfigPane.setVisible(false); + + addButton.setOnAction(ev->{ + try { + var controller = Gui.instance.getController(); + var tunnelControl = controller.getRouterWrapper().getTunnelControl(); + var tunnels = tunnelControl.getTunnels(); + if (tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) { + Tunnel t = new TunnelControl.ClientTunnel(clientDestAddrField.getText(), Integer.parseInt(clientPortField.getText())); + tunnels.add(t); + controller.tunnelTableList.add(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); + } else if (tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) { + Tunnel t = new TunnelControl.SocksTunnel(Integer.parseInt(socksPortField.getText())); + tunnels.add(t); + controller.tunnelTableList.add(t); + } + clientTunnelConfigPane.getScene().getWindow().hide(); + } + catch (Exception e) { + e.printStackTrace(); + } + }); + + TextField[] allTextFields = new TextField[] {clientDestAddrField, clientPortField, serverHostField, serverPortField, serverKeyField, serverAddrField, socksPortField}; + for(TextField f : allTextFields) { + f.textProperty().addListener((observable, oldValue, newValue) -> updateAddButtonState()); + } + + + cancelButton.setOnAction(e->{ + clientTunnelConfigPane.getScene().getWindow().hide(); + }); + + tunnelType.selectedToggleProperty().addListener((ov, oldToggle, newToggle)-> { + try { + var tunnelControl = Gui.instance.getController().getRouterWrapper().getTunnelControl(); + clientTunnelConfigPane.setVisible(false); + serverTunnelConfigPane.setVisible(false); + socksProxyConfigPane.setVisible(false); + if (newToggle.equals(clientTunnelRadioButton)) clientTunnelConfigPane.setVisible(true); + if (newToggle.equals(serverTunnelRadioButton)) { + var keyPair = tunnelControl.genKeyPair(); + serverKeyField.setText(keyPair.seckey + "," + keyPair.pubkey); + serverAddrField.setText(keyPair.b32Dest); + serverTunnelConfigPane.setVisible(true); + } + if (newToggle.equals(socksProxyRadioButton)) socksProxyConfigPane.setVisible(true); + updateAddButtonState(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }); + + serverAddrField.setEditable(false); + + serverKeyField.textProperty().addListener((observable, oldValue, newValue) -> { + serverAddrField.setText(""); + String key = newValue; + if(key!=null && !key.isEmpty()) { + try { + TunnelControl.KeyPair keyPair = new TunnelControl.KeyPair(key); + serverAddrField.setText(keyPair.b32Dest); + } + catch (Exception e) { + // ignore exception. user may be part way through entering string + } + + } + }); + + } + +} 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 new file mode 100644 index 0000000..38b0e4e --- /dev/null +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java @@ -0,0 +1,198 @@ +package org.getmonero.i2p.zero.gui; + +import javafx.application.Platform; +import javafx.beans.binding.DoubleBinding; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.getmonero.i2p.zero.RouterWrapper; +import static org.getmonero.i2p.zero.TunnelControl.Tunnel; + +import java.text.DecimalFormat; +import java.util.Properties; + +public class Controller { + + private RouterWrapper routerWrapper; + + @FXML + private BorderPane rootBorderPane; + @FXML private Slider bandwidthSlider; + @FXML private Label maxBandwidthLabel; + @FXML private ImageView masterToggle; + @FXML private AnchorPane bandwidthDisabledOverlay; + @FXML private Tab bandwidthTab; + @FXML private Tab tunnelsTab; + @FXML private Tab helpTab; + @FXML private Label statusLabel; + @FXML private Button tunnelAddButton; + @FXML private Button tunnelRemoveButton; + @FXML private TableView tunnelsTableView; + @FXML private TableColumn typeCol; + @FXML private TableColumn hostCol; + @FXML private TableColumn portCol; + @FXML private TableColumn i2PCol; + @FXML private Label bandwidthIn1s; + @FXML private Label bandwidthIn5m; + @FXML private Label bandwidthInAll; + @FXML private Label totalTransferredIn; + @FXML private Label bandwidthOut1s; + @FXML private Label bandwidthOut5m; + @FXML private Label bandwidthOutAll; + @FXML private Label totalTransferredOut; + + DecimalFormat format2dp = new DecimalFormat("0.00"); + private boolean masterState = true; + public final ObservableList tunnelTableList = FXCollections.observableArrayList(); + + private Stage getStage() { + return (Stage) rootBorderPane.getScene().getWindow(); + } + + @FXML private void initialize() { + + typeCol.setCellValueFactory(new PropertyValueFactory("type")); + hostCol.setCellValueFactory(new PropertyValueFactory("host")); + portCol.setCellValueFactory(new PropertyValueFactory("port")); + i2PCol.setCellValueFactory(new PropertyValueFactory("I2P")); + + DoubleBinding usedWidth = typeCol.widthProperty().add(hostCol.widthProperty()).add(portCol.widthProperty()); + i2PCol.prefWidthProperty().bind(tunnelsTableView.widthProperty().subtract(usedWidth).subtract(2)); + + tunnelsTableView.setItems(tunnelTableList); + + tunnelsTableView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldSelection, newSelection) -> { + if (newSelection == null) { + tunnelRemoveButton.setDisable(true); + } + else { + tunnelRemoveButton.setDisable(false); + } + }); + + tunnelRemoveButton.setOnAction(e->{ + Tunnel t = tunnelsTableView.getSelectionModel().getSelectedItem(); + getRouterWrapper().getTunnelControl().getTunnels().remove(t); + tunnelTableList.remove(t); + t.destroy(); + tunnelRemoveButton.setDisable(true); + }); + + tunnelAddButton.setOnAction(event->{ + try { + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.WINDOW_MODAL); + dialogStage.initOwner(getStage()); + dialogStage.setResizable(false); + dialogStage.setTitle("New tunnel"); + Scene dialogScene = new Scene(FXMLLoader.load(getClass().getResource("addTunnel.fxml"))); + dialogScene.getStylesheets().add("org/getmonero/i2p/zero/gui/gui.css"); + dialogStage.setScene(dialogScene); + dialogStage.show(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + EventHandler tabSelectionEventHandler = e->{ + Stage stage = getStage(); + if(bandwidthTab.isSelected()) { + stage.setWidth(360); + stage.setHeight(370); + } + else if(tunnelsTab.isSelected()) { + stage.setWidth(700); + } + }; + bandwidthTab.setOnSelectionChanged(tabSelectionEventHandler); + tunnelsTab.setOnSelectionChanged(tabSelectionEventHandler); + helpTab.setOnSelectionChanged(tabSelectionEventHandler); + + bandwidthSlider.valueProperty().addListener((observableValue, oldValue, newValue)-> { + maxBandwidthLabel.setText(String.format("%.1f", newValue.floatValue()) + " Mbps"); + routerWrapper.debouncedUpdateBandwidthLimitKBPerSec((int) Math.round(1024d*newValue.doubleValue()/8d)); + }); + + masterToggle.setOnMouseClicked(e->{ + masterState = !masterState; + bandwidthDisabledOverlay.setVisible(!masterState); + + if(masterState) { + masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-on.png")); + statusLabel.setVisible(true); + routerWrapper.start(); + tunnelAddButton.setDisable(false); + } + else { + masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-off.png")); + statusLabel.setVisible(false); + routerWrapper.stop(); + tunnelTableList.clear(); + tunnelAddButton.setDisable(true); + } + }); + + startRouter(); + + var bandwidthUpdateThread = new Thread(()->{ + while(!Gui.instance.isStopping()) { + if (routerWrapper.isStarted()) { + Platform.runLater(() -> { + bandwidthIn1s.setText(format2dp.format(routerWrapper.get1sRateInKBps()) + " KBps"); + bandwidthIn5m.setText(format2dp.format(routerWrapper.get5mRateInKBps()) + " KBps"); + bandwidthInAll.setText(format2dp.format(routerWrapper.getAvgRateInKBps()) + " KBps"); + totalTransferredIn.setText(format2dp.format(routerWrapper.getTotalInMB()) + " MB "); + bandwidthOut1s.setText(format2dp.format(routerWrapper.get1sRateOutKBps()) + " KBps"); + bandwidthOut5m.setText(format2dp.format(routerWrapper.get5mRateOutKBps()) + " KBps"); + bandwidthOutAll.setText(format2dp.format(routerWrapper.getAvgRateOutKBps()) + " KBps"); + totalTransferredOut.setText(format2dp.format(routerWrapper.getTotalOutMB()) + " MB "); + }); + } + try { Thread.sleep(1000); } catch (InterruptedException e) {} + } + }); + bandwidthUpdateThread.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(()->bandwidthUpdateThread.interrupt())); + + } + + public RouterWrapper getRouterWrapper() { + return routerWrapper; + } + + private int getBandwidthLimitKBPerSec() { + return (int)Math.round(bandwidthSlider.getValue()*1024d/8d); + } + + private void startRouter() { + + // need to launch Gui with parameters: --i2p.dir.base= and --i2p.dir.config= + + var params = Gui.instance.getParameters().getNamed(); + + Properties routerProperties = new Properties(); + routerProperties.put("i2p.dir.base", params.get("i2p.dir.base")); + routerProperties.put("i2p.dir.config", params.get("i2p.dir.config")); + routerProperties.put("i2np.inboundKBytesPerSecond", getBandwidthLimitKBPerSec()); + routerProperties.put("i2np.outboundKBytesPerSecond", getBandwidthLimitKBPerSec()); + routerProperties.put("router.sharePercentage", 80); + routerWrapper = new RouterWrapper(routerProperties); + routerWrapper.start(); + + } + + +} diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java new file mode 100644 index 0000000..0e502c4 --- /dev/null +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java @@ -0,0 +1,65 @@ +package org.getmonero.i2p.zero.gui; + +import java.awt.Taskbar; +import java.awt.image.BufferedImage; +import java.util.List; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Stage; + +import javax.imageio.ImageIO; + +public class Gui extends Application { + + + public static Gui instance; + private Controller controller; + private boolean isStopping = false; + + @Override + public void start(Stage primaryStage) throws Exception{ + + instance = this; + String osName = System.getProperty("os.name"); + if(osName.startsWith("Mac")) { + Taskbar taskbar = Taskbar.getTaskbar(); + BufferedImage image = ImageIO.read(getClass().getResource("icon.png")); + taskbar.setIconImage(image); + } + else primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("icon.png"))); + + FXMLLoader loader = new FXMLLoader(getClass().getResource("gui.fxml")); + Parent root = loader.load(); + controller = loader.getController(); + + primaryStage.setTitle("I2P-zero"); + primaryStage.setMinWidth(360); + primaryStage.setMinHeight(370); + Scene scene = new Scene(root); + scene.getStylesheets().add("org/getmonero/i2p/zero/gui/gui.css"); + primaryStage.setScene(scene); + primaryStage.show(); + + } + + @Override + public void stop() throws Exception { + isStopping = true; + } + + public boolean isStopping() { + return isStopping; + } + + public Controller getController() { + return controller; + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf new file mode 100755 index 0000000..2e31d02 Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf differ diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml new file mode 100644 index 0000000..6d16521 --- /dev/null +++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png new file mode 100644 index 0000000..c69b02d Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png differ diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png new file mode 100644 index 0000000..481ff76 Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png differ diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png new file mode 100644 index 0000000..f73c242 Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png differ diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png new file mode 100644 index 0000000..f7523f9 Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png differ diff --git a/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml new file mode 100644 index 0000000..0c99e01 --- /dev/null +++ b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/module-info.java b/org.getmonero.i2p.zero/src/module-info.java similarity index 83% rename from src/module-info.java rename to org.getmonero.i2p.zero/src/module-info.java index 36bf536..04f69f2 100644 --- a/src/module-info.java +++ b/org.getmonero.i2p.zero/src/module-info.java @@ -6,4 +6,5 @@ module org.getmonero.i2p.zero { requires sam; requires i2ptunnel; requires jdk.crypto.ec; + exports org.getmonero.i2p.zero; } \ No newline at end of file diff --git a/src/org/getmonero/i2p/zero/Main.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java similarity index 62% rename from src/org/getmonero/i2p/zero/Main.java rename to org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java index 5218e56..cf73c7d 100644 --- a/src/org/getmonero/i2p/zero/Main.java +++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java @@ -4,8 +4,6 @@ import java.io.File; import java.util.Properties; import java.util.stream.Collectors; -import net.i2p.router.Router; - public class Main { public static void main(String[] args) { @@ -37,37 +35,7 @@ public class Main { System.out.println("Options set: " + p.entrySet().stream().map(e->"--"+e.getKey()+"="+e.getValue()).collect(Collectors.joining(" "))); - Router router = new Router(p); - - new Thread(()->{ - router.setKillVMOnEnd(true); - router.runRouter(); - }).start(); - - new Thread(()->{ - try { - while(true) { - if(router.isAlive()) { - break; - } - else { - Thread.sleep(1000); - System.out.println("Waiting for I2P router to start..."); - } - } - - new Thread(new TunnelControl(router, new File(new File(p.getProperty("i2p.dir.config")), "tunnel"))).start(); - - } - catch (Exception e) { - e.printStackTrace(); - } - }).start(); - - Runtime.getRuntime().addShutdownHook(new Thread(()->{ - System.out.println("I2P router will shut down gracefully"); - router.shutdownGracefully(); - })); + new RouterWrapper(p).start(); } 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 new file mode 100644 index 0000000..c2ecd0f --- /dev/null +++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java @@ -0,0 +1,165 @@ +package org.getmonero.i2p.zero; + +import net.i2p.router.Router; +import net.i2p.router.transport.FIFOBandwidthRefiller; + +import java.io.File; +import java.util.Date; +import java.util.HashMap; +import java.util.Properties; + +public class RouterWrapper { + + private Router router; + private boolean started = false; + private Properties routerProperties; + private TunnelControl tunnelControl; + + public RouterWrapper(Properties p) { + this.routerProperties = p; + } + + public boolean isStarted() { + return started; + } + + public void start() { + + new Thread(()-> { + if(started) return; + started = true; + + router = new Router(routerProperties); + + new Thread(()->{ + router.setKillVMOnEnd(false); + router.runRouter(); + }).start(); + + new Thread(()->{ + try { + while(true) { + if(router.isAlive()) { + break; + } + else { + Thread.sleep(1000); + System.out.println("Waiting for I2P router to start..."); + } + } + + tunnelControl = new TunnelControl(router, new File(new File(routerProperties.getProperty("i2p.dir.config")), "tunnel")); + new Thread(tunnelControl).start(); + + } + catch (Exception e) { + e.printStackTrace(); + } + }).start(); + + Runtime.getRuntime().addShutdownHook(new Thread(()->stop())); + + }).start(); + + } + + public void stop() { + if(!started) return; + started = false; + tunnelControl.stop(); + System.out.println("I2P router will shut down gracefully"); + router.shutdownGracefully(); + } + + long lastTriggerTimestamp = 0; + public void debouncedUpdateBandwidthLimitKBPerSec(int n) { + long triggerTime = new Date().getTime(); + lastTriggerTimestamp = triggerTime; + new Thread(()->{ + try { Thread.sleep(2000); } catch(InterruptedException e) {} + if(lastTriggerTimestamp==triggerTime) { + // nothing happened after we were triggered, so proceed + updateBandwidthLimitKBPerSec(n); + } + }).start(); + } + + public void updateBandwidthLimitKBPerSec(int n) { + routerProperties.put("i2np.inboundKBytesPerSecond", n); + routerProperties.put("i2np.outboundKBytesPerSecond", n); + + var changes = new HashMap(); + + final int DEF_BURST_PCT = 10; + final int DEF_BURST_TIME = 20; + + int inboundRate = n; + int outboundRate = n; + + { + float rate = inboundRate / 1.024f; + float kb = DEF_BURST_TIME * rate; + changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate))); + changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb))); + rate -= Math.min(rate * DEF_BURST_PCT / 100, 50); + changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH, Integer.toString(Math.round(rate))); + } + { + float rate = outboundRate / 1.024f; + float kb = DEF_BURST_TIME * rate; + changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate))); + changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb))); + rate -= Math.min(rate * DEF_BURST_PCT / 100, 50); + changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH, Integer.toString(Math.round(rate))); + } + + boolean saved = router.saveConfig(changes, null); + // this has to be after the save + router.getContext().bandwidthLimiter().reinitialize(); + if(!saved) throw new RuntimeException("Error saving the new bandwidth limit"); + + } + + public double get1sRateInKBps() { + try { return router.getContext().bandwidthLimiter().getReceiveBps()/1024d; } catch (Exception e) { return 0; } + } + public double get1sRateOutKBps() { + try { return router.getContext().bandwidthLimiter().getSendBps()/1024d; } catch (Exception e) { return 0; } + } + public double get5mRateInKBps() { + try { + return router.getContext().statManager().getRate("bw.recvRate").getRate(5*60*1000).getAverageValue()/1024d; + } + catch (Exception e) { return 0; } + } + public double get5mRateOutKBps() { + try { + return router.getContext().statManager().getRate("bw.sendRate").getRate(5*60*1000).getAverageValue()/1024d; + } + catch (Exception e) { return 0; } + } + public double getAvgRateInKBps() { + try { + return router.getContext().statManager().getRate("bw.recvRate").getLifetimeAverageValue()/1024d; + } + catch (Exception e) { return 0; } + } + public double getAvgRateOutKBps() { + try { + return router.getContext().statManager().getRate("bw.sendRate").getLifetimeAverageValue()/1024d; + } + catch (Exception e) { return 0; } + } + public double getTotalInMB() { + try { return router.getContext().bandwidthLimiter().getTotalAllocatedInboundBytes()/1048576d; } catch (Exception e) { return 0; } + } + public double getTotalOutMB() { + try { return router.getContext().bandwidthLimiter().getTotalAllocatedOutboundBytes()/1048576d; } catch (Exception e) { return 0; } + } + + public TunnelControl getTunnelControl() { + return tunnelControl; + } + + +} 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 new file mode 100644 index 0000000..4288779 --- /dev/null +++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java @@ -0,0 +1,277 @@ +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.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; + } + + public interface Tunnel { + public String getType(); + public String getHost(); + public String getPort(); + public String getI2P(); + 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(()->{ + 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; } + } + public static class ServerTunnel implements Tunnel { + public String dest; + public String host; + public int port; + public 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)); + + // 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(()->{ + 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; } + + } + 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(()->{ + 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"; } + } + + public List getTunnels() { + return tunnels; + } + + public KeyPair genKeyPair() 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 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(); + } + } + + @Override + public void run() { + + // listen for socket connections to the tunnel controller. + // listen for the commands on port 30000: + // server.create // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port + // server.destroy // closes the tunnel listening for connections on the specified destination public key, and returns OK + // client.create // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key + // client.destroy // closes the tunnel listening for connections on the specified port, and returns OK + // socks.create // creates a socks proxy listening on the specified port + // socks.destroy // closes the socks proxy listening on the specified port, and returns OK + // + // send a command with bash: exec 3<>/dev/tcp/localhost/30000; echo "server.create localhost 80" >&3; cat <&3 + // + + 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]); + var tunnel = new ServerTunnel(destHost, destPort, genKeyPair(), 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; + } + + +} diff --git a/resources/launch-gui.bat b/resources/launch-gui.bat new file mode 100644 index 0000000..bb8293d --- /dev/null +++ b/resources/launch-gui.bat @@ -0,0 +1,3 @@ +@echo off +set DIR=%~dp0 +"%DIR%\java" -cp %DIR%\..\i2p.base\jbigi.jar -m org.getmonero.i2p.zero.gui --i2p.dir.base=%DIR%\..\i2p.base --i2p.dir.config=%DIR%\..\i2p.config %* \ No newline at end of file diff --git a/resources/launch-gui.sh b/resources/launch-gui.sh new file mode 100755 index 0000000..f0761f7 --- /dev/null +++ b/resources/launch-gui.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ $(uname -s) = Darwin ]; then + basedir=$(dirname $(cd "$(dirname "$0")"; pwd -P)) +else + basedir=$(dirname $(dirname $(readlink -fm $0))) +fi + +$basedir/bin/java -cp $basedir/i2p.base/jbigi.jar -m org.getmonero.i2p.zero.gui --i2p.dir.base=$basedir/i2p.base --i2p.dir.config=$basedir/i2p.config diff --git a/src/org/getmonero/i2p/zero/TunnelControl.java b/src/org/getmonero/i2p/zero/TunnelControl.java deleted file mode 100644 index eddd292..0000000 --- a/src/org/getmonero/i2p/zero/TunnelControl.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.getmonero.i2p.zero; - -import net.i2p.I2PAppContext; -import net.i2p.app.ClientAppManager; -import net.i2p.app.ClientAppManagerImpl; -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.BufferedReader; -import java.io.File; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.FileInputStream; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.nio.file.Files; -import java.util.HashMap; -import java.util.Map; - -public class TunnelControl implements Runnable { - - private Map clientTunnels = new HashMap<>(); - private Map serverTunnels = new HashMap<>(); - private Map socksTunnels = new HashMap<>(); - private int clientPortSeq = 30000; - private int serverSeq = 0; - private String tunnelConfigDirPrefix; - private Router router; - public TunnelControl(Router router, File tunnelConfigDir) { - this.router = router; - tunnelConfigDir.delete(); - tunnelConfigDir.mkdir(); - this.tunnelConfigDirPrefix = tunnelConfigDir.getAbsolutePath() + File.separator; - } - - @Override - public void run() { - - // listen for socket connections to the tunnel controller. - // listen for the commands on port 30000: - // server.create // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port - // server.destroy // closes the tunnel listening for connections on the specified destination public key, and returns OK - // client.create // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key - // client.destroy // closes the tunnel listening for connections on the specified port, and returns OK - // socks.create // creates a socks proxy listening on the specified port - // socks.destroy // closes the socks proxy listening on the specified port, and returns OK - // - // send a command with bash: exec 3<>/dev/tcp/localhost/30000; echo "server.create localhost 80" >&3; cat <&3 - // - try (var listener = new ServerSocket(clientPortSeq++, 0, InetAddress.getLoopbackAddress())) { - while (true) { - try (var socket = listener.accept()) { - var out = new PrintWriter(socket.getOutputStream(), true); - var in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - var args = in.readLine().split(" "); - - if(args[0].equals("server.create")) { - String destHost = args[1]; - String destPort = args[2]; - String seckeyPath = tunnelConfigDirPrefix + "seckey."+serverSeq+".dat"; - String pubkeyPath = tunnelConfigDirPrefix + "pubkey."+serverSeq+".dat"; - serverSeq++; - new I2PTunnel(new String[]{"-die", "-nocli", "-e", "genkeys " + seckeyPath + " " + pubkeyPath}); - String destPubKey = Base64.encode(Files.readAllBytes(new File(pubkeyPath).toPath())); - - // listen using the I2P server keypair, and forward incoming connections to a destination and port - new Thread(()->{ - I2PTunnel t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "server "+destHost+" "+destPort+" " + seckeyPath}); - serverTunnels.put(destPubKey, t); - }).start(); - try (var fis = new FileInputStream(seckeyPath)) { - Destination d = new Destination(); - d.readBytes(fis); - out.println(d.toBase32()); - } - } - else if(args[0].equals("server.destroy")) { - String destPubKey = args[1]; - new Thread(()->{ - var t = serverTunnels.get(destPubKey); - serverTunnels.remove(destPubKey); - t.runClose(new String[]{"forced", "all"}, t); - }).start(); - out.println("OK"); - } - else if(args[0].equals("client.create")) { - String destPubKey = args[1]; - int port = clientPortSeq++; - new Thread(()->{ - var t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "client " + port + " " + destPubKey}); - clientTunnels.put(port, t); - }).start(); - out.println(port); - } - else if(args[0].equals("client.destroy")) { - int port = Integer.parseInt(args[1]); - new Thread(()->{ - var t = clientTunnels.get(port); - clientTunnels.remove(port); - t.runClose(new String[]{"forced", "all"}, t); - }).start(); - out.println("OK"); - } - else if(args[0].equals("socks.create")) { - int port = Integer.parseInt(args[1]); - new Thread(()->{ - var t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "sockstunnel " + port}); - socksTunnels.put(port, t); - }).start(); - out.println("OK"); - } - else if(args[0].equals("socks.destroy")) { - int port = Integer.parseInt(args[1]); - new Thread(()->{ - var t = socksTunnels.get(port); - socksTunnels.remove(port); - t.runClose(new String[]{"forced", "all"}, t); - }).start(); - out.println("OK"); - } - else if(args[0].equals("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"); - } - - } - catch (Exception e) { - e.printStackTrace(); - } - } - } - catch (Exception e) { - e.printStackTrace(); - } - - } -}