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.net.SocketException; import java.util.Collections; import java.util.List; import junit.framework.Test; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.stubs.ActivityCallbackStub; import com.limegroup.gnutella.stubs.ConnectionManagerStub; import com.limegroup.gnutella.util.BaseTestCase; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.PrivilegedAccessor; /** * this class tests that the node properly detects if it is * capable of firewall to firewall transfers. */ public class FWTDetectionTest extends BaseTestCase { public FWTDetectionTest(String name) { super(name); } public static Test suite() { return buildTestSuite(FWTDetectionTest.class); } //make sure the RouterService class gets loaded before the UDPService class static RouterService router = new RouterService(new ActivityCallbackStub()); static int REMOTE_PORT1 = 10000; static int REMOTE_PORT2 = 10001; static UDPPonger ponger1 = new UDPPonger(REMOTE_PORT1); static UDPPonger ponger2 = new UDPPonger(REMOTE_PORT2); static File gnet; static CMStub cmStub; /** * 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. */ public static void globalSetUp() { ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); // the catcher in RouterService points elsewhere HostCatcher catcher = new HostCatcher(); cmStub = new CMStub(); try{ PrivilegedAccessor.setValue(RouterService.class,"manager",cmStub); PrivilegedAccessor.setValue(RouterService.class,"catcher",catcher); }catch(Exception bad) { ErrorService.error(bad); } router.start(); cmStub.setConnected(true); assertTrue(RouterService.isConnected()); cmStub.setConnected(false); assertFalse(RouterService.isConnected()); gnet = new File(CommonUtils.getUserSettingsDir(),"gnutella.net"); } public void setUp() { cmStub.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false); ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); try { UDPService service = UDPService.instance(); PrivilegedAccessor.setValue(service,"_lastReportedPort",new Integer(0)); PrivilegedAccessor.setValue(service,"_numReceivedIPPongs",new Integer(0)); PrivilegedAccessor.setValue(service,"_acceptedSolicitedIncoming",new Boolean(true)); PrivilegedAccessor.setValue(service,"_portStable",new Boolean(true)); PrivilegedAccessor.setValue(Acceptor.class,"_externalAddress",new byte[4]); }catch(Exception bad) { ErrorService.error(bad); } } public void tearDown() { ponger1.drain(); ponger2.drain(); } /** * tests the scenario where we are not connected */ public void testDisconnected() throws Exception { cmStub.setConnected(false); ConnectionSettings.LAST_FWT_STATE.setValue(false); assertTrue(UDPService.instance().canDoFWT()); ConnectionSettings.LAST_FWT_STATE.setValue(true); assertFalse(UDPService.instance().canDoFWT()); } /** * tets the scenario where we have and have not received a pong */ public void testNotReceivedPong() throws Exception { cmStub.setConnected(true); ConnectionSettings.LAST_FWT_STATE.setValue(false); assertTrue(UDPService.instance().canDoFWT()); ConnectionSettings.LAST_FWT_STATE.setValue(true); assertFalse(UDPService.instance().canDoFWT()); cmStub.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().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); cmStub.setConnected(true); ConnectionSettings.LAST_FWT_STATE.setValue(false); assertTrue(UDPService.instance().canDoFWT()); ConnectionSettings.LAST_FWT_STATE.setValue(true); assertFalse(UDPService.instance().canDoFWT()); //reply with a pong that does carry info RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); Endpoint myself = new Endpoint(RouterService.getExternalAddress(), RouterService.getPort()); ponger1.reply(myself); Thread.sleep(1000); cmStub.setConnected(true); ConnectionSettings.LAST_FWT_STATE.setValue(false); assertTrue(UDPService.instance().canDoFWT()); ConnectionSettings.LAST_FWT_STATE.setValue(true); assertTrue(UDPService.instance().canDoFWT()); } /** * tests the scenarios where we have received more than one pongs, * sometimes reporting different ports. */ public void testReceivedManyPongs() throws Exception { cmStub.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); UDPService.instance().setReceiveSolicited(true); assertEquals(RouterService.getPort(),UDPService.instance().getStableUDPPort()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //send one pong which does has a different port RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); ponger1.reply(new Endpoint(RouterService.getExternalAddress(),1000)); Thread.sleep(1000); cmStub.setConnected(true); assertFalse(UDPService.instance().canDoFWT()); // our external port should still point to our router service port assertEquals(RouterService.getPort(),UDPService.instance().getStableUDPPort()); // send a second pong.. now we should be able to do FWT ponger1.reply(new Endpoint(RouterService.getExternalAddress(),1000)); Thread.sleep(1000); cmStub.setConnected(true); assertTrue(UDPService.instance().canDoFWT()); // and our external port should become the new port assertEquals(1000,UDPService.instance().getStableUDPPort()); } /** * tests the scenario where we have a forced port different from our * external port */ public void testForcedPort() throws Exception { int localPort = RouterService.getAcceptor().getPort(false); ConnectionSettings.FORCED_PORT.setValue(1000); RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); ConnectionSettings.FORCE_IP_ADDRESS.setValue(true); ConnectionSettings.FORCED_IP_ADDRESS_STRING.setValue(InetAddress.getLocalHost().getHostAddress()); assertEquals(1000,RouterService.getPort()); assertNotEquals(1000,localPort); cmStub.setConnected(false); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(false); UDPService.instance().setReceiveSolicited(true); assertEquals(RouterService.getPort(),UDPService.instance().getStableUDPPort()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().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 Endpoint(RouterService.getExternalAddress(),localPort)); Thread.sleep(1000); // now we should be able to do FWT, and our port should be our local port cmStub.setConnected(true); assertTrue(UDPService.instance().canDoFWT()); assertEquals(localPort,UDPService.instance().getStableUDPPort()); // reset the value of num received pongs and send another pong, // carrying our forced address PrivilegedAccessor.setValue(UDPService.instance(),"_numReceivedIPPongs", new Integer(0)); ponger1.reply(new Endpoint(RouterService.getExternalAddress(),1000)); assertTrue(UDPService.instance().canDoFWT()); assertEquals(1000,UDPService.instance().getStableUDPPort()); //clean up ConnectionSettings.FORCE_IP_ADDRESS.setValue(false); } /** * tests scenarios where we can and can't do solicited */ public void testSolicited() throws Exception{ cmStub.setConnected(true); //make sure our port is valid ConnectionSettings.LOCAL_IS_PRIVATE.setValue(true); RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); assertTrue( NetworkUtils.isValidAddress(RouterService.getExternalAddress())); //if we can't do solicited, we're out UDPService.instance().setReceiveSolicited(false); assertFalse(UDPService.instance().canReceiveSolicited()); assertFalse(UDPService.instance().canDoFWT()); UDPService.instance().setReceiveSolicited(true); assertTrue(UDPService.instance().canReceiveSolicited()); assertTrue(UDPService.instance().canDoFWT()); } public void testInvalidExternal() throws Exception { cmStub.setConnected(true); // make sure we can receive solicited UDPService.instance().setReceiveSolicited(true); assertTrue(UDPService.instance().canReceiveSolicited()); // send a pong writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().sendUDPPings(); //we should receive a udp ping requesting ip assertTrue(ponger1.listen().requestsIP()); //send one pong which does has a different port RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); ponger1.reply(new Endpoint(RouterService.getExternalAddress(),RouterService.getPort())); Thread.sleep(1000); // try with valid external address RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); assertTrue( NetworkUtils.isValidAddress(RouterService.getExternalAddress())); assertTrue(UDPService.instance().canDoFWT()); // and with an invalid one RouterService.getAcceptor().setExternalAddress(InetAddress.getByName("0.0.0.0")); assertFalse( NetworkUtils.isValidAddress(RouterService.getExternalAddress())); assertFalse(UDPService.instance().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"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().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); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().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"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); Endpoint myself = new Endpoint(RouterService.getExternalAddress(), RouterService.getPort()); ponger1.reply(myself); ponger2.reply(myself); Thread.sleep(500); cmStub.setConnected(true); assertTrue(UDPService.instance().canDoFWT()); assertFalse(ConnectionSettings.LAST_FWT_STATE.getValue()); cmStub.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"); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().sendUDPPings(); assertTrue(ponger1.listen().requestsIP()); assertTrue(ponger2.listen().requestsIP()); RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); Endpoint badPort1 = new Endpoint(InetAddress.getLocalHost().getAddress(),12345); Endpoint badPort2 = new Endpoint(InetAddress.getLocalHost().getAddress(),12346); ponger1.reply(badPort1); ponger2.reply(badPort2); Thread.sleep(500); cmStub.setConnected(true); assertFalse(UDPService.instance().canDoFWT()); assertTrue(ConnectionSettings.LAST_FWT_STATE.getValue()); cmStub.setConnected(false); } /** * tests the scenario where a (malicious) pong does not affect our * status */ public void testMaliciousPongDoesNotDisable() throws Exception { RouterService.getAcceptor().setExternalAddress(InetAddress.getLocalHost()); cmStub.setConnected(true); assertTrue(UDPService.instance().canDoFWT()); writeToGnet("127.0.0.1:"+REMOTE_PORT1+"\n"+"127.0.0.1:"+REMOTE_PORT2+"\n"); cmStub.setConnected(false); RouterService.getHostCatcher().expire(); RouterService.getHostCatcher().sendUDPPings(); ponger1.listen(); Endpoint badAddress = new Endpoint("1.2.3.4",RouterService.getPort()); PingReply badGuid = PingReply.create(GUID.makeGuid(),(byte)1,badAddress); ponger1.replyPong(badGuid); Thread.sleep(1000); cmStub.setConnected(true); assertTrue(UDPService.instance().canDoFWT()); } public void testServerResponse() throws Exception { cmStub.setConnected(true); DatagramSocket sock = new DatagramSocket(20000); sock.setSoTimeout(500); PingRequest with = new PingRequest((byte)1); PingRequest without = new PingRequest((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.getLocalHost(),RouterService.getPort())); sock.send(pack); Thread.sleep(100); DatagramPacket read = new DatagramPacket(new byte[100],100); sock.receive(read); ByteArrayInputStream bais = new ByteArrayInputStream(read.getData()); PingReply replyWith = (PingReply)Message.read(bais); assertNotNull(replyWith.getMyInetAddress()); assertEquals(InetAddress.getLocalHost(),replyWith.getMyInetAddress()); 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.getLocalHost(),RouterService.getPort())); sock.send(pack); Thread.sleep(100); read = new DatagramPacket(new byte[100],100); sock.receive(read); bais = new ByteArrayInputStream(read.getData()); PingReply replyWithout = (PingReply)Message.read(bais); assertNull(replyWithout.getMyInetAddress()); assertEquals(0,replyWithout.getMyPort()); } private static void writeToGnet(String hosts) throws Exception { FileOutputStream fos = new FileOutputStream(gnet); fos.write(hosts.getBytes());fos.flush();fos.close(); } private static class UDPPonger { private DatagramSocket _sock; private SocketAddress _lastAddress; public PingReply reply; public boolean shouldAsk; private GUID _solGUID; public UDPPonger(int port) { try { _solGUID=(GUID) PrivilegedAccessor.getValue( UDPService.instance(),"SOLICITED_PING_GUID"); _sock = new DatagramSocket(port); _sock.setSoTimeout(5000); }catch(Exception bad) { ErrorService.error(bad); } } 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()); return (PingRequest) Message.read(bais); } /** * 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 = PingReply.create(_solGUID.bytes(),(byte)1); else toSend = PingReply.create(_solGUID.bytes(),(byte)1,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 drain() { try{ _sock.setSoTimeout(100); try{ while(true) _sock.receive(new DatagramPacket(new byte[1000],1000)); }catch(IOException expected) {} _sock.setSoTimeout(5000); }catch(SocketException bad) { ErrorService.error(bad); } } } private static class CMStub extends ConnectionManagerStub { private boolean connected; public boolean isConnected() { return connected; } public void setConnected(boolean yes) { connected=yes; } public boolean isFullyConnected() { return false; } public int getPreferredConnectionCount() { return 1; } public List getInitializedConnections() { return Collections.EMPTY_LIST; } } }