From 42a014f32aa76be7eb54cd3522214110cd3c4c38 Mon Sep 17 00:00:00 2001 From: akwizgran <michael@briarproject.org> Date: Tue, 28 Mar 2023 16:44:54 +0100 Subject: [PATCH] Add BridgeTest. --- onionwrapper-core/build.gradle | 12 ++ .../briarproject/onionwrapper/TestUtils.java | 70 +++++++ onionwrapper-java/build.gradle | 6 + .../briarproject/onionwrapper/BridgeTest.java | 186 ++++++++++++++++++ .../briarproject/onionwrapper/Multiset.java | 102 ++++++++++ 5 files changed, 376 insertions(+) create mode 100644 onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java create mode 100644 onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java create mode 100644 onionwrapper-java/src/test/java/org/briarproject/onionwrapper/Multiset.java diff --git a/onionwrapper-core/build.gradle b/onionwrapper-core/build.gradle index 0a83e78..a5fd862 100644 --- a/onionwrapper-core/build.gradle +++ b/onionwrapper-core/build.gradle @@ -15,3 +15,15 @@ dependencies { testImplementation "junit:junit:4.13.2" } + +// Make test classes available to other modules +configurations { + testOutput.extendsFrom(testCompile) +} +task jarTest(type: Jar, dependsOn: testClasses) { + from sourceSets.test.output, sourceSets.main.output + classifier = 'test' +} +artifacts { + testOutput jarTest +} diff --git a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java new file mode 100644 index 0000000..028916e --- /dev/null +++ b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java @@ -0,0 +1,70 @@ +package org.briarproject.onionwrapper; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; + +@ThreadSafe +@NotNullByDefault +public class TestUtils { + + private static final Logger LOG = getLogger(TestUtils.class.getName()); + + private static final AtomicInteger nextTestDir = new AtomicInteger(0); + + public static File getTestDirectory() { + return new File("test.tmp/" + nextTestDir.getAndIncrement()); + } + + public static void deleteTestDirectory(File testDir) { + deleteFileOrDir(testDir); + // Delete test.tmp if empty + testDir.getParentFile().delete(); + } + + public static void deleteFileOrDir(File f) { + if (f.isFile()) { + delete(f); + } else if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children == null) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Could not list files in " + f.getAbsolutePath()); + } + } else { + for (File child : children) deleteFileOrDir(child); + } + delete(f); + } + } + + public static void delete(File f) { + if (!f.delete() && LOG.isLoggable(WARNING)) { + LOG.warning("Could not delete " + f.getAbsolutePath()); + } + } + + public static boolean isLinux() { + String os = System.getProperty("os.name"); + return os != null && os.contains("Linux"); + } + + @Nullable + public static String getArchitectureForTorBinary() { + String arch = System.getProperty("os.arch"); + if (arch == null) return null; + //noinspection IfCanBeSwitch + if (arch.equals("amd64")) return "x86_64"; + else if (arch.equals("aarch64")) return "aarch64"; + else if (arch.equals("arm")) return "armhf"; + return null; + } +} diff --git a/onionwrapper-java/build.gradle b/onionwrapper-java/build.gradle index 9e35b43..f8e7667 100644 --- a/onionwrapper-java/build.gradle +++ b/onionwrapper-java/build.gradle @@ -12,4 +12,10 @@ dependencies { def jna_version = '4.5.2' implementation "net.java.dev.jna:jna:$jna_version" implementation "net.java.dev.jna:jna-platform:$jna_version" + + testImplementation project(path: ':onionwrapper-core', configuration: 'testOutput') + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.briarproject:tor-linux:0.4.7.13-2' + testImplementation 'org.briarproject:obfs4proxy-linux:0.0.14-tor2' + testImplementation 'org.briarproject:snowflake-linux:2.5.1' } diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java new file mode 100644 index 0000000..d49f4ff --- /dev/null +++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java @@ -0,0 +1,186 @@ +package org.briarproject.onionwrapper; + +import org.briarproject.onionwrapper.CircumventionProvider.BridgeType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.logging.Logger; + +import javax.annotation.concurrent.GuardedBy; + +import static java.util.Collections.singletonList; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.DEFAULT_OBFS4; +import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.MEEK; +import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4; +import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.SNOWFLAKE; +import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.VANILLA; +import static org.briarproject.onionwrapper.TestUtils.deleteTestDirectory; +import static org.briarproject.onionwrapper.TestUtils.getArchitectureForTorBinary; +import static org.briarproject.onionwrapper.TestUtils.getTestDirectory; +import static org.briarproject.onionwrapper.TestUtils.isLinux; +import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNotNull; +import static org.junit.Assume.assumeTrue; + +@RunWith(Parameterized.class) +public class BridgeTest extends BaseTest { + + private final static Logger LOG = getLogger(BridgeTest.class.getName()); + + private static final String[] SNOWFLAKE_COUNTRY_CODES = {"TM", "ZZ"}; + private static final int SOCKS_PORT = 59060; + private static final int CONTROL_PORT = 59061; + private final static long TIMEOUT = MINUTES.toMillis(2); + private final static long MEEK_TIMEOUT = MINUTES.toMillis(6); + private final static int UNREACHABLE_BRIDGES_ALLOWED = 6; + private final static int ATTEMPTS_PER_BRIDGE = 5; + + @Parameters + public static Iterable<Params> data() { + // Share stats among all the test instances + Stats stats = new Stats(); + CircumventionProvider provider = new CircumventionProviderImpl(); + List<Params> states = new ArrayList<>(); + for (int i = 0; i < ATTEMPTS_PER_BRIDGE; i++) { + for (String bridge : provider.getBridges(DEFAULT_OBFS4, "", true)) { + states.add(new Params(bridge, DEFAULT_OBFS4, stats, false)); + } + for (String bridge : provider.getBridges(NON_DEFAULT_OBFS4, "", true)) { + states.add(new Params(bridge, NON_DEFAULT_OBFS4, stats, false)); + } + for (String bridge : provider.getBridges(VANILLA, "", true)) { + states.add(new Params(bridge, VANILLA, stats, false)); + } + for (String bridge : provider.getBridges(MEEK, "", true)) { + states.add(new Params(bridge, MEEK, stats, true)); + } + for (String countryCode : SNOWFLAKE_COUNTRY_CODES) { + for (String bridge : provider.getBridges(SNOWFLAKE, countryCode, true)) { + states.add(new Params(bridge, SNOWFLAKE, stats, true)); + } + for (String bridge : provider.getBridges(SNOWFLAKE, countryCode, false)) { + states.add(new Params(bridge, SNOWFLAKE, stats, true)); + } + } + } + return states; + } + + private final ExecutorService executor = newCachedThreadPool(); + private final File torDir = getTestDirectory(); + private final Params params; + + public BridgeTest(Params params) { + this.params = params; + } + + @Before + public void setUp() { + assumeTrue(isLinux()); + assumeNotNull(getArchitectureForTorBinary()); + } + + @After + public void tearDown() { + deleteTestDirectory(torDir); + executor.shutdown(); + } + + @Test + public void testBridges() throws Exception { + if (params.stats.hasSucceeded(params.bridge)) { + LOG.info("Skipping previously successful bridge: " + params.bridge); + return; + } + + String architecture = requireNonNull(getArchitectureForTorBinary()); + TorWrapper tor = new UnixTorWrapper(executor, executor, architecture, torDir, + CONTROL_PORT, SOCKS_PORT); + + LOG.warning("Testing " + params.bridge); + try { + tor.start(); + tor.enableBridges(singletonList(params.bridge)); + tor.enableNetwork(true); + long start = System.currentTimeMillis(); + long timeout = params.bridgeType == MEEK ? MEEK_TIMEOUT : TIMEOUT; + while (System.currentTimeMillis() - start < timeout) { + if (tor.getTorState() == CONNECTED) break; + //noinspection BusyWait + Thread.sleep(500); + } + if (tor.getTorState() == CONNECTED) { + LOG.info("Connected to Tor: " + params.bridge); + params.stats.countSuccess(params.bridge); + } else { + LOG.warning("Could not connect to Tor within timeout: " + params.bridge); + params.stats.countFailure(params.bridge, params.essential); + } + } finally { + tor.stop(); + } + } + + private static class Params { + + private final String bridge; + private final BridgeType bridgeType; + private final Stats stats; + private final boolean essential; + + private Params(String bridge, BridgeType bridgeType, Stats stats, boolean essential) { + this.bridge = bridge; + this.bridgeType = bridgeType; + this.stats = stats; + this.essential = essential; + } + } + + private static class Stats { + + @GuardedBy("this") + private final Set<String> successes = new HashSet<>(); + @GuardedBy("this") + private final Multiset<String> failures = new Multiset<>(); + @GuardedBy("this") + private final Set<String> unreachable = new TreeSet<>(); + + private synchronized boolean hasSucceeded(String bridge) { + return successes.contains(bridge); + } + + private synchronized void countSuccess(String bridge) { + successes.add(bridge); + } + + private synchronized void countFailure(String bridge, boolean essential) { + if (failures.add(bridge) == ATTEMPTS_PER_BRIDGE) { + LOG.warning("Bridge is unreachable after " + + ATTEMPTS_PER_BRIDGE + " attempts: " + bridge); + unreachable.add(bridge); + if (unreachable.size() > UNREACHABLE_BRIDGES_ALLOWED) { + fail(unreachable.size() + " bridges are unreachable: " + unreachable); + } + if (essential) { + fail("essential bridge is unreachable"); + } + } + } + } +} diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/Multiset.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/Multiset.java new file mode 100644 index 0000000..3a05962 --- /dev/null +++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/Multiset.java @@ -0,0 +1,102 @@ +package org.briarproject.onionwrapper; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.annotation.concurrent.NotThreadSafe; + +@NotThreadSafe +@NotNullByDefault +public class Multiset<T> { + + private final Map<T, Integer> map = new HashMap<>(); + + private int total = 0; + + /** + * Returns how many items the multiset contains in total. + */ + public int getTotal() { + return total; + } + + /** + * Returns how many unique items the multiset contains. + */ + public int getUnique() { + return map.size(); + } + + /** + * Returns how many of the given item the multiset contains. + */ + public int getCount(T t) { + Integer count = map.get(t); + return count == null ? 0 : count; + } + + /** + * Adds the given item to the multiset and returns how many of the item + * the multiset now contains. + */ + public int add(T t) { + Integer count = map.get(t); + if (count == null) count = 0; + map.put(t, count + 1); + total++; + return count + 1; + } + + /** + * Removes the given item from the multiset and returns how many of the + * item the multiset now contains. + * + * @throws NoSuchElementException if the item is not in the multiset. + */ + public int remove(T t) { + Integer count = map.get(t); + if (count == null) throw new NoSuchElementException(); + if (count == 1) map.remove(t); + else map.put(t, count - 1); + total--; + return count - 1; + } + + /** + * Removes all occurrences of the given item from the multiset. + */ + public int removeAll(T t) { + Integer count = map.remove(t); + if (count == null) return 0; + total -= count; + return count; + } + + /** + * Returns true if the multiset contains any occurrences of the given item. + */ + public boolean contains(T t) { + return map.containsKey(t); + } + + /** + * Removes all items from the multiset. + */ + public void clear() { + map.clear(); + total = 0; + } + + /** + * Returns the set of unique items the multiset contains. The returned set + * is unmodifiable. + */ + public Set<T> keySet() { + return Collections.unmodifiableSet(map.keySet()); + } +} -- GitLab