- Add support for hostname lookups over I2CP with new
    HostLookup and HostReply messages.
  - Move username / password from CreateSession
    to GetDate for early authentication;
    this is an incompatible chage.
    Outside router context with authentication enabled,
    new clients will not work with old routers.
    Early authentication is not yet enforced, enable with
    i2cp.strictAuth=true. Will change default to true in a later release.
  - Block all actions before authentication.
  - Better disconnect messages to clients for diagnostics
  - Improve lookup command, add auth command in i2ptunnel CLI for testing
  - Don't start ClientWriterRunner thread in constructor
  - Don't flush in ClientWriterRunner unless necessary
  - Send GetDate even in SimpleSession outside of RouterContext
  - Improve SetDate wait logic to reduce locks and break out when
    Disconnect received
  - Add Disconnect handler to SimpleSession
  - I2Ping cleanups
  - Javadocs
This commit is contained in:
zzz
2013-12-21 00:21:48 +00:00
parent 38c02b44b9
commit cc97a19d3c
18 changed files with 1093 additions and 130 deletions

View File

@ -335,12 +335,14 @@ class ClientConnectionRunner {
* This is always bad.
* See ClientMessageEventListener.handleCreateSession()
* for why we don't send a SessionStatusMessage when we do this.
* @param reason will be truncated to 255 bytes
*/
void disconnectClient(String reason) {
disconnectClient(reason, Log.ERROR);
}
/**
* @param reason will be truncated to 255 bytes
* @param logLevel e.g. Log.WARN
* @since 0.8.2
*/
@ -351,6 +353,8 @@ class ClientConnectionRunner {
+ " config: "
+ _config);
DisconnectMessage msg = new DisconnectMessage();
if (reason.length() > 255)
reason = reason.substring(0, 255);
msg.setReason(reason);
try {
doSend(msg);

View File

@ -20,6 +20,7 @@ import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestroySessionMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
import net.i2p.data.i2cp.GetDateMessage;
import net.i2p.data.i2cp.HostLookupMessage;
import net.i2p.data.i2cp.I2CPMessage;
import net.i2p.data.i2cp.I2CPMessageException;
import net.i2p.data.i2cp.I2CPMessageReader;
@ -50,8 +51,11 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
protected final RouterContext _context;
protected final ClientConnectionRunner _runner;
private final boolean _enforceAuth;
private volatile boolean _authorized;
private static final String PROP_AUTH = "i2cp.auth";
/** if true, user/pw must be in GetDateMessage */
private static final String PROP_AUTH_STRICT = "i2cp.strictAuth";
/**
* @param enforceAuth set false for in-JVM, true for socket access
@ -61,6 +65,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
_log = _context.logManager().getLog(ClientMessageEventListener.class);
_runner = runner;
_enforceAuth = enforceAuth;
if ((!_enforceAuth) || !_context.getBooleanProperty(PROP_AUTH))
_authorized = true;
_context.statManager().createRateStat("client.distributeTime", "How long it took to inject the client message into the router", "ClientMessages", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
}
@ -72,6 +78,20 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
if (_runner.isDead()) return;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Message received: \n" + message);
int type = message.getType();
if (!_authorized) {
// TODO change to default true
boolean strict = _context.getBooleanProperty(PROP_AUTH_STRICT);
if ((strict && type != GetDateMessage.MESSAGE_TYPE) ||
(type != CreateSessionMessage.MESSAGE_TYPE &&
type != GetDateMessage.MESSAGE_TYPE &&
type != DestLookupMessage.MESSAGE_TYPE &&
type != GetBandwidthLimitsMessage.MESSAGE_TYPE)) {
_log.error("Received message type " + type + " without required authentication");
_runner.disconnectClient("Authorization required");
return;
}
}
switch (message.getType()) {
case GetDateMessage.MESSAGE_TYPE:
handleGetDate((GetDateMessage)message);
@ -103,6 +123,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
case DestLookupMessage.MESSAGE_TYPE:
handleDestLookup((DestLookupMessage)message);
break;
case HostLookupMessage.MESSAGE_TYPE:
handleHostLookup((HostLookupMessage)message);
break;
case ReconfigureSessionMessage.MESSAGE_TYPE:
handleReconfigureSession((ReconfigureSessionMessage)message);
break;
@ -124,6 +147,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
if (_log.shouldLog(Log.ERROR))
_log.error("Error occurred", error);
// Is this is a little drastic for an unknown message type?
// Send the whole exception string over for diagnostics
_runner.disconnectClient(error.toString());
_runner.stopRunning();
}
@ -137,6 +162,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
String clientVersion = message.getVersion();
if (clientVersion != null)
_runner.setClientVersion(clientVersion);
Properties props = message.getOptions();
if (!checkAuth(props))
return;
try {
// only send version if the client can handle it (0.8.7 or greater)
_runner.doSend(new SetDateMessage(clientVersion != null ? CoreVersion.VERSION : null));
@ -174,24 +202,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
}
// Auth, since 0.8.2
if (_enforceAuth && _context.getBooleanProperty(PROP_AUTH)) {
Properties props = in.getOptions();
String user = props.getProperty("i2cp.username");
String pw = props.getProperty("i2cp.password");
if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
_log.error("I2CP auth failed for client: " + props.getProperty("inbound.nickname"));
_runner.disconnectClient("Authorization required to create session, specify i2cp.username and i2cp.password in session options");
return;
}
PasswordManager mgr = new PasswordManager(_context);
if (!mgr.checkHash(PROP_AUTH, user, pw)) {
_log.error("I2CP auth failed for client: " + props.getProperty("inbound.nickname") + " user: " + user);
_runner.disconnectClient("Authorization failed for Create Session, user = " + user);
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("I2CP auth success for client: " + props.getProperty("inbound.nickname") + " user: " + user);
}
Properties inProps = in.getOptions();
if (!checkAuth(inProps))
return;
SessionId sessionId = new SessionId();
sessionId.setSessionId(getNextSessionId());
@ -213,6 +226,44 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
startCreateSessionJob();
}
/**
* Side effect - sets _authorized.
* Side effect - disconnects session if not authorized.
*
* @param props contains i2cp.username and i2cp.password, may be null
* @return success
* @since 0.9.10
*/
private boolean checkAuth(Properties props) {
if (_authorized)
return true;
if (_enforceAuth && _context.getBooleanProperty(PROP_AUTH)) {
String user = null;
String pw = null;
if (props != null) {
user = props.getProperty("i2cp.username");
pw = props.getProperty("i2cp.password");
}
if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
_log.error("I2CP auth failed");
_runner.disconnectClient("Authorization required, specify i2cp.username and i2cp.password in options");
_authorized = false;
return false;
}
PasswordManager mgr = new PasswordManager(_context);
if (!mgr.checkHash(PROP_AUTH, user, pw)) {
_log.error("I2CP auth failed user: " + user);
_runner.disconnectClient("Authorization failed, user = " + user);
_authorized = false;
return false;
}
if (_log.shouldLog(Log.INFO))
_log.info("I2CP auth success user: " + user);
}
_authorized = true;
return true;
}
/**
* Override for testing
* @since 0.9.8
@ -315,6 +366,15 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
_context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash()));
}
/**
* override for testing
* @since 0.9.10
*/
protected void handleHostLookup(HostLookupMessage message) {
_context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getReqID(),
message.getTimeout(), message.getHash(), message.getHostname()));
}
/**
* Message's Session ID ignored. This doesn't support removing previously set options.
* Nor do we bother with message.getSessionConfig().verifySignature() ... should we?

View File

@ -4,32 +4,83 @@
*/
package net.i2p.router.client;
import java.util.Locale;
import net.i2p.data.Base32;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.i2cp.DestReplyMessage;
import net.i2p.data.i2cp.HostReplyMessage;
import net.i2p.data.i2cp.I2CPMessage;
import net.i2p.data.i2cp.I2CPMessageException;
import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
/**
* Look up the lease of a hash, to convert it to a Destination for the client
* Look up the lease of a hash, to convert it to a Destination for the client.
* Or, since 0.9.10, lookup a host name in the naming service.
*/
class LookupDestJob extends JobImpl {
private final ClientConnectionRunner _runner;
private final long _reqID;
private final long _timeout;
private final Hash _hash;
private final String _name;
private static final long DEFAULT_TIMEOUT = 15*1000;
public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h) {
this(context, runner, -1, DEFAULT_TIMEOUT, h, null);
}
/**
* One of h or name non-null
* @param reqID must be >= 0 if name != null
* @since 0.9.10
*/
public LookupDestJob(RouterContext context, ClientConnectionRunner runner,
long reqID, long timeout, Hash h, String name) {
super(context);
if ((h == null && name == null) ||
(h != null && name != null) ||
(reqID < 0 && name != null))
throw new IllegalArgumentException();
_runner = runner;
_reqID = reqID;
_timeout = timeout;
if (name != null && name.length() == 60) {
// convert a b32 lookup to a hash lookup
String nlc = name.toLowerCase(Locale.US);
if (nlc.endsWith(".b32.i2p")) {
byte[] b = Base32.decode(nlc.substring(0, 52));
if (b != null && b.length == Hash.HASH_LENGTH) {
h = Hash.create(b);
name = null;
}
}
}
_hash = h;
_name = name;
}
public String getName() { return "LeaseSet Lookup for Client"; }
public String getName() { return _name != null ?
"HostName Lookup for Client" :
"LeaseSet Lookup for Client";
}
public void runJob() {
DoneJob done = new DoneJob(getContext());
// TODO add support for specifying the timeout in the lookup message
getContext().netDb().lookupLeaseSet(_hash, done, done, 15*1000);
if (_name != null) {
// inline, ignore timeout
Destination d = getContext().namingService().lookup(_name);
if (d != null)
returnDest(d);
else
returnFail();
} else {
DoneJob done = new DoneJob(getContext());
getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout);
}
}
private class DoneJob extends JobImpl {
@ -42,12 +93,16 @@ class LookupDestJob extends JobImpl {
if (ls != null)
returnDest(ls.getDestination());
else
returnHash(_hash);
returnFail();
}
}
private void returnDest(Destination d) {
DestReplyMessage msg = new DestReplyMessage(d);
I2CPMessage msg;
if (_reqID >= 0)
msg = new HostReplyMessage(d, _reqID);
else
msg = new DestReplyMessage(d);
try {
_runner.doSend(msg);
} catch (I2CPMessageException ime) {}
@ -57,8 +112,12 @@ class LookupDestJob extends JobImpl {
* Return the failed hash so the client can correlate replies with requests
* @since 0.8.3
*/
private void returnHash(Hash h) {
DestReplyMessage msg = new DestReplyMessage(h);
private void returnFail() {
I2CPMessage msg;
if (_reqID >= 0)
msg = new HostReplyMessage(HostReplyMessage.RESULT_FAILURE, _reqID);
else
msg = new DestReplyMessage(_hash);
try {
_runner.doSend(msg);
} catch (I2CPMessageException ime) {}