Code refactored, new cross platform I2P-zero GUI

pull/4/head
knaccc 5 years ago
parent 72de15f401
commit 7374e9455c

@ -1,6 +1,6 @@
<img src="https://github.com/knaccc/i2p-zero/blob/master/i2p-zero.png" align="left" width="336" height="124">
## 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 <a href="https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads">Microsoft Visual C++ Redistributable</a>
If it launches successfully, you'll see the message:

@ -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"

@ -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

@ -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

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="org.getmonero.i2p.zero" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx-swt.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.base.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.controls.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.fxml.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.graphics.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.media.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.swing.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdk-11.0.2/lib/javafx.web.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

@ -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;
}

@ -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
}
}
});
}
}

@ -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<Tunnel> 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<Tunnel> tunnelTableList = FXCollections.observableArrayList();
private Stage getStage() {
return (Stage) rootBorderPane.getScene().getWindow();
}
@FXML private void initialize() {
typeCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("type"));
hostCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("host"));
portCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("port"));
i2PCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("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<Event> 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();
}
}

@ -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);
}
}

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="241.0" prefWidth="770.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.AddTunnelController">
<children>
<Label layoutX="18.0" layoutY="9.0" prefHeight="27.0" prefWidth="120.0" text="Add a tunnel">
</Label>
<RadioButton fx:id="clientTunnelRadioButton" layoutX="19.0" layoutY="48.0" mnemonicParsing="false" selected="true" style="" text="Client tunnel - create a local port that forwards requests to a specified I2P destination address">
<toggleGroup>
<ToggleGroup fx:id="tunnelType" />
</toggleGroup>
</RadioButton>
<RadioButton fx:id="serverTunnelRadioButton" layoutX="19.0" layoutY="73.0" mnemonicParsing="false" text="Server tunnel - create an I2P destination address that will forward requests to the specified host and port" toggleGroup="$tunnelType" />
<RadioButton fx:id="socksProxyRadioButton" layoutX="19.0" layoutY="97.0" mnemonicParsing="false" text="SOCKS proxy - applications (like web browsers) that support SOCKS can use your proxy to connect to I2P destinations" toggleGroup="$tunnelType" />
<Button fx:id="addButton" defaultButton="true" disable="true" layoutX="712.0" layoutY="197.0" mnemonicParsing="false" text="Add" />
<Button fx:id="cancelButton" cancelButton="true" layoutX="647.0" layoutY="197.0" mnemonicParsing="false" text="Cancel" />
<Pane fx:id="clientTunnelConfigPane" layoutX="27.0" layoutY="133.0" prefHeight="67.0" prefWidth="523.0">
<children>
<TextField fx:id="clientDestAddrField" layoutX="165.0" prefHeight="27.0" prefWidth="353.0" />
<Label layoutX="1.0" layoutY="4.0" text="I2P destination address" />
<TextField fx:id="clientPortField" layoutX="165.0" layoutY="33.0" prefHeight="28.0" prefWidth="84.0" />
<Label layoutX="1.0" layoutY="37.0" text="Local port" />
</children>
</Pane>
<Pane fx:id="serverTunnelConfigPane" layoutX="27.0" layoutY="132.0" prefHeight="97.0" prefWidth="523.0" visible="false">
<children>
<TextField fx:id="serverHostField" layoutX="135.0" />
<Label layoutX="1.0" layoutY="5.0" text="Destination host" />
<TextField fx:id="serverPortField" layoutX="478.0" prefHeight="27.0" prefWidth="77.0" />
<Label layoutX="366.0" layoutY="5.0" text="Destination port" />
<TextField fx:id="serverKeyField" layoutX="135.0" layoutY="33.0" prefHeight="28.0" prefWidth="420.0" />
<Label layoutX="1.0" layoutY="38.0" text="Server secret key" />
<TextField fx:id="serverAddrField" layoutX="135.0" layoutY="66.0" prefHeight="28.0" prefWidth="420.0" />
<Label layoutX="1.0" layoutY="71.0" text="Server public address" />
</children>
</Pane>
<Pane fx:id="socksProxyConfigPane" layoutX="27.0" layoutY="132.0" prefHeight="39.0" prefWidth="523.0" visible="false">
<children>
<TextField fx:id="socksPortField" layoutX="124.0" prefHeight="27.0" prefWidth="77.0" />
<Label layoutX="1.0" layoutY="5.0" text="Local SOCKS port" />
</children>
</Pane>
</children>
</AnchorPane>

