forked from I2P_Developers/i2p.i2p
i2ptunnel: Add subsession support to servers, no UI yet
Update subsession javadocs
This commit is contained in:
@@ -313,8 +313,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subsession to a shared client if necessary.
|
||||
* Add a DSA_SHA1 subsession to the shared client if necessary.
|
||||
*
|
||||
* @return subsession, or null if none was added
|
||||
* @since 0.9.20
|
||||
*/
|
||||
protected static synchronized I2PSession addSubsession(I2PTunnel tunnel) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -29,12 +30,14 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory;
|
||||
import net.i2p.crypto.SigType;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
@@ -67,6 +70,8 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
private static final boolean DEFAULT_USE_POOL = true;
|
||||
public static final String PROP_USE_SSL = "useSSL";
|
||||
public static final String PROP_UNIQUE_LOCAL = "enableUniqueLocal";
|
||||
/** @since 0.9.30 */
|
||||
public static final String PROP_ALT_PKF = "altPrivKeyFile";
|
||||
/** apparently unused */
|
||||
protected static volatile long __serverId = 0;
|
||||
/** max number of threads - this many slowlorisses will DOS this server, but too high could OOM the JVM */
|
||||
@@ -217,6 +222,9 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
portNum, props);
|
||||
rv.setName("I2PTunnel Server");
|
||||
getTunnel().addSession(rv.getSession());
|
||||
String alt = props.getProperty(PROP_ALT_PKF);
|
||||
if (alt != null)
|
||||
addSubsession(rv, alt);
|
||||
return rv;
|
||||
} catch (I2PSessionException ise) {
|
||||
throw new IllegalArgumentException("Can't create socket manager", ise);
|
||||
@@ -225,6 +233,44 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a non-DSA_SHA1 subsession to the DSA_SHA1 server if necessary.
|
||||
*
|
||||
* @return subsession, or null if none was added
|
||||
* @since 0.9.30
|
||||
*/
|
||||
private I2PSession addSubsession(I2PSocketManager sMgr, String alt) {
|
||||
File altFile = TunnelController.filenameToFile(alt);
|
||||
if (alt == null)
|
||||
return null;
|
||||
I2PSession sess = sMgr.getSession();
|
||||
if (sess.getMyDestination().getSigType() != SigType.DSA_SHA1)
|
||||
return null;
|
||||
Properties props = new Properties();
|
||||
props.putAll(getTunnel().getClientOptions());
|
||||
// fixme get actual sig type
|
||||
String name = props.getProperty("inbound.nickname");
|
||||
if (name != null)
|
||||
props.setProperty("inbound.nickname", name + " (EdDSA)");
|
||||
name = props.getProperty("outbound.nickname");
|
||||
if (name != null)
|
||||
props.setProperty("outbound.nickname", name + " (EdDSA)");
|
||||
props.setProperty(I2PClient.PROP_SIGTYPE, "EdDSA_SHA512_Ed25519");
|
||||
FileInputStream privData = null;
|
||||
try {
|
||||
privData = new FileInputStream(altFile);
|
||||
return sMgr.addSubsession(privData, props);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Failed to add subssession", ioe);
|
||||
return null;
|
||||
} catch (I2PSessionException ise) {
|
||||
_log.error("Failed to add subssession", ise);
|
||||
return null;
|
||||
} finally {
|
||||
if (privData != null) try { privData.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Warning, blocks while connecting to router and building tunnels;
|
||||
@@ -238,6 +284,22 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
while (sockMgr.getSession().isClosed()) {
|
||||
try {
|
||||
sockMgr.getSession().connect();
|
||||
// Now connect the subsessions, if any
|
||||
List<I2PSession> subs = sockMgr.getSubsessions();
|
||||
if (!subs.isEmpty()) {
|
||||
for (I2PSession sub : subs) {
|
||||
try {
|
||||
sub.connect();
|
||||
if (_log.shouldInfo())
|
||||
_log.info("Connected subsession " + sub);
|
||||
} catch (I2PSessionException ise) {
|
||||
// not fatal?
|
||||
String msg = "Unable to connect subsession " + sub;
|
||||
this.l.log(msg);
|
||||
_log.error(msg, ise);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (I2PSessionException ise) {
|
||||
// try to make this error sensible as it will happen...
|
||||
String portNum = getTunnel().port;
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.i2p.i2ptunnel;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
@@ -16,12 +17,22 @@ import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.crypto.KeyGenerator;
|
||||
import net.i2p.crypto.SigType;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.KeyCertificate;
|
||||
import net.i2p.data.PrivateKey;
|
||||
import net.i2p.data.PrivateKeyFile;
|
||||
import net.i2p.data.PublicKey;
|
||||
import net.i2p.data.SigningPrivateKey;
|
||||
import net.i2p.data.SigningPublicKey;
|
||||
import net.i2p.data.SimpleDataStructure;
|
||||
import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.RandomSource;
|
||||
import net.i2p.util.SecureFile;
|
||||
import net.i2p.util.SecureFileOutputStream;
|
||||
import net.i2p.util.SystemVersion;
|
||||
@@ -83,6 +94,8 @@ public class TunnelController implements Logging {
|
||||
private static final String OPT_TAGS_SEND = PFX_OPTION + "crypto.tagsToSend";
|
||||
private static final String OPT_LOW_TAGS = PFX_OPTION + "crypto.lowTagThreshold";
|
||||
private static final String OPT_SIG_TYPE = PFX_OPTION + I2PClient.PROP_SIGTYPE;
|
||||
/** @since 0.9.30 */
|
||||
private static final String OPT_ALT_PKF = PFX_OPTION + I2PTunnelServer.PROP_ALT_PKF;
|
||||
|
||||
/** all of these @since 0.9.14 */
|
||||
public static final String TYPE_CONNECT = "connectclient";
|
||||
@@ -106,7 +119,7 @@ public class TunnelController implements Logging {
|
||||
*/
|
||||
public static final SigType PREFERRED_SIGTYPE;
|
||||
static {
|
||||
if (SystemVersion.isARM() || SystemVersion.isGNU() || SystemVersion.isAndroid()) {
|
||||
if (SystemVersion.isGNU() || SystemVersion.isAndroid()) {
|
||||
if (SigType.ECDSA_SHA256_P256.isAvailable())
|
||||
PREFERRED_SIGTYPE = SigType.ECDSA_SHA256_P256;
|
||||
else
|
||||
@@ -146,8 +159,13 @@ public class TunnelController implements Logging {
|
||||
setConfig(config, prefix);
|
||||
_messages = new ArrayList<String>(4);
|
||||
boolean keyOK = true;
|
||||
if (createKey && (getType().endsWith("server") || getPersistentClientKey()))
|
||||
if (createKey && (getType().endsWith("server") || getPersistentClientKey())) {
|
||||
keyOK = createPrivateKey();
|
||||
if (keyOK && getType().endsWith("server") && !getType().equals(TYPE_STREAMR_SERVER)) {
|
||||
// check rv?
|
||||
createAltPrivateKey();
|
||||
}
|
||||
}
|
||||
_state = keyOK && getStartOnLoad() ? TunnelState.START_ON_LOAD : TunnelState.STOPPED;
|
||||
}
|
||||
|
||||
@@ -186,7 +204,7 @@ public class TunnelController implements Logging {
|
||||
String destStr = dest.toBase64();
|
||||
log("Private key created and saved in " + keyFile.getAbsolutePath());
|
||||
log("You should backup this file in a secure place.");
|
||||
log("New destination: " + destStr);
|
||||
log("New alternate destination: " + destStr);
|
||||
String b32 = dest.toBase32();
|
||||
log("Base32: " + b32);
|
||||
File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
|
||||
@@ -214,6 +232,99 @@ public class TunnelController implements Logging {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates alternate Destination with the same encryption keys as the primary Destination,
|
||||
* but a different signing key.
|
||||
*
|
||||
* Must have already called createPrivateKey() successfully.
|
||||
* Does nothing unless option OPT_ALT_PKF is set with the privkey file name.
|
||||
* Does nothing if the file already exists.
|
||||
*
|
||||
* @return success
|
||||
* @since 0.9.30
|
||||
*/
|
||||
private boolean createAltPrivateKey() {
|
||||
if (PREFERRED_SIGTYPE == SigType.DSA_SHA1)
|
||||
return false;
|
||||
File keyFile = getPrivateKeyFile();
|
||||
if (keyFile == null)
|
||||
return false;
|
||||
if (!keyFile.exists())
|
||||
return false;
|
||||
File altFile = getAlternatePrivateKeyFile();
|
||||
if (altFile == null)
|
||||
return false;
|
||||
if (altFile.exists())
|
||||
return true;
|
||||
PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
|
||||
FileOutputStream out = null;
|
||||
try {
|
||||
Destination dest = pkf.getDestination();
|
||||
if (dest == null)
|
||||
return false;
|
||||
if (dest.getSigType() != SigType.DSA_SHA1)
|
||||
return false;
|
||||
PublicKey pub = dest.getPublicKey();
|
||||
PrivateKey priv = pkf.getPrivKey();
|
||||
SimpleDataStructure[] signingKeys = KeyGenerator.getInstance().generateSigningKeys(PREFERRED_SIGTYPE);
|
||||
SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0];
|
||||
SigningPrivateKey signingPrivKey = (SigningPrivateKey) signingKeys[1];
|
||||
KeyCertificate cert = new KeyCertificate(signingPubKey);
|
||||
Destination d = new Destination();
|
||||
d.setPublicKey(pub);
|
||||
d.setSigningPublicKey(signingPubKey);
|
||||
d.setCertificate(cert);
|
||||
int len = signingPubKey.length();
|
||||
if (len < 128) {
|
||||
byte[] pad = new byte[128 - len];
|
||||
RandomSource.getInstance().nextBytes(pad);
|
||||
d.setPadding(pad);
|
||||
} else if (len > 128) {
|
||||
// copy of excess data handled in KeyCertificate constructor
|
||||
}
|
||||
|
||||
out = new SecureFileOutputStream(altFile);
|
||||
d.writeBytes(out);
|
||||
priv.writeBytes(out);
|
||||
signingPrivKey.writeBytes(out);
|
||||
try { out.close(); } catch (IOException ioe) {}
|
||||
|
||||
String destStr = d.toBase64();
|
||||
log("Alternate private key created and saved in " + altFile.getAbsolutePath());
|
||||
log("You should backup this file in a secure place.");
|
||||
log("New destination: " + destStr);
|
||||
String b32 = d.toBase32();
|
||||
log("Base32: " + b32);
|
||||
File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
|
||||
if (backupDir.isDirectory() || backupDir.mkdir()) {
|
||||
String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat";
|
||||
File backup = new File(backupDir, name);
|
||||
if (FileUtil.copy(altFile, backup, false, true)) {
|
||||
SecureFileOutputStream.setPerms(backup);
|
||||
log("Alternate private key backup saved to " + backup.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (GeneralSecurityException e) {
|
||||
log("Error creating keys " + e);
|
||||
return false;
|
||||
} catch (I2PSessionException e) {
|
||||
log("Error creating keys " + e);
|
||||
return false;
|
||||
} catch (I2PException e) {
|
||||
log("Error creating keys " + e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
log("Error creating keys " + e);
|
||||
return false;
|
||||
} catch (RuntimeException e) {
|
||||
log("Error creating keys " + e);
|
||||
return false;
|
||||
} finally {
|
||||
if (out != null) try { out.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void startTunnelBackground() {
|
||||
synchronized (this) {
|
||||
if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD)
|
||||
@@ -797,7 +908,25 @@ public class TunnelController implements Logging {
|
||||
* @since 0.9.17
|
||||
*/
|
||||
public File getPrivateKeyFile() {
|
||||
String f = getPrivKeyFile();
|
||||
return filenameToFile(getPrivKeyFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not necessarily exist.
|
||||
* @return absolute path or null if unset
|
||||
* @since 0.9.30
|
||||
*/
|
||||
public File getAlternatePrivateKeyFile() {
|
||||
return filenameToFile(_config.getProperty(OPT_ALT_PKF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not necessarily exist.
|
||||
* @param f relative or absolute path, may be null
|
||||
* @return absolute path or null
|
||||
* @since 0.9.30
|
||||
*/
|
||||
static File filenameToFile(String f) {
|
||||
if (f == null)
|
||||
return null;
|
||||
f = f.trim();
|
||||
|
||||
@@ -38,6 +38,10 @@ public interface I2PSocketManager {
|
||||
public I2PSession getSession();
|
||||
|
||||
/**
|
||||
* For a server, you must call connect() on the returned object.
|
||||
* Connecting the primary session does NOT connect any subsessions.
|
||||
* If the primary session is not connected, connecting a subsession will connect the primary session first.
|
||||
*
|
||||
* @return a new subsession, non-null
|
||||
* @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
|
||||
* and different signing keys
|
||||
|
||||
@@ -236,6 +236,10 @@ public class I2PSocketManagerFull implements I2PSocketManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* For a server, you must call connect() on the returned object.
|
||||
* Connecting the primary session does NOT connect any subsessions.
|
||||
* If the primary session is not connected, connecting a subsession will connect the primary session first.
|
||||
*
|
||||
* @return a new subsession, non-null
|
||||
* @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
|
||||
* and different signing keys
|
||||
|
||||
@@ -272,8 +272,10 @@ public interface I2PSession {
|
||||
public List<I2PSession> getSubsessions();
|
||||
|
||||
/**
|
||||
* Actually connect the session and start receiving/sending messages
|
||||
*
|
||||
* Actually connect the session and start receiving/sending messages.
|
||||
* Connecting a primary session will not automatically connect subsessions.
|
||||
* Connecting a subsession will automatically connect the primary session
|
||||
* if not previously connected.
|
||||
*/
|
||||
public void connect() throws I2PSessionException;
|
||||
|
||||
|
||||
@@ -548,6 +548,10 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
|
||||
* Disconnect / destroy from another thread may be called simultaneously and
|
||||
* will (should?) interrupt the connect.
|
||||
*
|
||||
* Connecting a primary session will not automatically connect subsessions.
|
||||
* Connecting a subsession will automatically connect the primary session
|
||||
* if not previously connected.
|
||||
*
|
||||
* @throws I2PSessionException if there is a configuration error or the router is
|
||||
* not reachable
|
||||
*/
|
||||
|
||||
@@ -93,6 +93,9 @@ class SubSession extends I2PSessionMuxedImpl {
|
||||
* Disconnect / destroy from another thread may be called simultaneously and
|
||||
* will (should?) interrupt the connect.
|
||||
*
|
||||
* Connecting a subsession will automatically connect the primary session
|
||||
* if not previously connected.
|
||||
*
|
||||
* @throws I2PSessionException if there is a configuration error or the router is
|
||||
* not reachable
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user