You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
i2p-zero/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java

439 lines
15 KiB

package org.getmonero.i2p.zero;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.CommSystemFacade;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.router.transport.FIFOBandwidthRefiller;
import net.i2p.router.transport.TransportUtil;
import org.json.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
import java.util.HashMap;
import java.util.Optional;
import java.util.Properties;
public class RouterWrapper {
private Router router;
private boolean started = false;
private Properties routerProperties;
private TunnelControl tunnelControl;
private File i2PConfigDir;
private File i2PBaseDir;
private Runnable updateAvailableCallback;
public int routerExternalPort;
public RouterWrapper(Properties routerProperties, Runnable updateAvailableCallback) {
this.routerProperties = routerProperties;
this.updateAvailableCallback = updateAvailableCallback;
if(!routerProperties.contains("i2p.dir.base.template")) {
routerProperties.put("i2p.dir.base.template", new File(new File(System.getProperty("java.home")), "i2p.base").getAbsolutePath());
}
int bandwidthLimitKBps = loadBandwidthLimitKBps();
routerProperties.put("i2np.inboundKBytesPerSecond", bandwidthLimitKBps);
routerProperties.put("i2np.outboundKBytesPerSecond", bandwidthLimitKBps);
routerProperties.put("i2p.dir.base", System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "base");
routerProperties.put("i2p.dir.config", System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "config");
i2PConfigDir = new File(routerProperties.getProperty("i2p.dir.config"));
boolean createdConfigDir = false;
if(!i2PConfigDir.exists()) {
i2PConfigDir.mkdirs();
createdConfigDir = true;
}
i2PBaseDir = new File(routerProperties.getProperty("i2p.dir.base"));
if(!i2PBaseDir.exists()) {
i2PBaseDir.mkdirs();
copyFolderRecursively(Path.of(routerProperties.getProperty("i2p.dir.base.template")), i2PBaseDir.toPath());
}
if(createdConfigDir)
copyFolderRecursively(i2PBaseDir.toPath().resolve("hosts.txt") , i2PConfigDir.toPath().resolve("hosts.txt"));
}
public Router getRouter() {
return router;
}
public static void copyFolderRecursively(Path src, Path dest) {
try {
Files.walk(src).forEach(s -> {
try {
Path d = dest.resolve(src.relativize(s));
if(Files.isDirectory(s)) {
if(!Files.exists(d)) Files.createDirectory(d);
return;
}
Files.copy(s, d);
} catch(Exception e) {
e.printStackTrace();
}
});
} catch(Exception ex) {
ex.printStackTrace();
}
}
public boolean isStarted() {
return started;
}
public void start(Runnable routerIsAliveCallback) {
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()) {
try {
File routerConfigFile = new File(i2PConfigDir, "router.config");
if(!(routerConfigFile.exists() && routerConfigFile.canRead())) {
Thread.sleep(100);
continue;
}
Optional<String> portString = Files.lines(routerConfigFile.toPath()).filter(s -> s.startsWith("i2np.udp.port=")).findFirst();
if(portString.isEmpty()) {
Thread.sleep(100);
continue;
}
routerExternalPort = Integer.parseInt(portString.get().split("=")[1]);
break;
}
catch (Exception e) {
e.printStackTrace();
}
}
else {
Thread.sleep(1000);
System.out.println("Waiting for I2P router to start...");
}
}
System.out.println("I2P router now running");
new Thread(routerIsAliveCallback).start();
tunnelControl = new TunnelControl(this, i2PConfigDir, new File(i2PConfigDir, "tunnelTemp"));
new Thread(tunnelControl).start();
UpdateCheck.scheduleUpdateCheck(i2PConfigDir, router, updateAvailableCallback);
}
catch (Exception e) {
e.printStackTrace();
}
}).start();
}).start();
}
public void stop(boolean fastStopAndShutDown) {
if(!started) return;
started = false;
tunnelControl.stop(fastStopAndShutDown);
tunnelControl = null;
System.out.println("I2P router will shut down gracefully");
router.shutdownGracefully();
if(fastStopAndShutDown) {
// don't wait more than 2 seconds for shutdown. If tunnels are still opening, they can pause for up to 20 seconds, which is too long
new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.exit(0);
}).start();
}
}
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
updateBandwidthLimitKBps(n);
}
}).start();
}
public static final int defaultBandwidthKBps = (int)(0.5*1024)/8; // 0.5 MBit/s
public int loadBandwidthLimitKBps() {
try {
File configFile = new File(i2PConfigDir, "config.json");
if (configFile.exists()) {
JSONObject root = new JSONObject(Files.readString(configFile.toPath()));
return ((Long) root.get("bandwidthLimitKBps")).intValue();
} else return defaultBandwidthKBps;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public int getBandwidthLimitKBps() {
return Integer.parseInt(routerProperties.get("i2np.inboundKBytesPerSecond").toString());
}
public void updateBandwidthLimitKBps(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");
try {
File configFile = new File(i2PConfigDir, "config.json");
JSONObject root = new JSONObject();
if (configFile.exists()) {
root = new JSONObject(Files.readString(configFile.toPath()));
}
root.put("bandwidthLimitKBps", n);
Files.writeString(configFile.toPath(), root.toString(2));
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
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;
}
public enum NetworkState {
HIDDEN,
TESTING,
FIREWALLED,
RUNNING,
WARN,
ERROR,
CLOCKSKEW,
VMCOMM;
}
public static class NetworkStateMessage {
private NetworkState state;
private String msg;
NetworkStateMessage(NetworkState state, String msg) {
setMessage(state, msg);
}
public void setMessage(NetworkState state, String msg) {
this.state = state;
this.msg = msg;
}
public NetworkState getState() {
return state;
}
public String getMessage() {
return msg;
}
@Override
public String toString() {
return "(" + state + "; " + msg + ')';
}
}
public String _t(String s) {
return s;
}
final static String PROP_I2NP_NTCP_HOSTNAME = "i2np.ntcp.hostname";
final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port";
public boolean isRouterRunning() {
if(router==null) return false;
return router.isRunning();
}
public void waitForRouterRunning() {
while(!isRouterRunning()) { try { Thread.sleep(100); } catch (InterruptedException e) {} }
}
public RouterContext getRouterContext() {
return router.getContext();
}
public NetworkStateMessage getReachability() {
try {
RouterContext _context = router.getContext();
if (_context.commSystem().isDummy())
return new NetworkStateMessage(NetworkState.VMCOMM, "VM Comm System");
if (_context.router().getUptime() > 60*1000 && (!_context.router().gracefulShutdownInProgress()) &&
!_context.clientManager().isAlive())
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Client Manager I2CP Error - check logs")); // not a router problem but the user should know
// Warn based on actual skew from peers, not update status, so if we successfully offset
// the clock, we don't complain.
//if (!_context.clock().getUpdatedSuccessfully())
long skew = _context.commSystem().getFramedAveragePeerClockSkew(33);
// Display the actual skew, not the offset
if (Math.abs(skew) > 30*1000)
return new NetworkStateMessage(NetworkState.CLOCKSKEW, _t("ERR-Clock Skew"));
if (_context.router().isHidden())
return new NetworkStateMessage(NetworkState.HIDDEN, _t("Hidden"));
RouterInfo routerInfo = _context.router().getRouterInfo();
if (routerInfo == null)
return new NetworkStateMessage(NetworkState.TESTING, _t("Testing"));
CommSystemFacade.Status status = _context.commSystem().getStatus();
NetworkState state = NetworkState.RUNNING;
switch (status) {
case OK:
case IPV4_OK_IPV6_UNKNOWN:
case IPV4_OK_IPV6_FIREWALLED:
case IPV4_UNKNOWN_IPV6_OK:
case IPV4_DISABLED_IPV6_OK:
case IPV4_SNAT_IPV6_OK:
RouterAddress ra = routerInfo.getTargetAddress("NTCP");
if (ra == null)
return new NetworkStateMessage(NetworkState.RUNNING, _t(status.toStatusString()));
byte[] ip = ra.getIP();
if (ip == null)
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Unresolved TCP Address"));
// TODO set IPv6 arg based on configuration?
if (TransportUtil.isPubliclyRoutable(ip, true))
return new NetworkStateMessage(NetworkState.RUNNING, _t(status.toStatusString()));
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Private TCP Address"));
case IPV4_SNAT_IPV6_UNKNOWN:
case DIFFERENT:
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-SymmetricNAT"));
case REJECT_UNSOLICITED:
state = NetworkState.FIREWALLED;
case IPV4_DISABLED_IPV6_FIREWALLED:
if (routerInfo.getTargetAddress("NTCP") != null)
return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with Inbound TCP Enabled"));
// fall through...
case IPV4_FIREWALLED_IPV6_OK:
case IPV4_FIREWALLED_IPV6_UNKNOWN:
if (((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled())
return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Floodfill"));
//if (_context.router().getRouterInfo().getCapabilities().indexOf('O') >= 0)
// return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Fast"));
return new NetworkStateMessage(state, _t(status.toStatusString()));
case DISCONNECTED:
return new NetworkStateMessage(NetworkState.TESTING, _t("Disconnected - check network connection"));
case HOSED:
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Port In Use - Set i2np.udp.internalPort=xxxx in advanced config and restart"));
case UNKNOWN:
state = NetworkState.TESTING;
case IPV4_UNKNOWN_IPV6_FIREWALLED:
case IPV4_DISABLED_IPV6_UNKNOWN:
default:
ra = routerInfo.getTargetAddress("SSU");
if (ra == null && _context.router().getUptime() > 5*60*1000) {
if (_context.commSystem().countActivePeers() <= 0)
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-No Active Peers, Check Network Connection and Firewall"));
else if (_context.getProperty(PROP_I2NP_NTCP_HOSTNAME) == null ||
_context.getProperty(PROP_I2NP_NTCP_PORT) == null)
return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Disabled and Inbound TCP host/port not set"));
else
return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with UDP Disabled"));
}
return new NetworkStateMessage(state, _t(status.toStatusString()));
}
}
catch(Exception e) {
return new NetworkStateMessage(NetworkState.WARN, "Starting...");
}
}
}