package edu.washington.cs.oneswarm.f2f.datagram; import java.net.DatagramPacket; import java.net.InetAddress; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.crypto.NoSuchPaddingException; import org.gudy.azureus2.core3.util.DirectByteBuffer; import org.gudy.azureus2.core3.util.DirectByteBufferPool; import com.aelitis.azureus.core.networkmanager.RawMessage; import com.aelitis.azureus.core.peermanager.messaging.Message; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelDataMsg; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FChannelMsg; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FDatagramInit; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FDatagramOk; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessage; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessageFactory; import edu.washington.cs.oneswarm.f2f.network.FriendConnection; import edu.washington.cs.oneswarm.f2f.servicesharing.OSF2FServiceDataMsg; /** * Connection used in parallel to the standard SSL connection between 2 friends. * * * Wire level packet format: * [Unencrypted] * 8 bytes sequence number. * [Encrypted] * 1 byte message type. * x bytes payload * 20 bytes hmac * * @author isdal * */ public class DatagramConnection extends DatagramRateLimiter { /** * Receive states: * * NEW: no packets sent * * OK_SENT: init received, ok sent back * * ACTIVE: we have successfully decoded one udp packet and are expecting * more. * * CLOSED: the connection is closed. * * * State changes: * * NEW->OK_SENT: got an incoming init packet, send back an ok * * OK_SENT->ACTIVE: got an incoming udp packet, send back second ok. * */ enum ReceiveState { ACTIVE, CLOSED, NEW, OK_SENT; } /** * Send states: * * NEW: no packets sent * * INIT_SENT: remote side supports UDP and an init packet was sent over * * UDP_OK_SENT: the remote side acked our init packet with an ok * packet and we have sent over a udp ok. * * ACTIVE: the remote side acked our udp ok with an tcp ok packet. * * CLOSED: the connection is closed. * * State changes: * * NEW->INIT_SENT: friendconnection handshake completed, send INIT * packet * * INIT_SEND->UDP_OK_SENT: remote side acked our INIT with an tcp ok and we * have sent over a UDP OK packet. * * UDP_OK_SENT->ACTIVE: remote side acked our udp ok with an tcp ok, * udp channel is active and packets can be sent. */ enum SendState { ACTIVE, CLOSED, INIT_SENT, INIT_PENDING, UDP_OK_SENT, NEW } private final static byte AL = DirectByteBuffer.AL_NET_CRYPT; public final static Logger logger = Logger.getLogger(DatagramConnection.class.getName()); // According to netalyzr more than 98% of hosts have a path MTU of 1450 // bytes. Set max size to 1420 to be on the safer side. (room for 8 byte UDP // header and 20 byte ip header). // (MAX_DATAGRAM_SIZE - HMAC_SIZE - SEQUENCE_NUMBER_BYTES % BLOCK_SIZE) == 0 // must be 0; public static final int MAX_DATAGRAM_SIZE = 1420; // The actual payload we can use is a bit less. // -4 for length field // -1 for type field // -8 for sequence number // -1 for minimum padding // -20 for sha1 digest public static final int MAX_DATAGRAM_PAYLOAD_SIZE = MAX_DATAGRAM_SIZE - OSF2FMessage.MESSAGE_HEADER_LEN - DatagramEncrypter.SEQUENCE_NUMBER_BYTES - 1 - DatagramEncrypter.HMAC_SIZE; private final static int INITIAL_QUEUE_CAPACITY = 2; private final static int MAX_UNACKED_UDP_OKs = 10; private final static byte SS = DirectByteBuffer.SS_MSG; private final long createdAt = System.currentTimeMillis(); private DatagramDecrypter decrypter; // Visible for testing. final DatagramEncrypter encrypter; private final DatagramListener friendConnection; private int udpOkCount = 0; private long lastPacketReceived = System.currentTimeMillis(); private final DatagramConnectionManager manager; // Visible for testing DatagramConnection.ReceiveState receiveState = ReceiveState.NEW; private int remotePort; // Visible for testing DatagramConnection.SendState sendState = SendState.NEW; // Visible for testing final DatagramSendThread sendThread; private final InetAddress remoteIp; private final ByteBuffer decryptBuffer; private boolean registered; private final HashSet<String> remoteIpPorts = new HashSet<String>(); // Visible for testing final HashMap<Integer, DatagramRateLimitedChannelQueue> queueMap = new HashMap<Integer, DatagramRateLimitedChannelQueue>(); public DatagramConnection(DatagramConnectionManager manager, DatagramListener friendConnection) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidAlgorithmParameterException { this.friendConnection = friendConnection; this.encrypter = new DatagramEncrypter(); this.manager = manager; this.remoteIp = friendConnection.getRemoteIp(); this.decryptBuffer = ByteBuffer.allocateDirect(MAX_DATAGRAM_SIZE); sendThread = new DatagramSendThread(); sendThread.start(); } public void close() { sendState = SendState.CLOSED; receiveState = ReceiveState.CLOSED; deregister(); sendThread.quit(); } public long getAge() { return System.currentTimeMillis() - createdAt; } public OSF2FDatagramInit createInitMessage() { sendState = SendState.INIT_SENT; OSF2FDatagramInit initMessage = new OSF2FDatagramInit(OSF2FMessage.CURRENT_VERSION, encrypter.getCryptoAlgo(), encrypter.getKey(), encrypter.getIv(), encrypter.getHmac(), manager.getPort()); logger.fine(toString() + "Init message created: " + initMessage.getDescription()); return initMessage; } public int getCapacityForChannel(int channelId) { DatagramRateLimitedChannelQueue queue = this.queueMap.get(new Integer(channelId)); if (queue != null) { int tokens = queue.getAvailableTokens(); if (tokens < MAX_DATAGRAM_PAYLOAD_SIZE) { return 0; } return tokens; } else { return MAX_DATAGRAM_PAYLOAD_SIZE * INITIAL_QUEUE_CAPACITY; } } public int getPotentialCapacityForChannel(int channelId) { DatagramRateLimitedChannelQueue queue = this.queueMap.get(new Integer(channelId)); if (queue != null) { return queue.getTokenBucketSize(); } else { return MAX_DATAGRAM_PAYLOAD_SIZE * INITIAL_QUEUE_CAPACITY; } } Set<String> getKeys() { return remoteIpPorts; } public long getLastMessageSentTime() { return System.currentTimeMillis() - sendThread.lastPacketSent; } public void initMessageReceived(OSF2FDatagramInit message) { logger.fine(toString() + "Got init message: " + message.getDescription()); this.remotePort = message.getLocalPort(); this.remoteIpPorts.add(DatagramConnectionManagerImpl.getKey(remoteIp, remotePort)); try { decrypter = new DatagramDecrypter(message.getEncryptionKey(), message.getIv(), message.getHmacKey()); receiveState = ReceiveState.OK_SENT; register(); friendConnection.sendDatagramOk(new OSF2FDatagramOk(0)); } catch (Exception e) { e.printStackTrace(); sendState = SendState.CLOSED; return; } } private void register() { if (registered) { manager.deregister(this); } manager.register(this); this.registered = true; } private void deregister() { manager.deregister(this); clearExpiredChannels(); this.registered = false; } public boolean isSendingActive() { return sendState == SendState.ACTIVE; } @Override protected synchronized void addQueue(DatagramRateLimiter queue) { super.addQueue(queue); DatagramRateLimitedChannelQueue cQueue = (DatagramRateLimitedChannelQueue) queue; queueMap.put(cQueue.getChannelId(), cQueue); } @Override protected synchronized void removeQueue(DatagramRateLimiter queue) { super.removeQueue(queue); queueMap.remove(((DatagramRateLimitedChannelQueue) queue).getChannelId()); } boolean messageReceived(DatagramPacket packet) { if (decrypter == null) { logger.fine(toString() + "Got unknown packet"); return false; } synchronized (decrypter) { if (receiveState == ReceiveState.CLOSED) { logger.finest(toString() + "Got packet on closed connection"); return false; } Message message = null; try { byte[] data = packet.getData(); decryptBuffer.clear(); if (!decrypter.decrypt(data, packet.getOffset(), packet.getLength(), decryptBuffer)) { logger.finer(toString() + "DatagramDecryption error: " + toString() + " packet=" + packet); return false; } lastPacketReceived = System.currentTimeMillis(); int oldLimit = decryptBuffer.limit(); while (decryptBuffer.hasRemaining()) { // The message length is 1 (for the type field) + the actual // message length. int messageLength = decryptBuffer.getInt(); if (messageLength > MAX_DATAGRAM_SIZE || messageLength < 0) { logger.warning("got oversized length field!"); return false; } DirectByteBuffer messageBuffer = DirectByteBufferPool.getBuffer(AL, messageLength); // Set the limit so that only the current message is read. decryptBuffer.limit(decryptBuffer.position() + messageLength); messageBuffer.put(SS, decryptBuffer); messageBuffer.flip(SS); // Restore the limit to the old limit to prepare to read the // next message. decryptBuffer.limit(oldLimit); message = OSF2FMessageFactory.createOSF2FMessage(messageBuffer); if (message instanceof OSF2FChannelDataMsg) { logger.finest("creating service message from " + ((OSF2FChannelDataMsg) message).getPayload().remaining(SS) + " bytes"); message = OSF2FServiceDataMsg .fromChannelMessage((OSF2FChannelDataMsg) message); ((OSF2FChannelDataMsg) message).setDatagram(true); } if (logger.isLoggable(Level.FINEST)) { logger.finest(toString() + "packet decrypted: " + message.getDescription()); } if (receiveState == ReceiveState.OK_SENT) { // First packet received, set state to active and tell // friend connection to send ok. if (!(message instanceof OSF2FDatagramOk)) { logger.warning(toString() + "first datagram message not an OK message!"); receiveState = ReceiveState.CLOSED; return false; } OSF2FDatagramOk ok = (OSF2FDatagramOk) message; if (ok.getPaddingBytesNum() != MAX_DATAGRAM_PAYLOAD_SIZE) { logger.warning(toString() + "Got OK message, but the payload is cropped (buggy router on path?), len=" + ok.getPaddingBytesNum() + "<" + MAX_DATAGRAM_PAYLOAD_SIZE); return false; } friendConnection.sendDatagramOk(new OSF2FDatagramOk(0)); receiveState = ReceiveState.ACTIVE; } logger.finest("message decoded: " + message.getDescription()); friendConnection.datagramDecoded(message, messageLength); message = null; } return true; } catch (Exception e) { e.printStackTrace(); logger.warning(toString() + "Unable to decode datagram: " + e.getMessage()); return false; } finally { if (message != null) { message.destroy(); } } } } public String addRemoteIpPort(InetAddress ip, int port) { String key = DatagramConnectionManagerImpl.getKey(ip, port); this.remoteIpPorts.add(key); return key; } public void okMessageReceived() { logger.fine(toString() + "OK message received, state=" + sendState); if (sendState == SendState.UDP_OK_SENT) { sendState = SendState.ACTIVE; logger.fine(toString() + "State set to " + sendState); } else if (sendState == SendState.INIT_SENT) { sendUdpOK(); sendState = SendState.UDP_OK_SENT; } } private synchronized void sendChannelMessage(OSF2FChannelMsg msg) { int channelId = msg.getChannelId(); DatagramRateLimitedChannelQueue queue = queueMap.get(channelId); if (queue == null) { queue = new DatagramRateLimitedChannelQueue(channelId, sendThread); addQueue(queue); // Seed the new channel with half the number of tokens we have, and // half of what the main rate limiter has. transferTokens(queue, getAvailableTokens() / 2); DatagramRateLimiter mainRateLimiter = manager.getMainRateLimiter(); mainRateLimiter.transferTokens(queue, mainRateLimiter.getAvailableTokens() / 2); } queue.queuePacket(msg); } public void sendMessage(OSF2FMessage message) { if (sendState == SendState.CLOSED) { logger.finest("Tried to send packet on a closed connection"); return; } if (isTimedOut()) { logger.fine("Connection timed out (no packets received in a long time), closing"); close(); return; } try { if (message instanceof OSF2FChannelMsg) { // put in the appropriate channel queue sendChannelMessage((OSF2FChannelMsg) message); } else { // Send directly to socket. sendThread.queueMessage(message); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void sendUdpOK() { // We need to have received the remote init packet to be able to send. if (!(receiveState == ReceiveState.OK_SENT || receiveState == ReceiveState.ACTIVE)) { return; } if (sendState == SendState.ACTIVE) { sendMessage(new OSF2FDatagramOk(0)); } else if ((sendState == SendState.INIT_SENT || sendState == SendState.UDP_OK_SENT) && udpOkCount < MAX_UNACKED_UDP_OKs) { udpOkCount++; // Before the connection is set to active we need to make sure that // the remote side can receive udp packets of max mtu size (we use // 1400 bytes mtu without any path mtu discovery, 98% of paths can // support this packet size, and of the rest of the paths only 40% // handle // discovery anyway.). sendMessage(new OSF2FDatagramOk(MAX_DATAGRAM_PAYLOAD_SIZE)); } return; } @Override public String toString() { return "DatagramConnection-" + friendConnection.toString() + " "; } public boolean isTimedOut() { return System.currentTimeMillis() - lastPacketReceived > FriendConnection.KEEP_ALIVE_TIMEOUT; } public boolean isLanLocal() { return friendConnection.isLanLocal(); } public void reInitialize() { if (sendState == SendState.CLOSED) { logger.warning("tried to reinitialize closed connection"); return; } friendConnection.initDatagramConnection(); } public int getQueueLength() { return sendThread.queueLength; } public synchronized void clearExpiredChannels() { LinkedList<DatagramRateLimitedChannelQueue> toRemove = new LinkedList<DatagramRateLimitedChannelQueue>(); for (Iterator<DatagramRateLimitedChannelQueue> iterator = queueMap.values().iterator(); iterator .hasNext();) { DatagramRateLimitedChannelQueue queue = iterator.next(); if (queue.isExpired()) { toRemove.add(queue); } } for (DatagramRateLimitedChannelQueue queue : toRemove) { // Clear any messages in the queue. queue.clear(); // Take back any tokens, first give to the main connection DatagramRateLimiter mainRateLimiter = manager.getMainRateLimiter(); queue.transferTokens(mainRateLimiter, queue.getAvailableTokens()); // And any leftovers go to this connection. queue.transferTokens(this, queue.getAvailableTokens()); removeQueue(queue); } } /** * Sending encrypted udp packets is cpu intensive and potentially blocking. * Each connection is sending packets in its own thread. * * @author isdal * */ // Visible for testing. class DatagramSendThread implements Runnable { private long lastPacketSent = System.currentTimeMillis(); private final ByteBuffer[] unencryptedPayload; // Visible for testing. final LinkedBlockingQueue<OSF2FMessage> messageQueue; private final Thread thread; private final byte[] outgoingPacketBuf = new byte[2048]; private volatile boolean quit = false; private volatile int queueLength = 0; public DatagramSendThread() { messageQueue = new LinkedBlockingQueue<OSF2FMessage>(1024); thread = new Thread(this); thread.setName("DatagramSendThread-" + DatagramConnection.this.toString()); thread.setDaemon(true); unencryptedPayload = new ByteBuffer[MAX_DATAGRAM_PAYLOAD_SIZE]; } public void quit() { quit = true; thread.interrupt(); } public void start() { thread.start(); } public void queueMessage(OSF2FMessage message) throws InterruptedException { int messageSize = message.getMessageSize(); if (messageSize > MAX_DATAGRAM_PAYLOAD_SIZE) { logger.warning("tried to send too large datagram: " + messageSize); return; } queueLength += messageSize + OSF2FMessage.MESSAGE_HEADER_LEN; if (logger.isLoggable(Level.FINEST)) { logger.finest("message queued, queue_length=" + queueLength); } messageQueue.put(message); } @Override public void run() { try { // Create a vehicle for sending more than one message in the // same datagram. RawMessage[] messageBuffer = new RawMessage[1 + MAX_DATAGRAM_PAYLOAD_SIZE / 5]; while (!quit) { int datagramSize = 0; int packetNum = 0; OSF2FMessage message = messageQueue.take(); synchronized (encrypter) { do { final int messageSize = message.getMessageSize(); datagramSize += messageSize + OSF2FMessage.MESSAGE_HEADER_LEN; messageBuffer[packetNum++] = OSF2FMessageFactory .createOSF2FRawMessage(message); if (logger.isLoggable(Level.FINEST)) { logger.finest(String.format( "Adding message, packets=%d, size=%d message=%s", packetNum, datagramSize, message.getDescription())); } // This is going to get sent, update the queue size queueLength -= messageSize + OSF2FMessage.MESSAGE_HEADER_LEN; // Check if we can fit more packets in there. } while ((message = messageQueue.peek()) != null && datagramSize + message.getMessageSize() <= MAX_DATAGRAM_PAYLOAD_SIZE && (message = messageQueue.remove()) != null); sendMessage(messageBuffer, packetNum); // If we merged packets we can reuse the saved bytes. int headerBytesSaved = (packetNum - 1) * (DatagramEncrypter.SEQUENCE_NUMBER_BYTES + DatagramEncrypter.HMAC_SIZE); if (headerBytesSaved > 0) { DatagramConnection.this.refillBucket(headerBytesSaved); } } } } catch (InterruptedException e) { logger.fine("Datagram send thread closed: " + DatagramConnection.this.toString()); OSF2FMessage message; while ((message = messageQueue.poll()) != null) { message.destroy(); } } } /** * Send a message over this UDP connection. * * @param message */ private void sendMessage(RawMessage[] messages, int num) { try { lastPacketSent = System.currentTimeMillis(); int size = 0; int buffers = 0; for (int messageNum = 0; messageNum < num; messageNum++) { // Get the message data. DirectByteBuffer[] data = messages[messageNum].getRawData(); for (int i = 0; i < data.length; i++) { ByteBuffer bb = data[i].getBuffer(SS); unencryptedPayload[buffers++] = bb; size += bb.remaining(); } } if (logger.isLoggable(Level.FINEST)) { logger.finest("encrypting " + size + " bytes"); } if (size > outgoingPacketBuf.length) { logger.warning("Attempting to encrypt over-full packet of size " + size + " bytes."); } // Encrypt the serialized payload into the payload buffer. EncryptedPacket encrypted = encrypter.encrypt(unencryptedPayload, buffers, outgoingPacketBuf); // Create and send the packet. DatagramPacket packet = new DatagramPacket(outgoingPacketBuf, 0, encrypted.getLength(), remoteIp, remotePort); manager.send(packet, friendConnection.isLanLocal()); } catch (Exception e) { e.printStackTrace(); sendState = SendState.CLOSED; } finally { // Return the incoming messages buffers to the pool. for (int i = 0; i < num; i++) { messages[i].destroy(); } } } } public int getRemotePort() { return remotePort; } }