package org.briarproject.plugins.tcp; import org.briarproject.BriarTestCase; import org.briarproject.api.contact.ContactId; import org.briarproject.api.keyagreement.KeyAgreementConnection; import org.briarproject.api.keyagreement.KeyAgreementListener; import org.briarproject.api.keyagreement.TransportDescriptor; import org.briarproject.api.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexTransportConnection; import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.settings.Settings; import org.junit.Test; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.ServerSocket; import java.net.Socket; import java.util.Collections; import java.util.Hashtable; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class LanTcpPluginTest extends BriarTestCase { private final ContactId contactId = new ContactId(234); private final Backoff backoff = new TestBackoff(); @Test public void testAddressesAreOnSameLan() { LanTcpPlugin plugin = new LanTcpPlugin(null, null, null, 0, 0); // Local and remote in 10.0.0.0/8 should return true assertTrue(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(10, 255, 255, 255))); // Local and remote in 172.16.0.0/12 should return true assertTrue(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(172, 31, 255, 255))); // Local and remote in 192.168.0.0/16 should return true assertTrue(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(192, 168, 255, 255))); // Local and remote in different recognised prefixes should return false assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(172, 31, 255, 255))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(192, 168, 255, 255))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(10, 255, 255, 255))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(192, 168, 255, 255))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(10, 255, 255, 255))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(172, 31, 255, 255))); // Remote prefix unrecognised should return false assertFalse(plugin.addressesAreOnSameLan(makeAddress(10, 0, 0, 0), makeAddress(1, 2, 3, 4))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(172, 16, 0, 0), makeAddress(1, 2, 3, 4))); assertFalse(plugin.addressesAreOnSameLan(makeAddress(192, 168, 0, 0), makeAddress(1, 2, 3, 4))); // Both prefixes unrecognised should return true (could be link-local) assertTrue(plugin.addressesAreOnSameLan(makeAddress(1, 2, 3, 4), makeAddress(5, 6, 7, 8))); } private byte[] makeAddress(int... parts) { byte[] b = new byte[parts.length]; for (int i = 0; i < parts.length; i++) b[i] = (byte) parts[i]; return b; } @Test public void testIncomingConnection() throws Exception { if (!systemHasLocalIpv4Address()) { System.err.println("WARNING: Skipping test, no local IPv4 address"); return; } Callback callback = new Callback(); Executor executor = Executors.newCachedThreadPool(); DuplexPlugin plugin = new LanTcpPlugin(executor, backoff, callback, 0, 0); plugin.start(); // The plugin should have bound a socket and stored the port number assertTrue(callback.propertiesLatch.await(5, SECONDS)); String ipPorts = callback.local.get("ipPorts"); assertNotNull(ipPorts); String[] split = ipPorts.split(","); assertEquals(1, split.length); split = split[0].split(":"); assertEquals(2, split.length); String addrString = split[0], portString = split[1]; InetAddress addr = InetAddress.getByName(addrString); assertTrue(addr instanceof Inet4Address); assertFalse(addr.isLoopbackAddress()); assertTrue(addr.isLinkLocalAddress() || addr.isSiteLocalAddress()); int port = Integer.parseInt(portString); assertTrue(port > 0 && port < 65536); // The plugin should be listening on the port InetSocketAddress socketAddr = new InetSocketAddress(addr, port); Socket s = new Socket(); s.connect(socketAddr, 100); assertTrue(callback.connectionsLatch.await(5, SECONDS)); s.close(); // Stop the plugin plugin.stop(); } @Test public void testOutgoingConnection() throws Exception { if (!systemHasLocalIpv4Address()) { System.err.println("WARNING: Skipping test, no local IPv4 address"); return; } Callback callback = new Callback(); Executor executor = Executors.newCachedThreadPool(); DuplexPlugin plugin = new LanTcpPlugin(executor, backoff, callback, 0, 0); plugin.start(); // The plugin should have bound a socket and stored the port number assertTrue(callback.propertiesLatch.await(5, SECONDS)); assertTrue(callback.propertiesLatch.await(5, SECONDS)); String ipPorts = callback.local.get("ipPorts"); assertNotNull(ipPorts); String[] split = ipPorts.split(","); assertEquals(1, split.length); split = split[0].split(":"); assertEquals(2, split.length); String addrString = split[0]; // Listen on the same interface as the plugin final ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress(addrString, 0), 10); int port = ss.getLocalPort(); final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean error = new AtomicBoolean(false); new Thread() { @Override public void run() { try { ss.accept(); latch.countDown(); } catch (IOException e) { error.set(true); } } }.start(); // Tell the plugin about the port TransportProperties p = new TransportProperties(); p.put("ipPorts", addrString + ":" + port); callback.remote.put(contactId, p); // Connect to the port DuplexTransportConnection d = plugin.createConnection(contactId); assertNotNull(d); // Check that the connection was accepted assertTrue(latch.await(5, SECONDS)); assertFalse(error.get()); // Clean up d.getReader().dispose(false, true); d.getWriter().dispose(false); ss.close(); plugin.stop(); } @Test public void testIncomingKeyAgreementConnection() throws Exception { if (!systemHasLocalIpv4Address()) { System.err.println("WARNING: Skipping test, no local IPv4 address"); return; } Callback callback = new Callback(); Executor executor = Executors.newCachedThreadPool(); DuplexPlugin plugin = new LanTcpPlugin(executor, backoff, callback, 0, 0); plugin.start(); assertTrue(callback.propertiesLatch.await(5, SECONDS)); KeyAgreementListener kal = plugin.createKeyAgreementListener(null); Callable c = kal.listen(); FutureTask f = new FutureTask<>(c); new Thread(f).start(); // The plugin should have bound a socket and stored the port number TransportDescriptor d = kal.getDescriptor(); TransportProperties p = d.getProperties(); String ipPort = p.get("ipPort"); assertNotNull(ipPort); String[] split = ipPort.split(":"); assertEquals(2, split.length); String addrString = split[0], portString = split[1]; InetAddress addr = InetAddress.getByName(addrString); assertTrue(addr instanceof Inet4Address); assertFalse(addr.isLoopbackAddress()); assertTrue(addr.isLinkLocalAddress() || addr.isSiteLocalAddress()); int port = Integer.parseInt(portString); assertTrue(port > 0 && port < 65536); // The plugin should be listening on the port InetSocketAddress socketAddr = new InetSocketAddress(addr, port); Socket s = new Socket(); s.connect(socketAddr, 100); assertNotNull(f.get(5, SECONDS)); s.close(); kal.close(); // Stop the plugin plugin.stop(); } @Test public void testOutgoingKeyAgreementConnection() throws Exception { if (!systemHasLocalIpv4Address()) { System.err.println("WARNING: Skipping test, no local IPv4 address"); return; } Callback callback = new Callback(); Executor executor = Executors.newCachedThreadPool(); DuplexPlugin plugin = new LanTcpPlugin(executor, backoff, callback, 0, 0); plugin.start(); // The plugin should have bound a socket and stored the port number assertTrue(callback.propertiesLatch.await(5, SECONDS)); String ipPorts = callback.local.get("ipPorts"); assertNotNull(ipPorts); String[] split = ipPorts.split(","); assertEquals(1, split.length); split = split[0].split(":"); assertEquals(2, split.length); String addrString = split[0]; // Listen on the same interface as the plugin final ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress(addrString, 0), 10); int port = ss.getLocalPort(); final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean error = new AtomicBoolean(false); new Thread() { @Override public void run() { try { ss.accept(); latch.countDown(); } catch (IOException e) { error.set(true); } } }.start(); // Tell the plugin about the port TransportProperties p = new TransportProperties(); p.put("ipPort", addrString + ":" + port); TransportDescriptor desc = new TransportDescriptor(plugin.getId(), p); // Connect to the port DuplexTransportConnection d = plugin.createKeyAgreementConnection(null, desc, 5000); assertNotNull(d); // Check that the connection was accepted assertTrue(latch.await(5, SECONDS)); assertFalse(error.get()); // Clean up d.getReader().dispose(false, true); d.getWriter().dispose(false); ss.close(); plugin.stop(); } private boolean systemHasLocalIpv4Address() throws Exception { for (NetworkInterface i : Collections.list( NetworkInterface.getNetworkInterfaces())) { for (InetAddress a : Collections.list(i.getInetAddresses())) { if (a instanceof Inet4Address) return a.isLinkLocalAddress() || a.isSiteLocalAddress(); } } return false; } private static class Callback implements DuplexPluginCallback { private final Map remote = new Hashtable<>(); private final CountDownLatch propertiesLatch = new CountDownLatch(1); private final CountDownLatch connectionsLatch = new CountDownLatch(1); private final TransportProperties local = new TransportProperties(); public Settings getSettings() { return new Settings(); } public TransportProperties getLocalProperties() { return local; } public Map getRemoteProperties() { return remote; } public void mergeSettings(Settings s) { } public void mergeLocalProperties(TransportProperties p) { local.putAll(p); propertiesLatch.countDown(); } public int showChoice(String[] options, String... message) { return -1; } public boolean showConfirmationMessage(String... message) { return false; } public void showMessage(String... message) { } public void incomingConnectionCreated(DuplexTransportConnection d) { connectionsLatch.countDown(); } public void outgoingConnectionCreated(ContactId c, DuplexTransportConnection d) { } public void transportEnabled() { } public void transportDisabled() { } } private static class TestBackoff implements Backoff { public int getPollingInterval() { return 60 * 1000; } public void increment() { } public void reset() { } } }