package net.i2p.router.transport.udp; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import net.i2p.crypto.SigType; import net.i2p.data.Base64; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; import net.i2p.data.router.RouterIdentity; import net.i2p.data.SessionKey; import net.i2p.data.Signature; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; import net.i2p.util.Addresses; import net.i2p.util.Log; /** * Data for a new connection being established, where we initiated the * connection with a remote peer. In other words, we are Alice and * they are Bob. * */ class OutboundEstablishState { private final RouterContext _context; private final Log _log; // SessionRequest message private byte _sentX[]; private byte _bobIP[]; private int _bobPort; private final DHSessionKeyBuilder.Factory _keyFactory; private DHSessionKeyBuilder _keyBuilder; // SessionCreated message private byte _receivedY[]; private byte _aliceIP[]; private int _alicePort; private long _receivedRelayTag; private long _receivedSignedOnTime; private SessionKey _sessionKey; private SessionKey _macKey; private Signature _receivedSignature; // includes trailing padding to mod 16 private byte[] _receivedEncryptedSignature; private byte[] _receivedIV; // SessionConfirmed messages private long _sentSignedOnTime; private Signature _sentSignature; // general status private final long _establishBegin; //private long _lastReceive; private long _lastSend; private long _nextSend; private RemoteHostId _remoteHostId; private final RemoteHostId _claimedAddress; private final RouterIdentity _remotePeer; private final boolean _allowExtendedOptions; private final boolean _needIntroduction; private final SessionKey _introKey; private final Queue<OutNetMessage> _queuedMessages; private OutboundState _currentState; private long _introductionNonce; private boolean _isFirstMessageOurDSM; // intro private final UDPAddress _remoteAddress; private boolean _complete; // counts for backoff private int _confirmedSentCount; private int _requestSentCount; private int _introSentCount; // Times for timeout private long _confirmedSentTime; private long _requestSentTime; private long _introSentTime; public enum OutboundState { /** nothin sent yet */ OB_STATE_UNKNOWN, /** we have sent an initial request */ OB_STATE_REQUEST_SENT, /** we have received a signed creation packet */ OB_STATE_CREATED_RECEIVED, /** we have sent one or more confirmation packets */ OB_STATE_CONFIRMED_PARTIALLY, /** we have received a data packet */ OB_STATE_CONFIRMED_COMPLETELY, /** we need to have someone introduce us to the peer, but haven't received a RelayResponse yet */ OB_STATE_PENDING_INTRO, /** RelayResponse received */ OB_STATE_INTRODUCED, /** SessionConfirmed failed validation */ OB_STATE_VALIDATION_FAILED } /** basic delay before backoff * Transmissions at 0, 3, 9 sec * Previously: 1500 (0, 1.5, 4.5, 10.5) */ private static final long RETRANSMIT_DELAY = 3000; /** max delay including backoff */ private static final long MAX_DELAY = 15*1000; private static final long WAIT_FOR_HOLE_PUNCH_DELAY = 500; /** * @param claimedAddress an IP/port based RemoteHostId, or null if unknown * @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect * @param remotePeer must have supported sig type * @param allowExtendedOptions are we allowed to send extended options to Bob? * @param needIntroduction should we ask Bob to be an introducer for us? ignored unless allowExtendedOptions is true * @param introKey Bob's introduction key, as published in the netdb * @param addr non-null */ public OutboundEstablishState(RouterContext ctx, RemoteHostId claimedAddress, RemoteHostId remoteHostId, RouterIdentity remotePeer, boolean allowExtendedOptions, boolean needIntroduction, SessionKey introKey, UDPAddress addr, DHSessionKeyBuilder.Factory dh) { _context = ctx; _log = ctx.logManager().getLog(OutboundEstablishState.class); if (claimedAddress != null) { _bobIP = claimedAddress.getIP(); _bobPort = claimedAddress.getPort(); } else { //_bobIP = null; _bobPort = -1; } _claimedAddress = claimedAddress; _remoteHostId = remoteHostId; _allowExtendedOptions = allowExtendedOptions; _needIntroduction = needIntroduction; _remotePeer = remotePeer; _introKey = introKey; _queuedMessages = new LinkedBlockingQueue<OutNetMessage>(); _establishBegin = ctx.clock().now(); _remoteAddress = addr; _introductionNonce = -1; _keyFactory = dh; if (addr.getIntroducerCount() > 0) { if (_log.shouldLog(Log.DEBUG)) _log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr); _currentState = OutboundState.OB_STATE_PENDING_INTRO; } else { _currentState = OutboundState.OB_STATE_UNKNOWN; } } public synchronized OutboundState getState() { return _currentState; } /** @return if previously complete */ public synchronized boolean complete() { boolean already = _complete; _complete = true; return already; } /** @return non-null */ public UDPAddress getRemoteAddress() { return _remoteAddress; } public void setIntroNonce(long nonce) { _introductionNonce = nonce; } /** @return -1 if unset */ public long getIntroNonce() { return _introductionNonce; } /** * Are we allowed to send extended options to this peer? * @since 0.9.24 */ public boolean isExtendedOptionsAllowed() { return _allowExtendedOptions; } /** * Should we ask this peer to be an introducer for us? * Ignored unless allowExtendedOptions is true * @since 0.9.24 */ public boolean needIntroduction() { return _needIntroduction; } /** * Queue a message to be sent after the session is established. */ public void addMessage(OutNetMessage msg) { if (_queuedMessages.isEmpty()) { I2NPMessage m = msg.getMessage(); if (m.getType() == DatabaseStoreMessage.MESSAGE_TYPE) { DatabaseStoreMessage dsm = (DatabaseStoreMessage) m; if (dsm.getKey().equals(_context.routerHash())) { _isFirstMessageOurDSM = true; } } } // chance of a duplicate here in a race, that's ok if (!_queuedMessages.contains(msg)) _queuedMessages.offer(msg); else if (_log.shouldLog(Log.WARN)) _log.warn("attempt to add duplicate msg to queue: " + msg); } /** * Is the first message queued our own DatabaseStoreMessage? * @since 0.9.12 */ public boolean isFirstMessageOurDSM() { return _isFirstMessageOurDSM; } /** @return null if none */ public OutNetMessage getNextQueuedMessage() { return _queuedMessages.poll(); } public RouterIdentity getRemoteIdentity() { return _remotePeer; } /** * Bob's introduction key, as published in the netdb */ public SessionKey getIntroKey() { return _introKey; } /** caller must synch - only call once */ private void prepareSessionRequest() { _keyBuilder = _keyFactory.getBuilder(); byte X[] = _keyBuilder.getMyPublicValue().toByteArray(); if (X.length == 257) { _sentX = new byte[256]; System.arraycopy(X, 1, _sentX, 0, _sentX.length); } else if (X.length == 256) { _sentX = X; } else { _sentX = new byte[256]; System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length); } } public synchronized byte[] getSentX() { // We defer keygen until now so that it gets done in the Establisher loop, // and so that we don't waste entropy on failed introductions if (_sentX == null) prepareSessionRequest(); return _sentX; } /** * The remote side (Bob) - note that in some places he's called Charlie. * Warning - may change after introduction. May be null before introduction. */ public synchronized byte[] getSentIP() { return _bobIP; } /** * The remote side (Bob) - note that in some places he's called Charlie. * Warning - may change after introduction. May be -1 before introduction. */ public synchronized int getSentPort() { return _bobPort; } public synchronized void receiveSessionCreated(UDPPacketReader.SessionCreatedReader reader) { if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) { if (_log.shouldLog(Log.WARN)) _log.warn("Session created already failed"); return; } if (_receivedY != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Session created already received, ignoring"); return; // already received } _receivedY = new byte[UDPPacketReader.SessionCreatedReader.Y_LENGTH]; reader.readY(_receivedY, 0); if (_aliceIP == null) _aliceIP = new byte[reader.readIPSize()]; reader.readIP(_aliceIP, 0); _alicePort = reader.readPort(); _receivedRelayTag = reader.readRelayTag(); _receivedSignedOnTime = reader.readSignedOnTime(); // handle variable signature size SigType type = _remotePeer.getSigningPublicKey().getType(); if (type == null) { // shouldn't happen, we only connect to supported peers fail(); packetReceived(); return; } int sigLen = type.getSigLen(); int mod = sigLen % 16; int pad = (mod == 0) ? 0 : (16 - mod); int esigLen = sigLen + pad; _receivedEncryptedSignature = new byte[esigLen]; reader.readEncryptedSignature(_receivedEncryptedSignature, 0, esigLen); _receivedIV = new byte[UDPPacket.IV_SIZE]; reader.readIV(_receivedIV, 0); if (_log.shouldLog(Log.DEBUG)) _log.debug("Receive session created:Sig: " + Base64.encode(_receivedEncryptedSignature) + "receivedIV: " + Base64.encode(_receivedIV) + "AliceIP: " + Addresses.toString(_aliceIP) + " RelayTag: " + _receivedRelayTag + " SignedOn: " + _receivedSignedOnTime + ' ' + this.toString()); if (_currentState == OutboundState.OB_STATE_UNKNOWN || _currentState == OutboundState.OB_STATE_REQUEST_SENT || _currentState == OutboundState.OB_STATE_INTRODUCED || _currentState == OutboundState.OB_STATE_PENDING_INTRO) _currentState = OutboundState.OB_STATE_CREATED_RECEIVED; packetReceived(); } /** * Blocking call (run in the establisher thread) to determine if the * session was created properly. If it wasn't, all the SessionCreated * remnants are dropped (perhaps they were spoofed, etc) so that we can * receive another one * * Generates session key and mac key. * * @return true if valid */ public synchronized boolean validateSessionCreated() { if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) { if (_log.shouldLog(Log.WARN)) _log.warn("Session created already failed"); return false; } if (_receivedSignature != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Session created already validated"); return true; } boolean valid = true; try { generateSessionKey(); } catch (DHSessionKeyBuilder.InvalidPublicParameterException ippe) { if (_log.shouldLog(Log.WARN)) _log.warn("Peer " + getRemoteHostId() + " sent us an invalid DH parameter", ippe); valid = false; } if (valid) decryptSignature(); if (valid && verifySessionCreated()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Session created passed validation"); return true; } else { if (_log.shouldLog(Log.WARN)) _log.warn("Session created failed validation, clearing state for " + _remoteHostId.toString()); fail(); return false; } } /** * The SessionCreated validation failed */ public synchronized void fail() { _receivedY = null; _aliceIP = null; _receivedRelayTag = 0; _receivedSignedOnTime = -1; _receivedEncryptedSignature = null; _receivedIV = null; _receivedSignature = null; if (_keyBuilder != null) { if (_keyBuilder.getPeerPublicValue() == null) _keyFactory.returnUnused(_keyBuilder); _keyBuilder = null; } // sure, there's a chance the packet was corrupted, but in practice // this means that Bob doesn't know his external port, so give up. _currentState = OutboundState.OB_STATE_VALIDATION_FAILED; _nextSend = _context.clock().now(); } /** * Generates session key and mac key. * Caller must synch on this. */ private void generateSessionKey() throws DHSessionKeyBuilder.InvalidPublicParameterException { if (_sessionKey != null) return; if (_keyBuilder == null) throw new DHSessionKeyBuilder.InvalidPublicParameterException("Illegal state - never generated a key builder"); _keyBuilder.setPeerPublicValue(_receivedY); _sessionKey = _keyBuilder.getSessionKey(); ByteArray extra = _keyBuilder.getExtraBytes(); _macKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]); System.arraycopy(extra.getData(), 0, _macKey.getData(), 0, SessionKey.KEYSIZE_BYTES); if (_log.shouldLog(Log.DEBUG)) _log.debug("Established outbound keys. cipher: " + _sessionKey + " mac: " + _macKey); } /** * decrypt the signature (and subsequent pad bytes) with the * additional layer of encryption using the negotiated key along side * the packet's IV * * Caller must synch on this. * Only call this once! Decrypts in-place. */ private void decryptSignature() { if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString()); if (_sessionKey == null) throw new NullPointerException("SessionKey is null!"); if (_receivedIV == null) throw new NullPointerException("IV is null!"); _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0, _sessionKey, _receivedIV, _receivedEncryptedSignature.length); // handle variable signature size SigType type = _remotePeer.getSigningPublicKey().getType(); // if type == null throws NPE int sigLen = type.getSigLen(); int mod = sigLen % 16; if (mod != 0) { byte signatureBytes[] = new byte[sigLen]; System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, sigLen); _receivedSignature = new Signature(type, signatureBytes); } else { _receivedSignature = new Signature(type, _receivedEncryptedSignature); } if (_log.shouldLog(Log.DEBUG)) _log.debug("Decrypted received signature: " + Base64.encode(_receivedSignature.getData())); } /** * Verify: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's * new relay tag + Bob's signed on time * Caller must synch on this. */ private boolean verifySessionCreated() { byte signed[] = new byte[256+256 // X + Y + _aliceIP.length + 2 + _bobIP.length + 2 + 4 // sent relay tag + 4 // signed on time ]; int off = 0; System.arraycopy(_sentX, 0, signed, off, _sentX.length); off += _sentX.length; System.arraycopy(_receivedY, 0, signed, off, _receivedY.length); off += _receivedY.length; System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length); off += _aliceIP.length; DataHelper.toLong(signed, off, 2, _alicePort); off += 2; System.arraycopy(_bobIP, 0, signed, off, _bobIP.length); off += _bobIP.length; DataHelper.toLong(signed, off, 2, _bobPort); off += 2; DataHelper.toLong(signed, off, 4, _receivedRelayTag); off += 4; DataHelper.toLong(signed, off, 4, _receivedSignedOnTime); boolean valid = _context.dsa().verifySignature(_receivedSignature, signed, _remotePeer.getSigningPublicKey()); if (_log.shouldLog(Log.DEBUG) || (_log.shouldLog(Log.WARN) && !valid)) { StringBuilder buf = new StringBuilder(128); buf.append("Signed sessionCreated:"); buf.append(" Alice: ").append(Addresses.toString(_aliceIP, _alicePort)); buf.append(" Bob: ").append(Addresses.toString(_bobIP, _bobPort)); buf.append(" RelayTag: ").append(_receivedRelayTag); buf.append(" SignedOn: ").append(_receivedSignedOnTime); buf.append(" signature: ").append(Base64.encode(_receivedSignature.getData())); if (valid) _log.debug(buf.toString()); else if (_log.shouldLog(Log.WARN)) _log.warn("INVALID: " + buf.toString()); } return valid; } public synchronized SessionKey getCipherKey() { return _sessionKey; } public synchronized SessionKey getMACKey() { return _macKey; } public synchronized long getReceivedRelayTag() { return _receivedRelayTag; } public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; } public synchronized long getReceivedSignedOnTime() { return _receivedSignedOnTime; } public synchronized byte[] getReceivedIP() { return _aliceIP; } public synchronized int getReceivedPort() { return _alicePort; } /** * Let's sign everything so we can fragment properly. * * Note that while a SessionConfirmed could in theory be fragmented, * in practice a RouterIdentity is 387 bytes and a single fragment is 512 bytes max, * so it will never be fragmented. */ public synchronized void prepareSessionConfirmed() { if (_sentSignedOnTime > 0) return; byte signed[] = new byte[256+256 // X + Y + _aliceIP.length + 2 + _bobIP.length + 2 + 4 // Alice's relay key + 4 // signed on time ]; _sentSignedOnTime = _context.clock().now() / 1000; int off = 0; System.arraycopy(_sentX, 0, signed, off, _sentX.length); off += _sentX.length; System.arraycopy(_receivedY, 0, signed, off, _receivedY.length); off += _receivedY.length; System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length); off += _aliceIP.length; DataHelper.toLong(signed, off, 2, _alicePort); off += 2; System.arraycopy(_bobIP, 0, signed, off, _bobIP.length); off += _bobIP.length; DataHelper.toLong(signed, off, 2, _bobPort); off += 2; DataHelper.toLong(signed, off, 4, _receivedRelayTag); off += 4; DataHelper.toLong(signed, off, 4, _sentSignedOnTime); // BUG - if SigningPrivateKey is null, _sentSignature will be null, leading to NPE later // should we throw something from here? _sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey()); } public synchronized Signature getSentSignature() { return _sentSignature; } /** note that we just sent the SessionConfirmed packet */ public synchronized void confirmedPacketsSent() { _lastSend = _context.clock().now(); long delay; if (_confirmedSentCount == 0) { delay = RETRANSMIT_DELAY; _confirmedSentTime = _lastSend; } else { delay = Math.min(RETRANSMIT_DELAY << _confirmedSentCount, _confirmedSentTime + EstablishmentManager.OB_MESSAGE_TIMEOUT - _lastSend); } _confirmedSentCount++; _nextSend = _lastSend + delay; if (_log.shouldLog(Log.DEBUG)) _log.debug("Send confirm packets, nextSend in " + delay); if (_currentState == OutboundState.OB_STATE_UNKNOWN || _currentState == OutboundState.OB_STATE_PENDING_INTRO || _currentState == OutboundState.OB_STATE_INTRODUCED || _currentState == OutboundState.OB_STATE_REQUEST_SENT || _currentState == OutboundState.OB_STATE_CREATED_RECEIVED) _currentState = OutboundState.OB_STATE_CONFIRMED_PARTIALLY; } /** * @return when we sent the first SessionConfirmed packet, or 0 * @since 0.9.2 */ public long getConfirmedSentTime() { return _confirmedSentTime; } /** note that we just sent the SessionRequest packet */ public synchronized void requestSent() { _lastSend = _context.clock().now(); long delay; if (_requestSentCount == 0) { delay = RETRANSMIT_DELAY; _requestSentTime = _lastSend; } else { delay = Math.min(RETRANSMIT_DELAY << _requestSentCount, _requestSentTime + EstablishmentManager.OB_MESSAGE_TIMEOUT - _lastSend); } _requestSentCount++; _nextSend = _lastSend + delay; if (_log.shouldLog(Log.DEBUG)) _log.debug("Send a request packet, nextSend in " + delay); if (_currentState == OutboundState.OB_STATE_UNKNOWN || _currentState == OutboundState.OB_STATE_INTRODUCED) _currentState = OutboundState.OB_STATE_REQUEST_SENT; } /** * @return when we sent the first SessionRequest packet, or 0 * @since 0.9.2 */ public long getRequestSentTime() { return _requestSentTime; } /** note that we just sent the RelayRequest packet */ public synchronized void introSent() { _lastSend = _context.clock().now(); long delay; if (_introSentCount == 0) { delay = RETRANSMIT_DELAY; _introSentTime = _lastSend; } else { delay = Math.min(RETRANSMIT_DELAY << _introSentCount, _introSentTime + EstablishmentManager.OB_MESSAGE_TIMEOUT - _lastSend); } _introSentCount++; _nextSend = _lastSend + delay; if (_currentState == OutboundState.OB_STATE_UNKNOWN) _currentState = OutboundState.OB_STATE_PENDING_INTRO; } /** * @return when we sent the first RelayRequest packet, or 0 * @since 0.9.2 */ public long getIntroSentTime() { return _introSentTime; } public synchronized void introductionFailed() { _nextSend = _context.clock().now(); // keep the state as OB_STATE_PENDING_INTRO, so next time the EstablishmentManager asks us // whats up, it'll try a new random intro peer } /** * This changes the remoteHostId from a hash-based one or possibly * incorrect IP/port to what the introducer told us. * All params are for the remote end (NOT the introducer) and must have been validated already. */ public synchronized void introduced(byte bobIP[], int bobPort) { if (_currentState != OutboundState.OB_STATE_PENDING_INTRO) return; // we've already successfully been introduced, so don't overwrite old settings _nextSend = _context.clock().now() + WAIT_FOR_HOLE_PUNCH_DELAY; // wait briefly for the hole punching _currentState = OutboundState.OB_STATE_INTRODUCED; if (_claimedAddress != null && bobPort == _bobPort && DataHelper.eq(bobIP, _bobIP)) { // he's who he said he was _remoteHostId = _claimedAddress; } else { // no IP/port or wrong IP/port in RI _bobIP = bobIP; _bobPort = bobPort; _remoteHostId = new RemoteHostId(bobIP, bobPort); } if (_log.shouldLog(Log.INFO)) _log.info("Introduced to " + _remoteHostId + ", now lets get on with establishing"); } /** * Accelerate response to RelayResponse if we haven't sent it yet. * * @return true if we should send the SessionRequest now * @since 0.9.15 */ synchronized boolean receiveHolePunch() { if (_currentState != OutboundState.OB_STATE_INTRODUCED) return false; if (_requestSentCount > 0) return false; long now = _context.clock().now(); if (_log.shouldLog(Log.INFO)) _log.info(toString() + " accelerating SessionRequest by " + (_nextSend - now) + " ms"); _nextSend = now; return true; } /** how long have we been trying to establish this session? */ public long getLifetime() { return _context.clock().now() - _establishBegin; } public long getEstablishBeginTime() { return _establishBegin; } public synchronized long getNextSendTime() { return _nextSend; } /** * This should be what the state is currently indexed by in the _outboundStates table. * Beware - * During introduction, this is a router hash. * After introduced() is called, this is set to the IP/port the introducer told us. * @return non-null */ RemoteHostId getRemoteHostId() { return _remoteHostId; } /** * This will never be a hash-based address. * This is the 'claimed' (unverified) address from the netdb, or null. * It is not changed after introduction. Use getRemoteHostId() for the verified address. * @return may be null */ RemoteHostId getClaimedAddress() { return _claimedAddress; } /** we have received a real data packet, so we're done establishing */ public synchronized void dataReceived() { packetReceived(); _currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY; } private void packetReceived() { _nextSend = _context.clock().now(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Got a packet, nextSend == now"); } /** @since 0.8.9 */ @Override public String toString() { return "OES " + _remoteHostId + ' ' + _currentState; } }