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.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.core.api.connection.FWTStatusReason; import org.limewire.core.api.connection.FirewallTransferStatus; import org.limewire.core.api.connection.FirewallTransferStatusEvent; import org.limewire.core.settings.ConnectionSettings; import org.limewire.inject.EagerSingleton; import org.limewire.inspection.Inspectable; import org.limewire.inspection.InspectionPoint; import org.limewire.io.ByteBufferOutputStream; import org.limewire.io.GUID; import org.limewire.io.IpPort; import org.limewire.io.NetworkInstanceUtils; import org.limewire.io.NetworkUtils; import org.limewire.listener.AsynchronousEventBroadcaster; import org.limewire.listener.EventListener; import org.limewire.listener.ListenerSupport; import org.limewire.nio.NIODispatcher; import org.limewire.nio.observer.ReadWriteObserver; import org.limewire.rudp.ConnectionState; import org.limewire.rudp.UDPSocketChannelConnectionEvent; import org.limewire.security.AddressSecurityToken; import org.limewire.security.MACCalculator; import org.limewire.security.MACCalculatorRepositoryManager; import org.limewire.service.ErrorService; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.filters.IPFilter; import com.limegroup.gnutella.guess.GUESSEndpoint; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.MessageFactory; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.PingRequestFactory; import com.limegroup.gnutella.messages.Message.Network; import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage; /** * 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 * */ @EagerSingleton public class UDPService implements ReadWriteObserver { private static final Log LOG = LogFactory.getLog(UDPService.class); private static final MACCalculator PING_GENERATOR = MACCalculatorRepositoryManager.createDefaultCalculatorFactory().createMACCalculator(); /** * The DatagramChannel we're reading from & writing to. */ private DatagramChannel _channel; /** * The list of messages to be sent, as SendBundles. */ private final List<SendBundle> 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(); /** * Whether or not a firewall transfer has successfully been done */ private volatile boolean _successfulFWT; void resetLastConnectBackTime() { _lastConnectBackTime = System.currentTimeMillis() - acceptor.get().getIncomingExpireTime(); } /** The last reported port as seen from the outside * LOCKING: this */ private int _lastReportedPort; /** * The class C network that last reported our port to us * in a solicited response */ private int _lastReportedClassC; /** * 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]; private final NetworkManager networkManager; private final Provider<MessageDispatcher> messageDispatcher; private final Provider<IPFilter> ipFilter; private final Provider<ConnectionManager> connectionManager; private final Provider<MessageRouter> messageRouter; private final Provider<Acceptor> acceptor; private final Provider<QueryUnicaster> queryUnicaster; private final ScheduledExecutorService backgroundExecutor; private final ConnectionServices connectionServices; private final MessageFactory messageFactory; private final PingRequestFactory pingRequestFactory; private final NetworkInstanceUtils networkInstanceUtils; private final AsynchronousEventBroadcaster<FirewallTransferStatusEvent> fwtStatusBroadcaster; @InspectionPoint("udp sent messages") private final Message.MessageCounter sentMessageCounter = new Message.MessageCounter(50); @InspectionPoint("fwt capable") @SuppressWarnings("unused") private final Inspectable fwtCapable = new Inspectable() { @Override public Object inspect() { return canDoFWT(); } }; @Inject public UDPService(NetworkManager networkManager, Provider<MessageDispatcher> messageDispatcher, @Named("hostileFilter") Provider<IPFilter> ipFilter, Provider<ConnectionManager> connectionManager, Provider<MessageRouter> messageRouter, Provider<Acceptor> acceptor, Provider<QueryUnicaster> queryUnicaster, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, ConnectionServices connectionServices, MessageFactory messageFactory, PingRequestFactory pingRequestFactory, NetworkInstanceUtils networkInstanceUtils, AsynchronousEventBroadcaster<FirewallTransferStatusEvent> fwtStatusBroadcaster, ListenerSupport<UDPSocketChannelConnectionEvent> channelEventListenerSupport) { this.networkManager = networkManager; this.messageDispatcher = messageDispatcher; this.ipFilter = ipFilter; this.connectionManager = connectionManager; this.messageRouter = messageRouter; this.acceptor = acceptor; this.queryUnicaster = queryUnicaster; this.backgroundExecutor = backgroundExecutor; this.connectionServices = connectionServices; this.messageFactory = messageFactory; this.pingRequestFactory = pingRequestFactory; this.networkInstanceUtils = networkInstanceUtils; this.fwtStatusBroadcaster = fwtStatusBroadcaster; OUTGOING_MSGS = new LinkedList<SendBundle>(); byte[] backing = new byte[BUFFER_SIZE]; BUFFER = ByteBuffer.wrap(backing); // TODO convert this to a Service and move this // TODO initialize() fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent( FirewallTransferStatus.DOES_NOT_SUPPORT_FWT, FWTStatusReason.UNKNOWN)); channelEventListenerSupport.addListener(new UDPConnectionListener()); } /** * Schedules IncomingValidator & PeriodicPinger for periodic use. */ protected void scheduleServices() { backgroundExecutor.scheduleWithFixedDelay(new IncomingValidator(), acceptor.get().getTimeBetweenValidates(), acceptor.get().getTimeBetweenValidates(), TimeUnit.MILLISECONDS); backgroundExecutor.scheduleWithFixedDelay(new PeriodicPinger(), 0, PING_PERIOD, TimeUnit.MILLISECONDS); } /** @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() { if(!_started) scheduleServices(); 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. */ public 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. */ public void setListeningSocket(DatagramSocket datagramSocket) { if(_channel != null) { try { LOG.debug("Closing socket"); _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(); _successfulFWT = false; } // If it was already started at one point, re-start to register this new channel. if(wasStarted) start(); } } int getListeningPort() { synchronized(this) { if(_channel != null) return _channel.socket().getLocalPort(); else return -1; } } /** * Shuts down this service. */ public void shutdown() { setListeningSocket(null); } /** * Notification that a read can happen. */ public void handleRead() throws IOException { try { 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)) { ErrorService.error(new RuntimeException("non-inet SocketAddress: " + from)); continue; } InetSocketAddress addr = (InetSocketAddress) from; if (!NetworkUtils.isValidAddress(addr.getAddress())) continue; if (!NetworkUtils.isValidPort(addr.getPort())) continue; // don't go further if filtered. if(!ipFilter.get().allow(addr.getAddress().getAddress())) { LOG.debug("Received packet from hostile host"); return; } 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 = messageFactory.read(in, Network.UDP, IN_HEADER_BUF, addr); if(message == null) { LOG.debug("Received a null message"); continue; } processMessage(message, addr); } catch(IOException e) { LOG.debug("Could not parse message", e); } catch(BadPacketException e) { LOG.debug("Could not parse message", e); } } } catch(Throwable t) { // Do not let the exceptions propogate out, as that could // close UDPService. ErrorService.error(t); } } /** * 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.debug("Swallowing a UDPService ClosedChannelException", iox); } /** * Processes a single message. */ protected void processMessage(Message message, InetSocketAddress addr) { // FIXME: redundant check? if(!ipFilter.get().allow(message)) { LOG.debug("Received packet from hostile host"); return; } // FIXME: why do we mutate the GUIDs of ping replies? if(message instanceof PingReply) mutateGUID(message.getGUID(), addr.getAddress(), addr.getPort()); updateState(message, addr); messageDispatcher.get().dispatchUDP(message, addr); } /** Updates internal state of the UDP Service. */ private void updateState(Message message, InetSocketAddress addr) { _lastReceivedAny = System.currentTimeMillis(); if (isValidForIncoming(addr)) { if(!_acceptedSolicitedIncoming) LOG.info("Can receive solicited UDP"); _acceptedSolicitedIncoming = true; } if (!isGUESSCapable()) { if (message instanceof PingRequest) { GUID guid = new GUID(message.getGUID()); if(CONNECT_BACK_GUID.equals(guid) && isValidForIncoming(addr)) { synchronized (this) { if(!_acceptedUnsolicitedIncoming) LOG.info("Can receive unsolicited UDP"); _acceptedUnsolicitedIncoming = true; _lastReportedPort = networkManager.getPort(); // sent by the connect back request ConnectionSettings.HAS_STABLE_PORT.setValue(true); } updateFWTState(); } _lastUnsolicitedIncomingTime = _lastReceivedAny; } else if (message instanceof PingReply) { GUID guid = new GUID(message.getGUID()); if(!SOLICITED_PING_GUID.equals(guid) || !isValidForIncoming(addr )) return; PingReply r = (PingReply)message; if (r.getMyPort() != 0) { synchronized(this){ _numReceivedIPPongs++; if(LOG.isDebugEnabled()) { LOG.debug("Received IP pong from " + r.getAddress() + ":" + r.getPort() + " reporting " + r.getMyInetAddress().getHostAddress() + ":" + r.getMyPort()); } if(_numReceivedIPPongs > 1) { if(_lastReportedClassC != NetworkUtils.getClassC(r.getInetAddress())) { if(_lastReportedPort == r.getMyPort()) { ConnectionSettings.HAS_STABLE_PORT.setValue(true); } else { ConnectionSettings.HAS_STABLE_PORT.setValue(false); } } } _lastReportedPort = r.getMyPort(); _lastReportedClassC = NetworkUtils.getClassC(r.getInetAddress()); } updateFWTState(); } } } // 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; } public static void mutateGUID(byte[] guid, InetAddress ip, int port) { byte[] qk = PING_GENERATOR.getMACBytes(new AddressSecurityToken.AddressTokenData(ip,port)); for (int i = 0; i < qk.length; i++) guid[i] =(byte)(guid[i] ^ qk[i]); } /** * Determines whether or not the specified message is valid for setting * LimeWire as accepting UDP messages (solicited or unsolicited). */ private boolean isValidForIncoming(InetSocketAddress addr) { 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 !connectionManager.get().isConnectedTo(host) && !networkInstanceUtils.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.getInetSocketAddress()); } /** * 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 * @throws IllegalArgumentException if msg, ip, or err is null. */ public void send(Message msg, InetAddress ip, int port) { send(msg, new InetSocketAddress(ip, port)); } /** * Sends the specified <tt>Message</tt> to the specified host. * * @param msg the <tt>Message</tt> to send * @param addr the network address of the host to send the message to */ public void send(Message msg, InetSocketAddress addr) { if (msg == null) throw new IllegalArgumentException("Null Message"); if (!NetworkUtils.isValidSocketAddress(addr)) throw new IllegalArgumentException("Invalid addr: " + addr); if(_channel == null || _channel.socket().isClosed()) { LOG.debug("Socket not ready for writing"); return; } int length = msg.getTotalLength(); ByteBuffer buffer = NIODispatcher.instance().getBufferCache().getHeap(length); if(buffer.remaining() != length) throw new IllegalStateException("retrieved a buffer with wrong remaining! " + "wanted: " + length + ", had: " + buffer.remaining() + ", position: " + buffer.position() + ", limit: " + buffer.limit()); ByteBufferOutputStream baos = new ByteBufferOutputStream(buffer); 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; } buffer.flip(); if (msg instanceof PingRequest) mutateGUID(buffer.array(), addr.getAddress(), addr.getPort()); sentMessageCounter.countMessage(msg); send(buffer, addr, false); } public void send(ByteBuffer buffer, InetSocketAddress addr, boolean custom) { synchronized(OUTGOING_MSGS) { OUTGOING_MSGS.add(new SendBundle(buffer, addr, custom)); if(_channel != null) NIODispatcher.instance().interestWrite(_channel, true); } } /** * Notification that a write can happen. */ public boolean handleWrite() throws IOException { try { synchronized(OUTGOING_MSGS) { while(!OUTGOING_MSGS.isEmpty()) { boolean releaseBuffer = true; SendBundle bundle = 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(IOException ignored) { LOG.warn("Ignoring exception on socket", ignored); } finally { if(bundle.custom) { bundle.buffer.rewind(); releaseBuffer = false; } 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; } } catch(Throwable t) { // Don't let it propogate, since that could close UDPService! ErrorService.error(t); return true; } } /** Wrapper for outgoing data */ private static class SendBundle { private final ByteBuffer buffer; private final SocketAddress addr; private final boolean custom; SendBundle(ByteBuffer b, InetSocketAddress addr, boolean custom) { buffer = b; this.addr = addr; this.custom = custom; } } /** * 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(){ boolean retValue = false; // this does not affect EVER_DISABLED_FWT. if (!canReceiveSolicited()) { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.DOES_NOT_SUPPORT_FWT, FWTStatusReason.NO_SOLICITED_INCOMING_MESSAGES)); retValue = false; } else { boolean canDoFWTSetting = !ConnectionSettings.CANNOT_DO_FWT.getValue(); if (!connectionServices.isConnected()) { if(canDoFWTSetting) { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.SUPPORTS_FWT, FWTStatusReason.REUSING_STATUS_FROM_PREVIOUS_SESSION)); } else { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.DOES_NOT_SUPPORT_FWT, FWTStatusReason.REUSING_STATUS_FROM_PREVIOUS_SESSION)); } retValue = canDoFWTSetting; } else { boolean needToUpdateState; synchronized(this) { if (_numReceivedIPPongs < 1) { if(canDoFWTSetting) { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.SUPPORTS_FWT, FWTStatusReason.REUSING_STATUS_FROM_PREVIOUS_SESSION)); } else { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.DOES_NOT_SUPPORT_FWT, FWTStatusReason.REUSING_STATUS_FROM_PREVIOUS_SESSION)); } needToUpdateState = false; retValue = canDoFWTSetting; } else { needToUpdateState = true; } } if(needToUpdateState) { updateFWTState(); retValue = !ConnectionSettings.CANNOT_DO_FWT.getValue(); } } } return retValue; } private void updateFWTState() { boolean newFWTSetting = true; FWTStatusReason reason = FWTStatusReason.UNKNOWN; synchronized(this) { if (LOG.isTraceEnabled()) { LOG.trace("stable port "+ConnectionSettings.HAS_STABLE_PORT.getValue() + " last reported port "+_lastReportedPort+ " our external port "+networkManager.getPort()+ " our non-forced port "+acceptor.get().getPort(false)+ " number of received IP pongs "+_numReceivedIPPongs+ " valid external addr "+NetworkUtils.isValidAddress( networkManager.getExternalAddress())); } if(_successfulFWT || _acceptedUnsolicitedIncoming) { newFWTSetting = true; } else { if(!NetworkUtils.isValidAddress(networkManager.getExternalAddress())) { reason = FWTStatusReason.INVALID_EXTERNAL_ADDRESS; newFWTSetting = false; } boolean stablePort = ConnectionSettings.HAS_STABLE_PORT.getValue(); newFWTSetting = newFWTSetting && stablePort; if(!stablePort) { if(_numReceivedIPPongs < 2) { reason = FWTStatusReason.REUSING_STATUS_FROM_PREVIOUS_SESSION; } else { reason = FWTStatusReason.PORT_UNSTABLE; } } if (_numReceivedIPPongs == 1){ newFWTSetting = newFWTSetting && (_lastReportedPort == acceptor.get().getPort(false) || _lastReportedPort == networkManager.getPort()); } } } if(newFWTSetting) { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.SUPPORTS_FWT, FWTStatusReason.UNKNOWN)); } else { fwtStatusBroadcaster.broadcast(new FirewallTransferStatusEvent(FirewallTransferStatus.DOES_NOT_SUPPORT_FWT, reason)); } ConnectionSettings.CANNOT_DO_FWT.setValue(!newFWTSetting); } // Some getters for bug reporting public boolean portStable() { return ConnectionSettings.HAS_STABLE_PORT.getValue(); } 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 = acceptor.get().getPort(false); int forcedPort = networkManager.getPort(); synchronized(this) { if (ConnectionSettings.HAS_STABLE_PORT.getValue() && _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() { DatagramChannel channel = _channel; 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 */ @Override 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(); if ( (_acceptedUnsolicitedIncoming && //1) ((currTime - _lastUnsolicitedIncomingTime) > acceptor.get().getIncomingExpireTime())) || (!_acceptedUnsolicitedIncoming && //2) ((currTime - _lastConnectBackTime) > acceptor.get().getIncomingExpireTime())) ) { final GUID cbGuid = new GUID(GUID.makeGuid()); final MLImpl ml = new MLImpl(); messageRouter.get().registerMessageListener(cbGuid.bytes(), ml); // send a connectback request to a few peers and clear if(connectionManager.get().sendUDPConnectBackRequests(cbGuid)) { _lastConnectBackTime = System.currentTimeMillis(); Runnable checkThread = new Runnable() { public void run() { if ((_acceptedUnsolicitedIncoming && (_lastUnsolicitedIncomingTime < currTime)) || (!_acceptedUnsolicitedIncoming)) { if(ml._gotIncoming && !_acceptedUnsolicitedIncoming) LOG.info("Can receive unsolicited UDP"); // we set according to the message listener _acceptedUnsolicitedIncoming = ml._gotIncoming; // TODO set _lastReportedPort ?? // TODO set ConnectionSettings.STABLE_PORT ?? // TODO updateFWTState() ?? } messageRouter.get().unregisterMessageListener(cbGuid.bytes(), ml); } }; backgroundExecutor.schedule(checkThread, acceptor.get().getWaitTimeAfterRequests(), TimeUnit.MILLISECONDS); } else messageRouter.get().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.get().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 byte[] guid = getSolicitedGUID().bytes(); PingRequest pr = pingRequestFactory.createPingRequest(guid, (byte)1,(byte)0); pr.addIPRequest(); send(pr, ep.getInetAddress(), ep.getPort()); } } private class UDPConnectionListener implements EventListener<UDPSocketChannelConnectionEvent> { @Override public void handleEvent(UDPSocketChannelConnectionEvent event) { if(event.getType() == ConnectionState.CONNECTED && !_successfulFWT) { synchronized (this) { _successfulFWT = true; // TODO not sure this is correct - could also call networkManager.getPort() ? _lastReportedPort = event.getData().socket().getLocalPort(); ConnectionSettings.HAS_STABLE_PORT.setValue(true); } updateFWTState(); } } } }