package net.i2p.router.transport.udp;
import java.io.ByteArrayInputStream;
import java.io.IOException;
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.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.router.RouterIdentity;
import net.i2p.data.SessionKey;
import net.i2p.data.Signature;
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 the remote peer has
* initiated the connection with us. In other words, they are Alice and
* we are Bob.
*
* TODO do all these methods need to be synchronized?
*/
class InboundEstablishState {
private final RouterContext _context;
private final Log _log;
// SessionRequest message
private byte _receivedX[];
private byte _bobIP[];
private final int _bobPort;
private final DHSessionKeyBuilder _keyBuilder;
// SessionCreated message
private byte _sentY[];
private final byte _aliceIP[];
private final int _alicePort;
private long _sentRelayTag;
private long _sentSignedOnTime;
private SessionKey _sessionKey;
private SessionKey _macKey;
private Signature _sentSignature;
// SessionConfirmed messages - fragmented in theory but not in practice - see below
private byte _receivedIdentity[][];
private long _receivedSignedOnTime;
private byte _receivedSignature[];
private boolean _verificationAttempted;
// sig not verified
private RouterIdentity _receivedUnconfirmedIdentity;
// identical to uncomfirmed, but sig now verified
private RouterIdentity _receivedConfirmedIdentity;
// general status
private final long _establishBegin;
//private long _lastReceive;
private long _lastSend;
private long _nextSend;
private final RemoteHostId _remoteHostId;
private InboundState _currentState;
private final Queue<OutNetMessage> _queuedMessages;
// count for backoff
private int _createdSentCount;
// default true
private boolean _introductionRequested = true;
public enum InboundState {
/** nothin known yet */
IB_STATE_UNKNOWN,
/** we have received an initial request */
IB_STATE_REQUEST_RECEIVED,
/** we have sent a signed creation packet */
IB_STATE_CREATED_SENT,
/** we have received one but not all the confirmation packets
* This never happens in practice - see below. */
IB_STATE_CONFIRMED_PARTIALLY,
/** we have all the confirmation packets */
IB_STATE_CONFIRMED_COMPLETELY,
/** we are explicitly failing it */
IB_STATE_FAILED,
/** Successful completion, PeerState created and added to transport */
IB_STATE_COMPLETE
}
/** 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;
/**
* @param localPort Must be our external port, otherwise the signature of the
* SessionCreated message will be bad if the external port != the internal port.
*/
public InboundEstablishState(RouterContext ctx, byte remoteIP[], int remotePort, int localPort,
DHSessionKeyBuilder dh) {
_context = ctx;
_log = ctx.logManager().getLog(InboundEstablishState.class);
_aliceIP = remoteIP;
_alicePort = remotePort;
_remoteHostId = new RemoteHostId(_aliceIP, _alicePort);
_bobPort = localPort;
_currentState = InboundState.IB_STATE_UNKNOWN;
_establishBegin = ctx.clock().now();
_keyBuilder = dh;
_queuedMessages = new LinkedBlockingQueue<OutNetMessage>();
}
public synchronized InboundState getState() { return _currentState; }
/** @return if previously complete */
public synchronized boolean isComplete() {
return _currentState == InboundState.IB_STATE_COMPLETE ||
_currentState == InboundState.IB_STATE_FAILED;
}
/** Notify successful completion */
public synchronized void complete() {
_currentState = InboundState.IB_STATE_COMPLETE;
}
/**
* Queue a message to be sent after the session is established.
* This will only happen if we decide to send something during establishment
* @since 0.9.2
*/
public void addMessage(OutNetMessage msg) {
// 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);
}
/**
* Pull from the message queue
* @return null if none
* @since 0.9.2
*/
public OutNetMessage getNextQueuedMessage() {
return _queuedMessages.poll();
}
public synchronized void receiveSessionRequest(UDPPacketReader.SessionRequestReader req) {
if (_receivedX == null)
_receivedX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
req.readX(_receivedX, 0);
if (_bobIP == null)
_bobIP = new byte[req.readIPSize()];
req.readIP(_bobIP, 0);
byte[] ext = req.readExtendedOptions();
if (ext != null && ext.length >= UDPPacket.SESS_REQ_MIN_EXT_OPTIONS_LENGTH) {
_introductionRequested = (ext[1] & (byte) UDPPacket.SESS_REQ_EXT_FLAG_REQUEST_RELAY_TAG) != 0;
if (_log.shouldInfo())
_log.info("got sess req. w/ ext. options, need intro? " + _introductionRequested + ' ' + this);
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive sessionRequest, BobIP = " + Addresses.toString(_bobIP));
if (_currentState == InboundState.IB_STATE_UNKNOWN)
_currentState = InboundState.IB_STATE_REQUEST_RECEIVED;
packetReceived();
}
public synchronized boolean sessionRequestReceived() { return _receivedX != null; }
public synchronized byte[] getReceivedX() { return _receivedX; }
public synchronized byte[] getReceivedOurIP() { return _bobIP; }
/**
* True (default) if no extended options in session request,
* or value of flag bit in the extended options.
* @since 0.9.24
*/
public synchronized boolean isIntroductionRequested() { return _introductionRequested; }
/**
* Generates session key and mac key.
*/
public synchronized void generateSessionKey() throws DHSessionKeyBuilder.InvalidPublicParameterException {
if (_sessionKey != null) return;
_keyBuilder.setPeerPublicValue(_receivedX);
_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 inbound keys. cipher: " + Base64.encode(_sessionKey.getData())
+ " mac: " + Base64.encode(_macKey.getData()));
}
public synchronized SessionKey getCipherKey() { return _sessionKey; }
public synchronized SessionKey getMACKey() { return _macKey; }
/** what IP do they appear to be on? */
public byte[] getSentIP() { return _aliceIP; }
/** what port number do they appear to be coming from? */
public int getSentPort() { return _alicePort; }
public synchronized byte[] getSentY() {
if (_sentY == null)
_sentY = _keyBuilder.getMyPublicValueBytes();
return _sentY;
}
public synchronized void fail() {
_currentState = InboundState.IB_STATE_FAILED;
}
public synchronized long getSentRelayTag() { return _sentRelayTag; }
public synchronized void setSentRelayTag(long tag) { _sentRelayTag = tag; }
public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; }
public synchronized void prepareSessionCreated() {
if (_sentSignature == null) signSessionCreated();
}
public synchronized Signature getSentSignature() { return _sentSignature; }
/**
* Sign: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's
* new relay tag + Bob's signed on time
*/
private void signSessionCreated() {
byte signed[] = new byte[256 + 256 // X + Y
+ _aliceIP.length + 2
+ _bobIP.length + 2
+ 4 // sent relay tag
+ 4 // signed on time
];
_sentSignedOnTime = _context.clock().now() / 1000;
int off = 0;
System.arraycopy(_receivedX, 0, signed, off, _receivedX.length);
off += _receivedX.length;
getSentY();
System.arraycopy(_sentY, 0, signed, off, _sentY.length);
off += _sentY.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, _sentRelayTag);
off += 4;
DataHelper.toLong(signed, off, 4, _sentSignedOnTime);
_sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey());
if (_log.shouldLog(Log.DEBUG)) {
StringBuilder buf = new StringBuilder(128);
buf.append("Signing sessionCreated:");
//buf.append(" ReceivedX: ").append(Base64.encode(_receivedX));
//buf.append(" SentY: ").append(Base64.encode(_sentY));
buf.append(" Alice: ").append(Addresses.toString(_aliceIP, _alicePort));
buf.append(" Bob: ").append(Addresses.toString(_bobIP, _bobPort));
buf.append(" RelayTag: ").append(_sentRelayTag);
buf.append(" SignedOn: ").append(_sentSignedOnTime);
buf.append(" signature: ").append(Base64.encode(_sentSignature.getData()));
_log.debug(buf.toString());
}
}
/** note that we just sent a SessionCreated packet */
public synchronized void createdPacketSent() {
_lastSend = _context.clock().now();
long delay;
if (_createdSentCount == 0) {
delay = RETRANSMIT_DELAY;
} else {
delay = Math.min(RETRANSMIT_DELAY << _createdSentCount, MAX_DELAY);
}
_createdSentCount++;
_nextSend = _lastSend + delay;
if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) )
_currentState = InboundState.IB_STATE_CREATED_SENT;
}
/** 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; }
/** RemoteHostId, uniquely identifies an attempt */
RemoteHostId getRemoteHostId() { return _remoteHostId; }
/**
* 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 receiveSessionConfirmed(UDPPacketReader.SessionConfirmedReader conf) {
if (_receivedIdentity == null)
_receivedIdentity = new byte[conf.readTotalFragmentNum()][];
int cur = conf.readCurrentFragmentNum();
if (cur >= _receivedIdentity.length) {
// avoid AIOOBE
// should do more than this, but what? disconnect?
fail();
packetReceived();
return;
}
if (_receivedIdentity[cur] == null) {
byte fragment[] = new byte[conf.readCurrentFragmentSize()];
conf.readFragmentData(fragment, 0);
_receivedIdentity[cur] = fragment;
}
if (cur == _receivedIdentity.length-1) {
_receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
// TODO verify time to prevent replay attacks
buildIdentity();
if (_receivedUnconfirmedIdentity != null) {
SigType type = _receivedUnconfirmedIdentity.getSigningPublicKey().getType();
if (type != null) {
int sigLen = type.getSigLen();
if (_receivedSignature == null)
_receivedSignature = new byte[sigLen];
conf.readFinalSignature(_receivedSignature, 0, sigLen);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Unsupported sig type from: " + toString());
// _x() in UDPTransport
_context.banlist().banlistRouterForever(_receivedUnconfirmedIdentity.calculateHash(),
"Unsupported signature type");
fail();
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Bad ident from: " + toString());
fail();
}
}
if ( (_currentState == InboundState.IB_STATE_UNKNOWN) ||
(_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) ||
(_currentState == InboundState.IB_STATE_CREATED_SENT) ) {
if (confirmedFullyReceived())
_currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
else
_currentState = InboundState.IB_STATE_CONFIRMED_PARTIALLY;
}
packetReceived();
}
/**
* Have we fully received the SessionConfirmed messages from Alice?
* Caller must synch on this.
*/
private boolean confirmedFullyReceived() {
if (_receivedIdentity != null) {
for (int i = 0; i < _receivedIdentity.length; i++) {
if (_receivedIdentity[i] == null)
return false;
}
return true;
} else {
return false;
}
}
/**
* Who is Alice (null if forged/unknown)
*
* Note that this isn't really confirmed - see below.
*/
public synchronized RouterIdentity getConfirmedIdentity() {
if (!_verificationAttempted) {
verifyIdentity();
_verificationAttempted = true;
}
return _receivedConfirmedIdentity;
}
/**
* Construct Alice's RouterIdentity.
* Must have received all fragments.
* Sets _receivedUnconfirmedIdentity, unless invalid.
*
* Caller must synch on this.
*
* @since 0.9.16 was in verifyIdentity()
*/
private void buildIdentity() {
if (_receivedUnconfirmedIdentity != null)
return; // dup pkt?
int frags = _receivedIdentity.length;
byte[] ident;
if (frags > 1) {
int identSize = 0;
for (int i = 0; i < _receivedIdentity.length; i++)
identSize += _receivedIdentity[i].length;
ident = new byte[identSize];
int off = 0;
for (int i = 0; i < _receivedIdentity.length; i++) {
int len = _receivedIdentity[i].length;
System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
off += len;
}
} else {
// no need to copy
ident = _receivedIdentity[0];
}
ByteArrayInputStream in = new ByteArrayInputStream(ident);
RouterIdentity peer = new RouterIdentity();
try {
peer.readBytes(in);
_receivedUnconfirmedIdentity = peer;
} catch (DataFormatException dfe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Improperly formatted yet fully received ident", dfe);
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Improperly formatted yet fully received ident", ioe);
}
}
/**
* Determine if Alice sent us a valid confirmation packet. The
* identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
* + Alice's new relay key + Alice's signed on time
*
* Note that the protocol does not include a signature of the RouterIdentity,
* which could be a problem?
*
* Caller must synch on this.
*/
private void verifyIdentity() {
if (_receivedUnconfirmedIdentity == null)
return; // either not yet recvd or bad ident
if (_receivedSignature == null)
return; // either not yet recvd or bad sig
byte signed[] = new byte[256+256 // X + Y
+ _aliceIP.length + 2
+ _bobIP.length + 2
+ 4 // Alice's relay key
+ 4 // signed on time
];
int off = 0;
System.arraycopy(_receivedX, 0, signed, off, _receivedX.length);
off += _receivedX.length;
getSentY();
System.arraycopy(_sentY, 0, signed, off, _sentY.length);
off += _sentY.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, _sentRelayTag);
off += 4;
DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
Signature sig = new Signature(_receivedUnconfirmedIdentity.getSigType(), _receivedSignature);
boolean ok = _context.dsa().verifySignature(sig, signed, _receivedUnconfirmedIdentity.getSigningPublicKey());
if (ok) {
// todo partial spoof detection - get peer.calculateHash(),
// lookup in netdb locally, if not equal, fail?
_receivedConfirmedIdentity = _receivedUnconfirmedIdentity;
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Signature failed from " + _receivedUnconfirmedIdentity);
}
}
private void packetReceived() {
_nextSend = _context.clock().now();
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(128);
buf.append("IES ");
buf.append(Addresses.toString(_aliceIP, _alicePort));
//if (_receivedX != null)
// buf.append(" ReceivedX: ").append(Base64.encode(_receivedX, 0, 4));
//if (_sentY != null)
// buf.append(" SentY: ").append(Base64.encode(_sentY, 0, 4));
//buf.append(" Bob: ").append(Addresses.toString(_bobIP, _bobPort));
buf.append(" RelayTag: ").append(_sentRelayTag);
//buf.append(" SignedOn: ").append(_sentSignedOnTime);
buf.append(' ').append(_currentState);
return buf.toString();
}
}