@ -0,0 +1,44 @@
@font-face {
font-family: 'Open Sans Regular';
src: url('OpenSans-Regular.ttf');
}
.tab .tab-label {
-fx-font-size: 12px;
-fx-font-family: 'Open Sans Regular';
}
.text-area {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}
.radio-button {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}
.label {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}
.button {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}
.text-field {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}
.slider {
-fx-font-size: 12px;
-fx-font-family: 'Open Sans Regular';
}
.table-view {
-fx-font-size: 13px;
-fx-font-family: 'Open Sans Regular';
}

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.StackPane?>
<BorderPane fx:id="rootBorderPane" minHeight="340.0" minWidth="360.0" prefHeight="340.0" prefWidth="360.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.Controller">
<center>
<TabPane>
<tabs>
<Tab fx:id="bandwidthTab" closable="false" text="Bandwidth">
<content>
<BorderPane minHeight="-Infinity" minWidth="-Infinity">
<center>
<StackPane BorderPane.alignment="CENTER">
<children>
<AnchorPane maxHeight="230.0" maxWidth="340.0" minHeight="211.0" minWidth="340.0" prefHeight="216.0" prefWidth="340.0">
<children>
<Slider fx:id="bandwidthSlider" blockIncrement="0.5" layoutX="19.0" layoutY="165.0" majorTickUnit="5.0" max="20.0" min="0.5" minorTickCount="0" prefHeight="33.0" prefWidth="295.0" showTickLabels="true" value="0.5" />
<Label layoutX="22.0" layoutY="134.0" style="-fx-font-size: 16;" styleClass="bandwidthLabel" text="Max. Bandwidth">
</Label>
<Label fx:id="maxBandwidthLabel" layoutX="166.0" layoutY="134.0" prefHeight="22.0" prefWidth="149.0" style="-fx-font-size: 16;" text="0.5 Mbps" textAlignment="right">
</Label>
<Label layoutX="22.0" layoutY="3.0" style="-fx-font-size: 16;" text="Bandwidth usage">
</Label>
<Label layoutX="22.0" layoutY="34.0" styleClass="bandwidth" text="1 second average">
</Label>
<Label layoutX="22.0" layoutY="54.0" styleClass="bandwidth" text="5 minute averge">
</Label>
<Label layoutX="22.0" layoutY="74.0" text="All-time average">
</Label>
<Label layoutX="22.0" layoutY="94.0" text="Total transferred">
</Label>
<Label fx:id="bandwidthIn1s" alignment="CENTER_RIGHT" layoutX="140.0" layoutY="34.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="bandwidthIn5m" alignment="CENTER_RIGHT" layoutX="140.0" layoutY="54.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="bandwidthInAll" alignment="CENTER_RIGHT" layoutX="140.0" layoutY="74.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="totalTransferredIn" alignment="CENTER_RIGHT" layoutX="140.0" layoutY="94.0" prefHeight="18.0" prefWidth="80.0" text="0.00 MB">
</Label>
<Label alignment="CENTER_RIGHT" layoutX="140.0" layoutY="14.0" prefHeight="18.0" prefWidth="80.0" text="In">
</Label>
<Label fx:id="bandwidthOut1s" alignment="CENTER_RIGHT" layoutX="231.0" layoutY="34.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="bandwidthOut5m" alignment="CENTER_RIGHT" layoutX="231.0" layoutY="54.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="bandwidthOutAll" alignment="CENTER_RIGHT" layoutX="231.0" layoutY="74.0" prefHeight="18.0" prefWidth="80.0" text="0.00 KBps">
</Label>
<Label fx:id="totalTransferredOut" alignment="CENTER_RIGHT" layoutX="231.0" layoutY="94.0" prefHeight="18.0" prefWidth="80.0" text="0.00 MB">
</Label>
<Label alignment="CENTER_RIGHT" layoutX="231.0" layoutY="14.0" prefHeight="18.0" prefWidth="80.0" text="Out">
</Label>
</children>
<StackPane.margin>
<Insets top="16.0" />
</StackPane.margin>
</AnchorPane>
<AnchorPane fx:id="bandwidthDisabledOverlay" opacity="0.61" prefHeight="79.0" prefWidth="143.0" style="-fx-background-color: #fff;" visible="false" />
</children>
</StackPane>
</center>
</BorderPane>
</content>
</Tab>
<Tab fx:id="tunnelsTab" closable="false" text="Tunnels">
<content>
<BorderPane layoutX="9.0" layoutY="56.0" prefWidth="355.0">
<center>
<TableView fx:id="tunnelsTableView" prefHeight="139.0" prefWidth="318.0">
<columns>
<TableColumn fx:id="typeCol" prefWidth="93.0" text="Type" />
<TableColumn fx:id="hostCol" prefWidth="186.0" text="Host" />
<TableColumn fx:id="portCol" minWidth="0.0" prefWidth="70.0" text="Port" />
<TableColumn fx:id="i2PCol" prefWidth="280.0" text="I2P address" />
</columns>
</TableView>
</center>
<bottom>
<BorderPane>
<right>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="32.0" prefWidth="339.0" BorderPane.alignment="CENTER">
<children>
<Button fx:id="tunnelAddButton" layoutX="301.0" layoutY="7.0" mnemonicParsing="false" text="Add">
</Button>
<Button fx:id="tunnelRemoveButton" disable="true" layoutX="231.0" layoutY="7.0" mnemonicParsing="false" text="Remove">
</Button>
</children>
</AnchorPane>
</right>
</BorderPane>
</bottom>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</BorderPane>
</content>
</Tab>
<Tab fx:id="helpTab" closable="false" text="Help">
<content>
<BorderPane>
<center>
<TextArea fx:id="helpTextArea" editable="false" text="I2P hides your IP address when you connect to other I2P destinations. &#10;&#10;For example, if you are using a Monero wallet: When your Monero wallet needs to announce a transaction, none of the other Monero I2P nodes that it announces the transaction to will be able to know your IP address.&#10;&#10;You can create your own I2P destination addresses using the Tunnels tab. Connections received to these I2P addresses will be forwarded to the destination of your choice (such as a local web server). You can also create your own client tunnels, which will allow existing software to easily communicate with remote I2P destinations.&#10;&#10;I2P achives privacy by routing your traffic through a series of other I2P nodes. Each node between you and your final destination will not be able to tell whether the next node in the chain is your final destination.&#10;&#10;The nodes you connect to will not know whether you are originating a tunnel, or whether you're simply acting as a link in the chain of somebody else's tunnel.&#10;&#10;I2P automatically encrypts all traffic, so no node will be able to know what kind of traffic it is forwarding on behalf of others. &#10;&#10;For more information, visit https://geti2p.net" wrapText="true" BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets />
</BorderPane.margin>
<opaqueInsets>
<Insets />
</opaqueInsets></TextArea>
</center>
</BorderPane>
</content>
</Tab>
</tabs>
</TabPane>
</center>
<top>
<BorderPane prefHeight="76.0" prefWidth="400.0" style="-fx-background-color: #fff;" BorderPane.alignment="CENTER">
<left>
<Pane prefHeight="71.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children>
<ImageView fitHeight="150.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@i2p-zero.png" />
</image>
</ImageView>
</children>
</Pane>
</left>
<right>
<Pane prefHeight="76.0" prefWidth="155.0" style="-fx-background-color: #fff;" BorderPane.alignment="CENTER">
<children>
<ImageView fx:id="masterToggle" fitHeight="34.0" fitWidth="101.0" layoutX="23.0" layoutY="10.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@toggle-on.png" />
</image>
</ImageView>
<Label fx:id="statusLabel" layoutY="44.0" prefHeight="18.0" prefWidth="148.0" style="-fx-alignment: center;" text="Status: Firewalled">
</Label>
</children>
</Pane>
</right>
</BorderPane>
</top>
</BorderPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/i2p.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/i2ptunnel.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/mstreaming.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/router.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/sam.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/streaming.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

