package com.limegroup.gnutella; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collections; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import junit.framework.Test; import org.limewire.core.settings.ConnectionSettings; import org.limewire.io.GUID; import org.limewire.io.IpPort; import org.limewire.io.IpPortImpl; import org.limewire.io.NetworkInstanceUtils; import org.limewire.io.NetworkUtils; import org.limewire.net.ConnectionDispatcher; import org.limewire.net.SocketsManager; import org.limewire.util.CommonUtils; import org.limewire.util.PrivilegedAccessor; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.Stage; import com.google.inject.name.Named; import com.limegroup.gnutella.connection.ConnectionCheckerManager; import com.limegroup.gnutella.connection.RoutedConnection; import com.limegroup.gnutella.connection.RoutedConnectionFactory; import com.limegroup.gnutella.filters.IPFilter; import com.limegroup.gnutella.messages.MessageFactory; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingReplyFactory; 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.CapabilitiesVMFactory; import com.limegroup.gnutella.simpp.SimppManager; import com.limegroup.gnutella.stubs.ConnectionManagerStub; import com.limegroup.gnutella.stubs.NetworkManagerStub; import com.limegroup.gnutella.util.LimeTestCase; /** * this class tests that the node properly detects if it is * capable of firewall to firewall transfers. */ public class FWTDetectionTest extends LimeTestCase { public FWTDetectionTest(String name) { super(name); } public static Test suite() { return buildTestSuite(FWTDetectionTest.class); } private int REMOTE_PORT1 = 10000; private int REMOTE_PORT2 = 10001; private UDPPonger ponger1; private UDPPonger ponger2; private File gnutellaDotNetFile; private CMStub connectionManager; private Injector injector; private UDPService udpService; private NetworkManagerStub networkManager; /** * the basic testing routine is a node with a few hosts in its gnutella.net * the node sends an initial ping to them, and they return various * pongs. */ @Override public void setUp() throws Exception { ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); injector = LimeTestUtils.createInjector(Stage.PRODUCTION, new AbstractModule() { @Override protected void configure() { bind(ConnectionManager.class).to(CMStub.class); bind(NetworkManager.class).to(NetworkManagerStub.class); } }); connectionManager = (CMStub)injector.getInstance(ConnectionManager.class); gnutellaDotNetFile = new File(CommonUtils.getUserSettingsDir(),"gnutella.net"); connectionManager.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false); ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); udpService = injector.getInstance(UDPService.class); PrivilegedAccessor.setValue(udpService,"_lastReportedPort", 0); PrivilegedAccessor.setValue(udpService,"_numReceivedIPPongs", 0); PrivilegedAccessor.setValue(udpService,"_acceptedSolicitedIncoming", true); ConnectionSettings.HAS_STABLE_PORT.setValue(true); networkManager = (NetworkManagerStub)injector.getInstance(NetworkManager.class); networkManager.setPort(7788); networkManager.setAddress(InetAddress.getLocalHost().getAddress()); networkManager.setExternalAddress(InetAddress.getLocalHost().getAddress()); networkManager.setSolicitedGUID(udpService.getSolicitedGUID()); MessageFactory messageFactory = injector.getInstance(MessageFactory.class); PingReplyFactory pingReplyFactory = injector.getInstance(PingReplyFactory.class); ponger1 = new UDPPonger(REMOTE_PORT1, messageFactory, pingReplyFactory, new byte []{1,2,3,4}); ponger2 = new UDPPonger(REMOTE_PORT2, messageFactory, pingReplyFactory, new byte []{2,3,4,5}); injector.getInstance(LifecycleManager.class).start(); } @Override public void tearDown() throws Exception { if(ponger1 != null) ponger1.drainAndClose(); if(ponger2 != null) ponger2.drainAndClose(); } /** * tests the scenario where we are not connected */ public void testDisconnected() throws Exception { connectionManager.setConnected(false); ConnectionSettings.CANNOT_DO_FWT.setValue(false); assertTrue(udpService.canDoFWT()); ConnectionSettings.CANNOT_DO_FWT.setValue(true); assertFalse(udpService.canDoFWT()); } /** * tets the scenario where we have and have not received a pong */ public void testNotReceivedPong() throws Exception { connectionManager.setConnected(true); ConnectionSettings.CANNOT_DO_FWT.setValue(false); assertTrue(udpService.canDoFWT()); ConnectionSettings.CANNOT_DO_FWT.setValue(true); assertFalse(udpService.canDoFWT()); connectionManager.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //reply with a pong that does not carry info ponger1.reply(null); connectionManager.setConnected(true); ConnectionSettings.CANNOT_DO_FWT.setValue(false); assertTrue(udpService.canDoFWT()); ConnectionSettings.CANNOT_DO_FWT.setValue(true); assertFalse(udpService.canDoFWT()); //reply with a pong that does carry info IpPort myself = new IpPortImpl(networkManager.getExternalAddress(), networkManager.getPort()); ponger1.reply(myself); Thread.sleep(1000); connectionManager.setConnected(true); ConnectionSettings.CANNOT_DO_FWT.setValue(false); assertTrue(udpService.canDoFWT()); ConnectionSettings.CANNOT_DO_FWT.setValue(true); assertTrue(udpService.canDoFWT()); } /** * tests the scenarios where we have received more than one pongs, * sometimes reporting different ports. */ public void testReceivedManyPongs() throws Exception { connectionManager.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); udpService.setReceiveSolicited(true); Acceptor acceptor = injector.getInstance(Acceptor.class); // make sure local != forced port, so we know udpService is used forced assertNotEquals(networkManager.getPort(), acceptor.getPort(false)); assertEquals(networkManager.getPort(),udpService.getStableUDPPort()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //send one pong which does has a different port ponger1.reply(new IpPortImpl(networkManager.getExternalAddress(),1000)); Thread.sleep(1000); connectionManager.setConnected(true); assertFalse(udpService.canDoFWT()); // our external port should still point to our router service port assertEquals(networkManager.getPort(), udpService.getStableUDPPort()); // send a second pong.. now we should be able to do FWT ponger1.reply(new IpPortImpl(networkManager.getExternalAddress(),1000)); Thread.sleep(1000); connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); // and our external port should become the new port assertEquals(1000,udpService.getStableUDPPort()); } /** * tests the scenario where we have a forced port different from our * external port */ public void testForcedPort() throws Exception { Acceptor acceptor = injector.getInstance(Acceptor.class); int localPort = acceptor.getPort(false); networkManager.setPort(1000); assertNotEquals(1000,localPort); connectionManager.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); udpService.setReceiveSolicited(true); assertEquals(networkManager.getPort(),udpService.getStableUDPPort()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //send one pong which carries our local ip address ponger1.reply(new IpPortImpl(networkManager.getExternalAddress(),localPort)); Thread.sleep(1000); // now we should be able to do FWT, and our port should be our local port connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); assertEquals(localPort,udpService.getStableUDPPort()); // reset the value of num received pongs and send another pong, // carrying our forced address PrivilegedAccessor.setValue(udpService,"_numReceivedIPPongs", 0); ponger1.reply(new IpPortImpl(networkManager.getExternalAddress(),1000)); assertTrue(udpService.canDoFWT()); assertEquals(1000,udpService.getStableUDPPort()); //clean up ConnectionSettings.FORCE_IP_ADDRESS.setValue(false); } /** * tests scenarios where we can and can't do solicited */ public void testSolicited() throws Exception{ //if we can't do solicited, we're out udpService.setReceiveSolicited(false); assertFalse(udpService.canReceiveSolicited()); assertFalse(udpService.canDoFWT()); udpService.setReceiveSolicited(true); assertTrue(udpService.canReceiveSolicited()); assertTrue(udpService.canDoFWT()); } public void testInvalidExternal() throws Exception { connectionManager.setConnected(true); udpService.setReceiveSolicited(true); // send a pong writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //send one pong which does has a different port ponger1.reply(new IpPortImpl(networkManager.getExternalAddress(), networkManager.getPort())); Thread.sleep(1000); // try with valid external address assertTrue(udpService.canDoFWT()); // and with an invalid one networkManager.setExternalAddress(InetAddress.getByName("0.0.0.0").getAddress()); assertFalse(NetworkUtils.isValidAddress(networkManager.getExternalAddress())); assertFalse(udpService.canDoFWT()); } /** * tests if the pings are requesting ip:port check properly */ public void testPingsRequesting() throws Exception { // make sure we have not received incoming connection in the past ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); // if we have received incoming, pings should not be requesting ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(true); hostCatcher.expire(); hostCatcher.sendUDPPings(); assertFalse(ponger1.listen().requestsIP()); } /** * tests the case where both pinged hosts reply with the same * ip:port. */ public void testPongsCarryGoodInfo() throws Exception { ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"+"127.0.0.1:"+REMOTE_PORT2+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); IpPort myself = new IpPortImpl(networkManager.getExternalAddress(), networkManager.getPort()); ponger1.reply(myself); ponger2.reply(myself); Thread.sleep(500); connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); assertFalse(ConnectionSettings.CANNOT_DO_FWT.getValue()); connectionManager.setConnected(false); } /** * tests the case where a pong says we have a different port */ public void testPongCarriesBadPort() throws Exception{ ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"+"127.0.0.1:"+REMOTE_PORT2+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); IpPort badPort1 = new IpPortImpl(networkManager.getExternalAddress(), 12345); IpPort badPort2 = new IpPortImpl(networkManager.getExternalAddress(),12346); ponger1.reply(badPort1); ponger2.reply(badPort2); Thread.sleep(500); connectionManager.setConnected(true); assertFalse(udpService.canDoFWT()); assertTrue(ConnectionSettings.CANNOT_DO_FWT.getValue()); connectionManager.setConnected(false); } /** * tests the scenario where a (malicious) pong does not affect our * status */ public void testMaliciousPongDoesNotDisable() throws Exception { connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"+"127.0.0.1:"+REMOTE_PORT2+"\n"); connectionManager.setConnected(false); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); ponger1.listen(); IpPort badAddress = new IpPortImpl("1.2.3.4", networkManager.getPort()); PingReply badGuid = injector.getInstance(PingReplyFactory.class).create(GUID.makeGuid(),(byte)1,badAddress); ponger1.replyPong(badGuid); Thread.sleep(1000); connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); } public void testGoodPongsReenableFWTAfterBadOnesDisable() throws Exception { ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"+"127.0.0.1:"+REMOTE_PORT2+"\n"); HostCatcher hostCatcher = injector.getInstance(HostCatcher.class); hostCatcher.expire(); hostCatcher.sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); IpPort badPort1 = new IpPortImpl(networkManager.getExternalAddress(), 12345); IpPort badPort2 = new IpPortImpl(networkManager.getExternalAddress(),12346); ponger1.reply(badPort1); ponger2.reply(badPort2); Thread.sleep(500); connectionManager.setConnected(true); assertFalse(udpService.canDoFWT()); assertTrue(ConnectionSettings.CANNOT_DO_FWT.getValue()); connectionManager.setConnected(false); // now send good ports hostCatcher.expire(); hostCatcher.sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); IpPort goodPort1 = new IpPortImpl(networkManager.getExternalAddress(), networkManager.getPort()); IpPort goodPort2 = new IpPortImpl(networkManager.getExternalAddress(), networkManager.getPort()); ponger1.reply(goodPort1); ponger2.reply(goodPort2); Thread.sleep(500); connectionManager.setConnected(true); assertTrue(udpService.canDoFWT()); assertFalse(ConnectionSettings.CANNOT_DO_FWT.getValue()); connectionManager.setConnected(false); } public void testServerResponse() throws Exception { connectionManager.setConnected(true); DatagramSocket sock = new DatagramSocket(20000); sock.setSoTimeout(500); PingRequestFactory pingRequestFactory = injector.getInstance(PingRequestFactory.class); PingRequest with = pingRequestFactory.createPingRequest((byte)1); PingRequest without = pingRequestFactory.createPingRequest((byte)1); with.addIPRequest(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); with.write(baos); byte[] data = baos.toByteArray(); DatagramPacket pack = new DatagramPacket(data, data.length, new InetSocketAddress( InetAddress.getByAddress(networkManager.getExternalAddress()), udpService.getListeningPort())); sock.send(pack); Thread.sleep(100); DatagramPacket read = new DatagramPacket(new byte[100],100); sock.receive(read); ByteArrayInputStream bais = new ByteArrayInputStream(read.getData()); MessageFactory messageFactory = injector.getInstance(MessageFactory.class); PingReply replyWith = (PingReply)messageFactory.read(bais, Network.TCP); assertNotNull(replyWith.getMyInetAddress()); assertEquals(networkManager.getExternalAddress(), replyWith.getMyInetAddress().getAddress()); assertEquals(20000,replyWith.getMyPort()); // test a ping without baos = new ByteArrayOutputStream(); without.write(baos); data = baos.toByteArray(); pack = new DatagramPacket(data, data.length, new InetSocketAddress( InetAddress.getByAddress(networkManager.getExternalAddress()), udpService.getListeningPort())); sock.send(pack); Thread.sleep(100); read = new DatagramPacket(new byte[100],100); sock.receive(read); bais = new ByteArrayInputStream(read.getData()); PingReply replyWithout = (PingReply)messageFactory.read(bais, Network.TCP); assertNull(replyWithout.getMyInetAddress()); assertEquals(0,replyWithout.getMyPort()); } private void writeToGnet(String hosts) throws Exception { FileOutputStream fos = new FileOutputStream(gnutellaDotNetFile); fos.write(hosts.getBytes());fos.flush();fos.close(); } private class UDPPonger { private MessageFactory messageFactory; private PingReplyFactory pingReplyFactory; private final DatagramSocket _sock; private SocketAddress _lastAddress; public PingReply reply; public boolean shouldAsk; private byte [] lastReceived; private byte [] respondersIP; public UDPPonger(int port, MessageFactory messageFactory, PingReplyFactory pingReplyFactory, byte [] respondersIP) throws IOException { _sock = new DatagramSocket(port); _sock.setSoTimeout(5000); this.pingReplyFactory = pingReplyFactory; this.messageFactory = messageFactory; this.respondersIP = respondersIP; } public PingRequest listen() throws Exception { byte [] data = new byte[1024]; //receive a ping. DatagramPacket pack = new DatagramPacket(data,1024); _sock.receive(pack); _lastAddress = pack.getSocketAddress(); ByteArrayInputStream bais = new ByteArrayInputStream(pack.getData()); PingRequest ret = (PingRequest)messageFactory.read(bais, Network.TCP); lastReceived = ret.getGUID(); return ret; } /** * send a pong with the specified address back to the pinger. * it uses the solicited ping */ public void reply(IpPort reply) throws Exception{ PingReply toSend; if (reply==null) toSend = pingReplyFactory.create(lastReceived,(byte)1, _sock.getLocalPort(), respondersIP); else toSend = pingReplyFactory.create(lastReceived,(byte)1, _sock.getLocalPort(), respondersIP, reply); replyPong(toSend); } public void replyPong(PingReply reply) throws Exception{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); reply.write(baos); byte []data = baos.toByteArray(); DatagramPacket pack = new DatagramPacket(data,data.length,_lastAddress); _sock.send(pack); } public void drainAndClose() throws IOException { _sock.setSoTimeout(100); try{ while(true) _sock.receive(new DatagramPacket(new byte[1000],1000)); } catch(IOException expected) {} _sock.close(); } } @Singleton private static class CMStub extends ConnectionManagerStub { @Inject public CMStub(NetworkManager networkManager, Provider<HostCatcher> hostCatcher, @Named("global") Provider<ConnectionDispatcher> connectionDispatcher, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, Provider<SimppManager> simppManager, CapabilitiesVMFactory capabilitiesVMFactory, RoutedConnectionFactory managedConnectionFactory, Provider<QueryUnicaster> queryUnicaster, SocketsManager socketsManager, ConnectionServices connectionServices, Provider<NodeAssigner> nodeAssigner, Provider<IPFilter> ipFilter, ConnectionCheckerManager connectionCheckerManager, PingRequestFactory pingRequestFactory, NetworkInstanceUtils networkInstanceUtils) { super(networkManager, hostCatcher, connectionDispatcher, backgroundExecutor, simppManager, capabilitiesVMFactory, managedConnectionFactory, queryUnicaster, socketsManager, connectionServices, nodeAssigner, ipFilter, connectionCheckerManager, pingRequestFactory, networkInstanceUtils); } private boolean connected; @Override public boolean isConnected() { return connected; } @Override public void setConnected(boolean yes) { connected=yes; } @Override public boolean isFullyConnected() { return false; } @Override public int getPreferredConnectionCount() { return 1; } @Override public List<RoutedConnection> getInitializedConnections() { return Collections.emptyList(); } } }