Skip to content
Snippets Groups Projects
Commit 42a014f3 authored by akwizgran's avatar akwizgran
Browse files

Add BridgeTest.

parent 36c4e893
No related branches found
No related tags found
No related merge requests found
......@@ -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
}
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;
}
}
......@@ -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'
}
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");
}
}
}
}
}
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());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment