package com.limegroup.gnutella; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; import java.net.SocketAddress; import java.net.SocketException; import java.net.BindException; import java.net.ConnectException; import java.net.NoRouteToHostException; import java.net.PortUnreachableException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; import java.nio.ByteBuffer; import java.util.List; import java.util.LinkedList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.guess.GUESSEndpoint; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.BufferByteArrayOutputStream; import com.limegroup.gnutella.io.ByteBufferCache; import com.limegroup.gnutella.io.ReadWriteObserver; import com.limegroup.gnutella.io.NIODispatcher; /** * This class handles UDP messaging services. It both sends and * receives messages, routing received messages to their appropriate * handlers. This also handles issues related to the GUESS proposal, * such as making sure that the UDP and TCP port match and sending * UDP acks for queries. * * @see UDPReplyHandler * @see MessageRouter * @see QueryUnicaster * */ public class UDPService implements ReadWriteObserver { private static final Log LOG = LogFactory.getLog(UDPService.class); /** * Constant for the single <tt>UDPService</tt> instance. */ private final static UDPService INSTANCE = new UDPService(); /** * The DatagramChannel we're reading from & writing to. */ private DatagramChannel _channel; /** * The list of messages to be sent, as SendBundles. */ private final List OUTGOING_MSGS; /** * The buffer that's re-used for reading incoming messages. */ private final ByteBuffer BUFFER; /** * The maximum size of a UDP message we'll accept. */ private final int BUFFER_SIZE = 1024 * 2; /** True if the UDPService has ever received a solicited incoming UDP * packet. */ private volatile boolean _acceptedSolicitedIncoming = false; /** True if the UDPService has ever received a unsolicited incoming UDP * packet. */ private volatile boolean _acceptedUnsolicitedIncoming = false; /** The last time the _acceptedUnsolicitedIncoming was set. */ private long _lastUnsolicitedIncomingTime = 0; /** * The last time we received any udp packet */ private volatile long _lastReceivedAny = 0; /** The last time we sent a UDP Connect Back. */ private long _lastConnectBackTime = System.currentTimeMillis(); void resetLastConnectBackTime() { _lastConnectBackTime = System.currentTimeMillis() - Acceptor.INCOMING_EXPIRE_TIME; } /** Whether our NAT assigns stable ports for successive connections * LOCKING: this */ private boolean _portStable = true; /** The last reported port as seen from the outside * LOCKING: this */ private int _lastReportedPort; /** * The number of pongs carrying IP:Port info we have received. * LOCKING: this */ private int _numReceivedIPPongs; /** * The GUID that we advertise out for UDPConnectBack requests. */ private final GUID CONNECT_BACK_GUID = new GUID(GUID.makeGuid()); /** * The GUID that we send for Pings, useful to test solicited support. */ private final GUID SOLICITED_PING_GUID = new GUID(GUID.makeGuid()); /** * Determines if this was ever started. */ private boolean _started = false; /** * The time between UDP pings. Used by the PeriodicPinger. This is * useful for nodes behind certain firewalls (notably the MS firewall). */ private static final long PING_PERIOD = 85 * 1000; // 85 seconds /** * A buffer used for reading the header of incoming messages. */ private static final byte[] IN_HEADER_BUF = new byte[23]; /** * Instance accessor. */ public static UDPService instance() { return INSTANCE; } /** * Constructs a new <tt>UDPAcceptor</tt>. */ protected UDPService() { OUTGOING_MSGS = new LinkedList(); byte[] backing = new byte[BUFFER_SIZE]; BUFFER = ByteBuffer.wrap(backing); scheduleServices(); } /** * Schedules IncomingValidator & PeriodicPinger for periodic use. */ protected void scheduleServices() { RouterService.schedule(new IncomingValidator(), Acceptor.TIME_BETWEEN_VALIDATES, Acceptor.TIME_BETWEEN_VALIDATES); RouterService.schedule(new PeriodicPinger(), 0, PING_PERIOD); } /** @return The GUID to send for UDPConnectBack attempts.... */ public GUID getConnectBackGUID() { return CONNECT_BACK_GUID; } /** @return The GUID to send for Solicited Ping attempts.... */ public GUID getSolicitedGUID() { return SOLICITED_PING_GUID; } /** * Starts listening for UDP messages & allowing UDP messages to be written. */ public void start() { DatagramChannel channel; synchronized(this) { _started = true; channel = _channel; } if(channel != null) NIODispatcher.instance().registerReadWrite(channel, this); } /** * Returns a new DatagramSocket that is bound to the given port. This * value should be passed to setListeningSocket(DatagramSocket) to commit * to the new port. If setListeningSocket is NOT called, you should close * the return socket. * @return a new DatagramSocket that is bound to the specified port. * @exception IOException Thrown if the DatagramSocket could not be * created. */ DatagramSocket newListeningSocket(int port) throws IOException { try { DatagramChannel channel = DatagramChannel.open(); channel.configureBlocking(false); DatagramSocket s = channel.socket(); s.setReceiveBufferSize(64*1024); s.setSendBufferSize(64*1024); s.bind(new InetSocketAddress(port)); return s; } catch (SecurityException se) { throw new IOException("security exception on port: "+port); } } /** * Changes the DatagramSocket used for sending/receiving. Typically called * by Acceptor to commit to the new port. * @param datagramSocket the new listening socket, which must be be the * return value of newListeningSocket(int). A value of null disables * UDP sending and receiving. */ void setListeningSocket(DatagramSocket datagramSocket) { if(_channel != null) { try { _channel.close(); } catch(IOException ignored) {} } if(datagramSocket != null) { boolean wasStarted; synchronized(this) { _channel = datagramSocket.getChannel(); if(_channel == null) throw new IllegalArgumentException("No channel!"); wasStarted = _started; // set the port in the FWT records _lastReportedPort=_channel.socket().getLocalPort(); _portStable=true; } // If it was already started at one point, re-start to register this new channel. if(wasStarted) start(); } } /** * Shuts down this service. */ public void shutdown() { setListeningSocket(null); } /** * Notification that a read can happen. */ public void handleRead() throws IOException { while(true) { BUFFER.clear(); SocketAddress from; try { from = _channel.receive(BUFFER); } catch(IOException iox) { break; } catch(Error error) { // Stupid implementations giving bogus errors. Grrr!. break; } // no packet. if(from == null) break; if(!(from instanceof InetSocketAddress)) { Assert.silent(false, "non-inet SocketAddress: " + from); continue; } InetSocketAddress addr = (InetSocketAddress)from; if(!NetworkUtils.isValidAddress(addr.getAddress())) continue; if(!NetworkUtils.isValidPort(addr.getPort())) continue; byte[] data = BUFFER.array(); int length = BUFFER.position(); try { // we do things the old way temporarily InputStream in = new ByteArrayInputStream(data, 0, length); Message message = Message.read(in, Message.N_UDP, IN_HEADER_BUF); if(message == null) continue; processMessage(message, addr); } catch (IOException ignored) { } catch (BadPacketException ignored) { } } } /** * Notification that an IOException occurred while reading/writing. */ public void handleIOException(IOException iox) { if( !(iox instanceof java.nio.channels.ClosedChannelException ) ) ErrorService.error(iox, "UDP Error."); else LOG.trace("Swallowing a UDPService ClosedChannelException", iox); } /** * Processes a single message. */ protected void processMessage(Message message, InetSocketAddress addr) { updateState(message, addr); MessageDispatcher.instance().dispatchUDP(message, addr); } /** Updates internal state of the UDP Service. */ private void updateState(Message message, InetSocketAddress addr) { _lastReceivedAny = System.currentTimeMillis(); if (!isGUESSCapable()) { if (message instanceof PingRequest) { GUID guid = new GUID(message.getGUID()); if(isValidForIncoming(CONNECT_BACK_GUID, guid, addr)) { _acceptedUnsolicitedIncoming = true; } _lastUnsolicitedIncomingTime = _lastReceivedAny; } else if (message instanceof PingReply) { GUID guid = new GUID(message.getGUID()); if(!isValidForIncoming(SOLICITED_PING_GUID, guid, addr )) return; _acceptedSolicitedIncoming = true; PingReply r = (PingReply)message; if (r.getMyPort() != 0) { synchronized(this){ _numReceivedIPPongs++; if (_numReceivedIPPongs==1) _lastReportedPort=r.getMyPort(); else if (_lastReportedPort!=r.getMyPort()) { _portStable = false; _lastReportedPort = r.getMyPort(); } } } } } // ReplyNumberVMs are always sent in an unsolicited manner, // so we can use this fact to keep the last unsolicited up // to date if (message instanceof ReplyNumberVendorMessage) _lastUnsolicitedIncomingTime = _lastReceivedAny; } /** * Determines whether or not the specified message is valid for setting * LimeWire as accepting UDP messages (solicited or unsolicited). */ private boolean isValidForIncoming(GUID match, GUID guidReceived, InetSocketAddress addr) { if(!match.equals(guidReceived)) return false; String host = addr.getAddress().getHostAddress(); // If addr is connected to us, then return false. Otherwise (not connected), only return true if either: // 1) the non-connected party is NOT private // OR // 2) the non-connected party _is_ private, and the LOCAL_IS_PRIVATE is set to false return !RouterService.getConnectionManager().isConnectedTo(host) && !NetworkUtils.isPrivateAddress(addr.getAddress()) ; } /** * Sends the specified <tt>Message</tt> to the specified host. * * @param msg the <tt>Message</tt> to send * @param host the host to send the message to */ public void send(Message msg, IpPort host) { send(msg, host.getInetAddress(), host.getPort()); } /** * Sends the <tt>Message</tt> via UDP to the port and IP address specified. * This method should not be called if the client is not GUESS enabled. * * @param msg the <tt>Message</tt> to send * @param ip the <tt>InetAddress</tt> to send to * @param port the port to send to */ public void send(Message msg, InetAddress ip, int port) throws IllegalArgumentException { try { send(msg, InetAddress.getByAddress(ip.getAddress()), port, ErrorService.getErrorCallback()); } catch(UnknownHostException ignored) {} } /** * Sends the <tt>Message</tt> via UDP to the port and IP address specified. * This method should not be called if the client is not GUESS enabled. * * @param msg the <tt>Message</tt> to send * @param ip the <tt>InetAddress</tt> to send to * @param port the port to send to * @param err an <tt>ErrorCallback<tt> if you want to be notified errors * @throws IllegalArgumentException if msg, ip, or err is null. */ public void send(Message msg, InetAddress ip, int port, ErrorCallback err) throws IllegalArgumentException { if (err == null) throw new IllegalArgumentException("Null ErrorCallback"); if (msg == null) throw new IllegalArgumentException("Null Message"); if (ip == null) throw new IllegalArgumentException("Null InetAddress"); if (!NetworkUtils.isValidPort(port)) throw new IllegalArgumentException("Invalid Port: " + port); if(_channel == null || _channel.socket().isClosed()) return; // ignore if not open. BufferByteArrayOutputStream baos = new BufferByteArrayOutputStream( NIODispatcher.instance().getBufferCache().getHeap(msg.getTotalLength())); try { msg.writeQuickly(baos); } catch(IOException e) { // this should not happen -- we should always be able to write // to this output stream in memory ErrorService.error(e); // can't send the hit, so return return; } ByteBuffer buffer = (ByteBuffer)baos.buffer().flip(); synchronized(OUTGOING_MSGS) { OUTGOING_MSGS.add(new SendBundle(buffer, ip, port, err)); if(_channel != null) NIODispatcher.instance().interestWrite(_channel, true); } } /** * Notification that a write can happen. */ public boolean handleWrite() throws IOException { synchronized(OUTGOING_MSGS) { while(!OUTGOING_MSGS.isEmpty()) { boolean releaseBuffer = true; SendBundle bundle = (SendBundle)OUTGOING_MSGS.remove(0); try { if(_channel.send(bundle.buffer, bundle.addr) == 0) { // we removed the bundle from the list but couldn't send it, // so we have to put it back in. OUTGOING_MSGS.add(0, bundle); releaseBuffer = false; return true; // no room left to send. } } catch(BindException ignored) { } catch(ConnectException ignored) { } catch(NoRouteToHostException ignored) { } catch(PortUnreachableException ignored) { } catch(SocketException ignored) { LOG.warn("Ignoring exception on socket", ignored); } finally { if (releaseBuffer) NIODispatcher.instance().getBufferCache().release(bundle.buffer); } } // if there's no data left to send, we don't wanna be notified of write events. NIODispatcher.instance().interestWrite(_channel, false); return false; } } /** Wrapper for outgoing data */ private static class SendBundle { private final ByteBuffer buffer; private final SocketAddress addr; private final ErrorCallback callback; SendBundle(ByteBuffer b, InetAddress addr, int port, ErrorCallback c) { buffer = b; this.addr = new InetSocketAddress(addr, port); callback = c; } } /** * Returns whether or not this node is capable of sending its own * GUESS queries. This would not be the case only if this node * has not successfully received an incoming UDP packet. * * @return <tt>true</tt> if this node is capable of running its own * GUESS queries, <tt>false</tt> otherwise */ public boolean isGUESSCapable() { return canReceiveUnsolicited() && canReceiveSolicited(); } /** * Returns whether or not this node is capable of receiving UNSOLICITED * UDP packets. It is false until a UDP ConnectBack ping has been received. * * @return <tt>true</tt> if this node has accepted a UNSOLICITED UDP packet. */ public boolean canReceiveUnsolicited() { return _acceptedUnsolicitedIncoming; } /** * Returns whether or not this node is capable of receiving SOLICITED * UDP packets. * * @return <tt>true</tt> if this node has accepted a SOLICITED UDP packet. */ public boolean canReceiveSolicited() { return _acceptedSolicitedIncoming; } /** * * @return whether this node can do Firewall-to-firewall transfers. * Until we get back any udp packet, the answer is no. * If we have received an udp packet but are not connected, or haven't * received a pong carrying ip info yet, see if we ever disabled fwt in the * past. * If we are connected and have gotten a single ip pong, our port must be * the same as our tcp port or our forced tcp port. * If we have received more than one ip pong, they must all report the same * port. */ public boolean canDoFWT(){ // this does not affect EVER_DISABLED_FWT. if (!canReceiveSolicited()) return false; if (!RouterService.isConnected()) return !ConnectionSettings.LAST_FWT_STATE.getValue(); boolean ret = true; synchronized(this) { if (_numReceivedIPPongs < 1) return !ConnectionSettings.LAST_FWT_STATE.getValue(); if (LOG.isTraceEnabled()) { LOG.trace("stable "+_portStable+ " last reported port "+_lastReportedPort+ " our external port "+RouterService.getPort()+ " our non-forced port "+RouterService.getAcceptor().getPort(false)+ " number of received IP pongs "+_numReceivedIPPongs+ " valid external addr "+NetworkUtils.isValidAddress( RouterService.getExternalAddress())); } ret= NetworkUtils.isValidAddress(RouterService.getExternalAddress()) && _portStable; if (_numReceivedIPPongs == 1){ ret = ret && (_lastReportedPort == RouterService.getAcceptor().getPort(false) || _lastReportedPort == RouterService.getPort()); } } ConnectionSettings.LAST_FWT_STATE.setValue(!ret); return ret; } // Some getters for bug reporting public boolean portStable() { return _portStable; } public int receivedIpPong() { return _numReceivedIPPongs; } public int lastReportedPort() { return _lastReportedPort; } /** * @return the stable UDP port as seen from the outside. * If we have received more than one IPPongs and they report * the same port, we return that. * If we have received just one IPpong, and if its address * matches either our local port or external port, return that. * If we have not received any IPpongs, return whatever * RouterService thinks our port is. */ public int getStableUDPPort() { int localPort = RouterService.getAcceptor().getPort(false); int forcedPort = RouterService.getPort(); synchronized(this) { if (_portStable && _numReceivedIPPongs > 1) return _lastReportedPort; if (_numReceivedIPPongs == 1 && (localPort == _lastReportedPort || forcedPort == _lastReportedPort)) return _lastReportedPort; } return forcedPort; // we haven't received an ippong. } /** * Sets whether or not this node is capable of receiving SOLICITED * UDP packets. This is useful for testing UDPConnections. * */ public void setReceiveSolicited(boolean value) { _acceptedSolicitedIncoming = value; } public long getLastReceivedTime() { return _lastReceivedAny; } /** * Returns whether or not the UDP socket is listening for incoming * messsages. * * @return <tt>true</tt> if the UDP socket is listening for incoming * UDP messages, <tt>false</tt> otherwise */ public boolean isListening() { if(_channel == null) return false; return (_channel.socket().getLocalPort() != -1); } /** * Overrides Object.toString to give more informative information * about the class. * * @return the <tt>DatagramSocket</tt> data */ public String toString() { return "UDPService::channel: " + _channel; } private static class MLImpl implements MessageListener { public boolean _gotIncoming = false; public void processMessage(Message m, ReplyHandler handler) { if ((m instanceof PingRequest)) _gotIncoming = true; } public void registered(byte[] guid) {} public void unregistered(byte[] guid) {} } private class IncomingValidator implements Runnable { public IncomingValidator() {} public void run() { // clear and revalidate if 1) we haven't had in incoming in an hour // or 2) we've never had incoming and we haven't checked in an hour final long currTime = System.currentTimeMillis(); final MessageRouter mr = RouterService.getMessageRouter(); final ConnectionManager cm = RouterService.getConnectionManager(); // if these haven't been created yet, exit and wait till they have. if(mr == null || cm == null) return; if ( (_acceptedUnsolicitedIncoming && //1) ((currTime - _lastUnsolicitedIncomingTime) > Acceptor.INCOMING_EXPIRE_TIME)) || (!_acceptedUnsolicitedIncoming && //2) ((currTime - _lastConnectBackTime) > Acceptor.INCOMING_EXPIRE_TIME)) ) { final GUID cbGuid = new GUID(GUID.makeGuid()); final MLImpl ml = new MLImpl(); mr.registerMessageListener(cbGuid.bytes(), ml); // send a connectback request to a few peers and clear if(cm.sendUDPConnectBackRequests(cbGuid)) { _lastConnectBackTime = System.currentTimeMillis(); Runnable checkThread = new Runnable() { public void run() { if ((_acceptedUnsolicitedIncoming && (_lastUnsolicitedIncomingTime < currTime)) || (!_acceptedUnsolicitedIncoming)) { // we set according to the message listener _acceptedUnsolicitedIncoming = ml._gotIncoming; } mr.unregisterMessageListener(cbGuid.bytes(), ml); } }; RouterService.schedule(checkThread, Acceptor.WAIT_TIME_AFTER_REQUESTS, 0); } else mr.unregisterMessageListener(cbGuid.bytes(), ml); } } } private class PeriodicPinger implements Runnable { public void run() { // straightforward - send a UDP ping to a host. it doesn't really // matter who the guy is - we are just sending to open up any // potential firewall to UDP traffic GUESSEndpoint ep = QueryUnicaster.instance().getUnicastEndpoint(); if (ep == null) return; // only do this if you can receive some form of UDP traffic. if (!canReceiveSolicited() && !canReceiveUnsolicited()) return; // good to use the solicited guid PingRequest pr = new PingRequest(getSolicitedGUID().bytes(), (byte)1, (byte)0); pr.addIPRequest(); send(pr, ep.getAddress(), ep.getPort()); } } }