package net.i2p.client.impl; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import net.i2p.I2PAppContext; import net.i2p.client.I2PSessionException; import net.i2p.client.SendMessageOptions; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.data.LeaseSet; import net.i2p.data.Payload; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.SigningPrivateKey; import net.i2p.data.i2cp.AbuseReason; import net.i2p.data.i2cp.AbuseSeverity; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; import net.i2p.data.i2cp.DestroySessionMessage; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.ReconfigureSessionMessage; import net.i2p.data.i2cp.ReportAbuseMessage; import net.i2p.data.i2cp.SendMessageMessage; import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionConfig; import net.i2p.data.i2cp.SessionId; import net.i2p.util.Log; /** * Produce the various messages the session needs to send to the router. * * @author jrandom */ class I2CPMessageProducer { private final Log _log; private final I2PAppContext _context; private int _maxBytesPerSecond; private volatile int _sendPeriodBytes; private volatile long _sendPeriodBeginTime; private final ReentrantLock _lock; private static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond"; /** see ConnectionOptions in streaming - MTU + streaming overhead + gzip overhead */ private static final int TYP_SIZE = 1730 + 28 + 23; private static final int MIN_RATE = 2 * TYP_SIZE; public I2CPMessageProducer(I2PAppContext context) { _context = context; _log = context.logManager().getLog(I2CPMessageProducer.class); _lock = new ReentrantLock(true); context.statManager().createRateStat("client.sendThrottled", "Times waited for bandwidth", "ClientMessages", new long[] { 60*1000 }); context.statManager().createRateStat("client.sendDropped", "Length of msg dropped waiting for bandwidth", "ClientMessages", new long[] { 60*1000 }); } /** * Update the bandwidth setting * @since 0.8.4 */ public void updateBandwidth(I2PSessionImpl session) { String max = session.getOptions().getProperty(PROP_MAX_BW); if (max != null) { try { int iMax = Integer.parseInt(max); if (iMax > 0) // round up to next higher TYP_SIZE for efficiency, then add some fudge for small messages _maxBytesPerSecond = 256 + Math.max(MIN_RATE, TYP_SIZE * ((iMax + TYP_SIZE - 1) / TYP_SIZE)); else _maxBytesPerSecond = 0; } catch (NumberFormatException nfe) {} } if (_log.shouldLog(Log.DEBUG)) _log.debug("Setting " + _maxBytesPerSecond + " BPS max"); } /** * Send all the messages that a client needs to send to a router to establish * a new session. */ public void connect(I2PSessionImpl session) throws I2PSessionException { updateBandwidth(session); CreateSessionMessage msg = new CreateSessionMessage(); SessionConfig cfg = new SessionConfig(session.getMyDestination()); cfg.setOptions(session.getOptions()); if (_log.shouldLog(Log.DEBUG)) _log.debug("config created"); try { cfg.signSessionConfig(session.getPrivateKey()); } catch (DataFormatException dfe) { throw new I2PSessionException("Unable to sign the session config", dfe); } if (_log.shouldLog(Log.DEBUG)) _log.debug("config signed"); msg.setSessionConfig(cfg); if (_log.shouldLog(Log.DEBUG)) _log.debug("config loaded into message"); session.sendMessage_unchecked(msg); if (_log.shouldLog(Log.DEBUG)) _log.debug("config message sent"); } /** * Send messages to the router destroying the session and disconnecting * */ public void disconnect(I2PSessionImpl session) throws I2PSessionException { if (session.isClosed()) return; DestroySessionMessage dmsg = new DestroySessionMessage(); dmsg.setSessionId(session.getSessionId()); session.sendMessage_unchecked(dmsg); // use DisconnectMessage only if we fail and drop connection... // todo: update the code to fire off DisconnectMessage on socket error //DisconnectMessage msg = new DisconnectMessage(); //msg.setReason("Destroy called"); //session.sendMessage(msg); } /** * Package up and send the payload to the router for delivery * * @param nonce 0 to 0xffffffff; if 0, the router will not reply with a MessageStatusMessage * @param tag unused - no end-to-end crypto * @param tags unused - no end-to-end crypto * @param key unused - no end-to-end crypto * @param newKey unused - no end-to-end crypto */ public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag, SessionKey key, Set<SessionTag> tags, SessionKey newKey, long expires) throws I2PSessionException { sendMessage(session, dest, nonce, payload, expires, 0); } /** * Package up and send the payload to the router for delivery * * @param nonce 0 to 0xffffffff; if 0, the router will not reply with a MessageStatusMessage * @since 0.8.4 */ public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, long expires, int flags) throws I2PSessionException { if (!updateBps(payload.length, expires)) // drop the message... send fail notification? return; SendMessageMessage msg; if (expires > 0 || flags > 0) { SendMessageExpiresMessage smsg = new SendMessageExpiresMessage(); smsg.setExpiration(expires); smsg.setFlags(flags); msg = smsg; } else msg = new SendMessageMessage(); msg.setDestination(dest); SessionId sid = session.getSessionId(); if (sid == null) { _log.error(session.toString() + " send message w/o session", new Exception()); return; } msg.setSessionId(sid); msg.setNonce(nonce); Payload data = createPayload(dest, payload, null, null, null, null); msg.setPayload(data); session.sendMessage(msg); } /** * Package up and send the payload to the router for delivery * * @param nonce 0 to 0xffffffff; if 0, the router will not reply with a MessageStatusMessage * @since 0.9.2 */ public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SendMessageOptions options) throws I2PSessionException { long expires = options.getTime(); if (!updateBps(payload.length, expires)) // drop the message... send fail notification? return; SendMessageMessage msg = new SendMessageExpiresMessage(options); msg.setDestination(dest); SessionId sid = session.getSessionId(); if (sid == null) { _log.error(session.toString() + " send message w/o session", new Exception()); return; } msg.setSessionId(sid); msg.setNonce(nonce); Payload data = createPayload(dest, payload, null, null, null, null); msg.setPayload(data); session.sendMessage(msg); } /** * Super-simple bandwidth throttler. * We only calculate on a one-second basis, so large messages * (compared to the one-second limit) may exceed the limits. * Tuned for streaming, may not work well for large datagrams. * * This does poorly with low rate limits since it doesn't credit * bandwidth across two periods. So the limit is rounded up, * and the min limit is set to 2x the typ size, above. * * Blocking so this could be very bad for retransmissions, * as it could clog StreamingTimer. * Waits are somewhat "fair" using ReentrantLock. * While out-of-order transmission is acceptable, fairness * reduces the chance of starvation. ReentrantLock does not * guarantee in-order execution due to thread priority issues, * so out-of-order may still occur. But shouldn't happen within * the same thread anyway... Also note that small messages may * go ahead of large ones that are waiting for the next window. * Also, threads waiting a second time go to the back of the line. * * Since this is at the I2CP layer, it includes streaming overhead, * streaming acks and retransmissions, * gzip overhead (or "underhead" for compression), * repliable datagram overhead, etc. * However, it does not, of course, include the substantial overhead * imposed by the router for the leaseset, tags, encryption, * and fixed-size tunnel messages. * * @param expires if > 0, an expiration date * @return true if we should send the message, false to drop it */ private boolean updateBps(int len, long expires) { if (_maxBytesPerSecond <= 0) return true; //synchronized(this) { _lock.lock(); try { int waitCount = 0; while (true) { long now = _context.clock().now(); if (waitCount > 0 && expires > 0 && expires < now) { // just say no to bufferbloat... drop the message right here _context.statManager().addRateData("client.sendDropped", len, 0); if (_log.shouldLog(Log.WARN)) _log.warn("Dropping " + len + " byte msg expired in queue"); return false; } long period = now - _sendPeriodBeginTime; if (period >= 2000) { // start new period, always let it through no matter how big _sendPeriodBytes = len; _sendPeriodBeginTime = now; if (_log.shouldLog(Log.DEBUG)) _log.debug("New period after idle, " + len + " bytes"); return true; } if (period >= 1000) { // start new period // Allow burst within 2 sec, only advance window by 1 sec, and // every other second give credit for unused bytes in previous period if (_sendPeriodBytes > 0 && ((_sendPeriodBeginTime / 1000) & 0x01) == 0) _sendPeriodBytes += len - _maxBytesPerSecond; else _sendPeriodBytes = len; _sendPeriodBeginTime += 1000; if (_log.shouldLog(Log.DEBUG)) _log.debug("New period, " + len + " bytes"); return true; } if (_sendPeriodBytes + len <= _maxBytesPerSecond) { // still bytes available in this period _sendPeriodBytes += len; if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending " + len + ", Elapsed " + period + "ms, total " + _sendPeriodBytes + " bytes"); return true; } if (waitCount >= 2) { // just say no to bufferbloat... drop the message right here _context.statManager().addRateData("client.sendDropped", len, 0); if (_log.shouldLog(Log.WARN)) _log.warn("Dropping " + len + " byte msg after waiting " + waitCount + " times"); return false; } // wait until next period _context.statManager().addRateData("client.sendThrottled", ++waitCount, 0); if (_log.shouldLog(Log.DEBUG)) _log.debug("Throttled " + len + " bytes, wait #" + waitCount + ' ' + (1000 - period) + "ms" /*, new Exception()*/); try { //this.wait(1000 - period); _lock.newCondition().await(1000 - period, TimeUnit.MILLISECONDS); } catch (InterruptedException ie) {} } } finally { _lock.unlock(); } } /** * Should we include the I2CP end to end crypto (which is in addition to any * garlic crypto added by the router) * */ static final boolean END_TO_END_CRYPTO = false; /** * Create a new signed payload and send it off to the destination * * @param tag unused - no end-to-end crypto * @param tags unused - no end-to-end crypto * @param key unused - no end-to-end crypto * @param newKey unused - no end-to-end crypto */ private Payload createPayload(Destination dest, byte[] payload, SessionTag tag, SessionKey key, Set<SessionTag> tags, SessionKey newKey) throws I2PSessionException { if (dest == null) throw new I2PSessionException("No destination specified"); if (payload == null) throw new I2PSessionException("No payload specified"); Payload data = new Payload(); if (!END_TO_END_CRYPTO) { data.setEncryptedData(payload); return data; } // no padding at this level // the garlic may pad, and the tunnels may pad, and the transports may pad int size = payload.length; byte encr[] = _context.elGamalAESEngine().encrypt(payload, dest.getPublicKey(), key, tags, tag, newKey, size); // yes, in an intelligent component, newTags would be queued for confirmation along with key, and // generateNewTags would only generate tags if necessary data.setEncryptedData(encr); //_log.debug("Encrypting the payload to public key " + dest.getPublicKey().toBase64() + "\nPayload: " // + data.calculateHash()); return data; } /** * Send an abuse message to the router */ public void reportAbuse(I2PSessionImpl session, int msgId, int severity) throws I2PSessionException { ReportAbuseMessage msg = new ReportAbuseMessage(); MessageId id = new MessageId(); id.setMessageId(msgId); msg.setMessageId(id); AbuseReason reason = new AbuseReason(); reason.setReason("Not specified"); msg.setReason(reason); AbuseSeverity sv = new AbuseSeverity(); sv.setSeverity(severity); msg.setSeverity(sv); session.sendMessage(msg); } /** * Create a new signed leaseSet in response to a request to do so and send it * to the router * */ public void createLeaseSet(I2PSessionImpl session, LeaseSet leaseSet, SigningPrivateKey signingPriv, PrivateKey priv) throws I2PSessionException { CreateLeaseSetMessage msg = new CreateLeaseSetMessage(); msg.setLeaseSet(leaseSet); msg.setPrivateKey(priv); msg.setSigningPrivateKey(signingPriv); SessionId sid = session.getSessionId(); if (sid == null) { _log.error(session.toString() + " create LS w/o session", new Exception()); return; } msg.setSessionId(sid); session.sendMessage_unchecked(msg); } /** * Update number of tunnels * * @param tunnels 0 for original configured number */ public void updateTunnels(I2PSessionImpl session, int tunnels) throws I2PSessionException { ReconfigureSessionMessage msg = new ReconfigureSessionMessage(); SessionConfig cfg = new SessionConfig(session.getMyDestination()); Properties props = session.getOptions(); if (tunnels > 0) { Properties newprops = new Properties(); newprops.putAll(props); props = newprops; props.setProperty("inbound.quantity", "" + tunnels); props.setProperty("outbound.quantity", "" + tunnels); props.setProperty("inbound.backupQuantity", "0"); props.setProperty("outbound.backupQuantity", "0"); } cfg.setOptions(props); try { cfg.signSessionConfig(session.getPrivateKey()); } catch (DataFormatException dfe) { throw new I2PSessionException("Unable to sign the session config", dfe); } msg.setSessionConfig(cfg); SessionId sid = session.getSessionId(); if (sid == null) { _log.error(session.toString() + " update config w/o session", new Exception()); return; } msg.setSessionId(sid); session.sendMessage(msg); } }