Merge branch 'v3-hidden-service-migration' into 'master'

Use v3 hidden services (alongside v2 if a service already exists)

See merge request briar/briar!1263
This commit is contained in:
Torsten Grote
2020-07-20 16:55:08 +00:00
4 changed files with 132 additions and 27 deletions

View File

@@ -1,5 +1,7 @@
package org.briarproject.bramble.api.plugin;
import static java.util.concurrent.TimeUnit.DAYS;
public interface TorConstants {
TransportId ID = new TransportId("org.briarproject.bramble.tor");
@@ -19,6 +21,14 @@ public interface TorConstants {
String PREF_TOR_PORT = "port";
String PREF_TOR_MOBILE = "useMobileData";
String PREF_TOR_ONLY_WHEN_CHARGING = "onlyWhenCharging";
String HS_PRIVATE_KEY_V2 = "onionPrivKey";
String HS_PRIVATE_KEY_V3 = "onionPrivKey3";
String HS_V3_CREATED = "onionPrivKey3Created";
/**
* How long to publish a v3 hidden service before retiring the v2 service.
*/
long V3_MIGRATION_PERIOD_MS = DAYS.toMillis(180);
// Values for PREF_TOR_NETWORK
int PREF_TOR_NETWORK_AUTOMATIC = 0;

View File

@@ -79,6 +79,9 @@ import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_PLUG
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.HS_PRIVATE_KEY_V2;
import static org.briarproject.bramble.api.plugin.TorConstants.HS_PRIVATE_KEY_V3;
import static org.briarproject.bramble.api.plugin.TorConstants.HS_V3_CREATED;
import static org.briarproject.bramble.api.plugin.TorConstants.ID;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
@@ -92,6 +95,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
import static org.briarproject.bramble.api.plugin.TorConstants.V3_MIGRATION_PERIOD_MS;
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -438,15 +442,66 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
private void publishHiddenService(String port) {
if (!state.isTorRunning()) return;
LOG.info("Creating hidden service");
String privKey = settings.get(HS_PRIVKEY);
// TODO: Remove support for v2 hidden services after a reasonable
// migration period (migration started 2020-06-30)
String privKey2 = settings.get(HS_PRIVATE_KEY_V2);
String privKey3 = settings.get(HS_PRIVATE_KEY_V3);
String v3Created = settings.get(HS_V3_CREATED);
// Publish a v2 hidden service if we've already created one, and
// either we've never published a v3 hidden service or we're still
// in the migration period since first publishing it
if (!isNullOrEmpty(privKey2)) {
long now = clock.currentTimeMillis();
long then = v3Created == null ? now : Long.parseLong(v3Created);
if (now - then >= V3_MIGRATION_PERIOD_MS) retireV2HiddenService();
else publishV2HiddenService(port, privKey2);
}
publishV3HiddenService(port, privKey3);
}
private void publishV2HiddenService(String port, String privKey) {
LOG.info("Creating v2 hidden service");
Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port);
Map<String, String> response;
try {
response = controlConnection.addOnion(privKey, portLines);
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (!response.containsKey(HS_ADDRESS)) {
LOG.warning("Tor did not return a hidden service address");
return;
}
String onion2 = response.get(HS_ADDRESS);
if (LOG.isLoggable(INFO)) {
LOG.info("V2 hidden service " + scrubOnion(onion2));
}
// The hostname has already been published and the private key stored
}
private void retireV2HiddenService() {
LOG.info("Retiring v2 hidden service");
TransportProperties p = new TransportProperties();
p.put(PROP_ONION_V2, "");
callback.mergeLocalProperties(p);
Settings s = new Settings();
s.put(HS_PRIVATE_KEY_V2, "");
callback.mergeSettings(s);
}
private void publishV3HiddenService(String port, @Nullable String privKey) {
LOG.info("Creating v3 hidden service");
Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port);
Map<String, String> response;
try {
// Use the control connection to set up the hidden service
if (privKey == null)
response = controlConnection.addOnion(portLines);
else response = controlConnection.addOnion(privKey, portLines);
if (privKey == null) {
response = controlConnection.addOnion("NEW:ED25519-V3",
portLines, null);
} else {
response = controlConnection.addOnion(privKey, portLines);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
@@ -459,17 +514,19 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
LOG.warning("Tor did not return a private key");
return;
}
// Publish the hidden service's onion hostname in transport properties
String onion2 = response.get(HS_ADDRESS);
if (LOG.isLoggable(INFO))
LOG.info("Hidden service " + scrubOnion(onion2));
TransportProperties p = new TransportProperties();
p.put(PROP_ONION_V2, onion2);
callback.mergeLocalProperties(p);
String onion3 = response.get(HS_ADDRESS);
if (LOG.isLoggable(INFO)) {
LOG.info("V3 hidden service " + scrubOnion(onion3));
}
if (privKey == null) {
// Publish the hidden service's onion hostname in transport props
TransportProperties p = new TransportProperties();
p.put(PROP_ONION_V3, onion3);
callback.mergeLocalProperties(p);
// Save the hidden service's private key for next time
Settings s = new Settings();
s.put(HS_PRIVKEY, response.get(HS_PRIVKEY));
s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY));
s.put(HS_V3_CREATED, String.valueOf(clock.currentTimeMillis()));
callback.mergeSettings(s);
}
}
@@ -575,12 +632,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
if (getState() != ACTIVE) return null;
String bestOnion = null;
// TODO: Remove support for v2 hidden services after a reasonable
// migration period (migration started 2020-06-30)
String bestOnion = null, version = null;
String onion2 = p.get(PROP_ONION_V2);
String onion3 = p.get(PROP_ONION_V3);
if (!isNullOrEmpty(onion2)) {
if (ONION_V2.matcher(onion2).matches()) {
bestOnion = onion2;
version = "v2";
} else {
// Don't scrub the address so we can find the problem
if (LOG.isLoggable(INFO))
@@ -590,6 +650,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!isNullOrEmpty(onion3)) {
if (ONION_V3.matcher(onion3).matches()) {
bestOnion = onion3;
version = "v3";
} else {
// Don't scrub the address so we can find the problem
if (LOG.isLoggable(INFO))
@@ -599,17 +660,21 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (bestOnion == null) return null;
Socket s = null;
try {
if (LOG.isLoggable(INFO))
LOG.info("Connecting to " + scrubOnion(bestOnion));
if (LOG.isLoggable(INFO)) {
LOG.info("Connecting to " + version + " "
+ scrubOnion(bestOnion));
}
s = torSocketFactory.createSocket(bestOnion + ".onion", 80);
s.setSoTimeout(socketTimeout);
if (LOG.isLoggable(INFO))
LOG.info("Connected to " + scrubOnion(bestOnion));
if (LOG.isLoggable(INFO)) {
LOG.info("Connected to " + version + " "
+ scrubOnion(bestOnion));
}
return new TorTransportConnection(this, s);
} catch (IOException e) {
if (LOG.isLoggable(INFO)) {
LOG.info("Could not connect to " + scrubOnion(bestOnion)
+ ": " + e.toString());
LOG.info("Could not connect to " + version + " "
+ scrubOnion(bestOnion) + ": " + e.toString());
}
tryToClose(s, LOG, WARNING);
return null;
@@ -731,8 +796,16 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
@Override
public void unrecognized(String type, String msg) {
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED"))
LOG.info("Descriptor uploaded");
if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
if (LOG.isLoggable(INFO)) {
String[] words = msg.split(" ");
if (words.length > 1 && ONION_V3.matcher(words[1]).matches()) {
LOG.info("V3 descriptor uploaded");
} else {
LOG.info("V2 descriptor uploaded");
}
}
}
}
@Override

View File

@@ -30,6 +30,7 @@ import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
@@ -41,6 +42,7 @@ import static org.briarproject.bramble.api.properties.TransportPropertyConstants
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_LOCAL;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_TRANSPORT_ID;
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MSG_KEY_VERSION;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@Immutable
@NotNullByDefault
@@ -272,14 +274,22 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
LatestUpdate latest = findLatest(txn, localGroup.getId(), t,
true);
if (latest == null) {
merged = p;
merged = new TransportProperties(p);
Iterator<String> it = merged.values().iterator();
while (it.hasNext()) {
if (isNullOrEmpty(it.next())) it.remove();
}
changed = true;
} else {
BdfList message = clientHelper.getMessageAsList(txn,
latest.messageId);
TransportProperties old = parseProperties(message);
merged = new TransportProperties(old);
merged.putAll(p);
for (Entry<String, String> e : p.entrySet()) {
String key = e.getKey(), value = e.getValue();
if (isNullOrEmpty(value)) merged.remove(key);
else merged.put(key, value);
}
changed = !merged.equals(old);
}
if (changed) {

View File

@@ -566,6 +566,10 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
Contact contact = getContact();
Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION);
// Property with an empty value should be discarded
TransportProperties properties = new TransportProperties(fooProperties);
properties.put("fooKey3", "");
context.checking(new DbExpectations() {{
oneOf(db).transaction(with(false), withDbRunnable(txn));
// There are no existing properties to merge with
@@ -589,7 +593,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
}});
TransportPropertyManagerImpl t = createInstance();
t.mergeLocalProperties(new TransportId("foo"), fooProperties);
t.mergeLocalProperties(new TransportId("foo"), properties);
}
@Test
@@ -605,16 +609,24 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
MessageId localGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> localGroupMessageMetadata =
singletonMap(localGroupUpdateId, oldMetadata);
MessageId contactGroupUpdateId = new MessageId(getRandomId());
Map<MessageId, BdfDictionary> contactGroupMessageMetadata =
singletonMap(contactGroupUpdateId, oldMetadata);
TransportProperties oldProperties = new TransportProperties();
oldProperties.put("fooKey1", "oldFooValue1");
oldProperties.put("fooKey3", "oldFooValue3");
BdfDictionary oldPropertiesDict = BdfDictionary.of(
new BdfEntry("fooKey1", "oldFooValue1")
new BdfEntry("fooKey1", "oldFooValue1"),
new BdfEntry("fooKey3", "oldFooValue3")
);
BdfList oldUpdate = BdfList.of("foo", 1, oldPropertiesDict);
// Property assigned an empty value should be removed
TransportProperties properties = new TransportProperties(fooProperties);
properties.put("fooKey3", "");
context.checking(new DbExpectations() {{
oneOf(db).transaction(with(false), withDbRunnable(txn));
// Merge the new properties with the existing properties
@@ -647,7 +659,7 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
}});
TransportPropertyManagerImpl t = createInstance();
t.mergeLocalProperties(new TransportId("foo"), fooProperties);
t.mergeLocalProperties(new TransportId("foo"), properties);
}
private void expectGetLocalProperties(Transaction txn) throws Exception {