@ -6,4 +6,5 @@ module org.getmonero.i2p.zero {
requires sam;
requires i2ptunnel;
requires jdk.crypto.ec;
exports org.getmonero.i2p.zero;
}

@ -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();
}

@ -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<String, String>();
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;
}
}

@ -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<Tunnel> 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<Tunnel> 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 <host> <port> // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port
// server.destroy <i2p destination public key> // closes the tunnel listening for connections on the specified destination public key, and returns OK
// client.create <i2p destination public key> // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key
// client.destroy <port> // closes the tunnel listening for connections on the specified port, and returns OK
// socks.create <port> // creates a socks proxy listening on the specified port
// socks.destroy <port> // 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;
}
}

@ -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 %*

@ -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

@ -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<Integer, I2PTunnel> clientTunnels = new HashMap<>();
private Map<String, I2PTunnel> serverTunnels = new HashMap<>();
private Map<Integer, I2PTunnel> 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 <host> <port> // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port
// server.destroy <i2p destination public key> // closes the tunnel listening for connections on the specified destination public key, and returns OK
// client.create <i2p destination public key> // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key
// client.destroy <port> // closes the tunnel listening for connections on the specified port, and returns OK
// socks.create <port> // creates a socks proxy listening on the specified port
// socks.destroy <port> // 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();
}
}
}
Loading…
Cancel
Save