mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-07 06:44:16 +00:00
Tor using the Orchid library
This commit is contained in:
parent
c5e82e6bc5
commit
99448b730a
@ -280,6 +280,11 @@
|
||||
<version>9.1-901.jdbc4</version>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>com.subgraph</groupId>
|
||||
<artifactId>orchid</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -23,6 +23,7 @@ import javax.net.SocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -65,14 +66,15 @@ public class BlockingClient implements MessageWriteTarget {
|
||||
// sure it doesnt get too large or have to call read too often.
|
||||
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
|
||||
parser.setWriteTarget(this);
|
||||
socket = socketFactory.createSocket();
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (clientSet != null)
|
||||
clientSet.add(BlockingClient.this);
|
||||
try {
|
||||
socket.connect(serverAddress, connectTimeoutMillis);
|
||||
InetSocketAddress iServerAddress = (InetSocketAddress)serverAddress;
|
||||
socket = socketFactory.createSocket(iServerAddress.getAddress(), iServerAddress.getPort());
|
||||
//socket.connect(serverAddress, connectTimeoutMillis);
|
||||
parser.connectionOpened();
|
||||
InputStream stream = socket.getInputStream();
|
||||
byte[] readBuff = new byte[dbuf.capacity()];
|
||||
|
@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Copyright 2014 Miron Cuperman
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.bitcoin.net.discovery;
|
||||
|
||||
import com.google.bitcoin.core.NetworkParameters;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorClient;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* <p>Supports peer discovery through Tor.</p>
|
||||
*
|
||||
* <p>Failure to obtain at least four different peers through different exit nodes will cause
|
||||
* a PeerDiscoveryException will be thrown during getPeers().
|
||||
* </p>
|
||||
*
|
||||
* <p>DNS seeds do not attempt to enumerate every peer on the network. If you want more peers
|
||||
* to connect to, you need to discover them via other means (like addr broadcasts).</p>
|
||||
*/
|
||||
public class TorDiscovery implements PeerDiscovery {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorDiscovery.class);
|
||||
public static final int MINIMUM_ROUTER_COUNT = 4;
|
||||
public static final int MINIMUM_ROUTER_LOOKUP_COUNT = 10;
|
||||
public static final int RECEIVE_RETRIES = 3;
|
||||
public static final int RESOLVE_STREAM_ID = 0x1000; // An arbitrary stream ID
|
||||
public static final int RESOLVE_CNAME = 0x00;
|
||||
public static final int RESOLVE_ERROR = 0xf0;
|
||||
public static final int RESOLVE_IPV4 = 0x04;
|
||||
public static final int RESOLVE_IPV6 = 0x06;
|
||||
|
||||
private final String[] hostNames;
|
||||
private final NetworkParameters netParams;
|
||||
private final CircuitPathChooser pathChooser;
|
||||
private final TorClient torClient;
|
||||
private ListeningExecutorService threadPool;
|
||||
|
||||
/**
|
||||
* Supports finding peers through Tor. Community run DNS entry points will be used.
|
||||
*
|
||||
* @param netParams Network parameters to be used for port information.
|
||||
*/
|
||||
public TorDiscovery(NetworkParameters netParams, TorClient torClient) {
|
||||
this(netParams.getDnsSeeds(), netParams, torClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports finding peers through Tor.
|
||||
*
|
||||
* @param hostNames Host names to be examined for seed addresses.
|
||||
* @param netParams Network parameters to be used for port information.
|
||||
* @param torClient an already-started Tor client.
|
||||
*/
|
||||
public TorDiscovery(String[] hostNames, NetworkParameters netParams, TorClient torClient) {
|
||||
this.hostNames = hostNames;
|
||||
this.netParams = netParams;
|
||||
|
||||
this.torClient = torClient;
|
||||
this.pathChooser = CircuitPathChooser.create(torClient.getConfig(), torClient.getDirectory());
|
||||
}
|
||||
|
||||
private static class Lookup {
|
||||
final Router router;
|
||||
final InetAddress address;
|
||||
|
||||
Lookup(Router router, InetAddress address) {
|
||||
this.router = router;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException {
|
||||
if (hostNames == null)
|
||||
throw new PeerDiscoveryException("Unable to find any peers via DNS");
|
||||
|
||||
Set<Router> routers = Sets.newHashSet();
|
||||
ArrayList<ExitTarget> dummyTargets = Lists.newArrayList();
|
||||
|
||||
// Collect exit nodes until we have enough
|
||||
while (routers.size() < MINIMUM_ROUTER_LOOKUP_COUNT) {
|
||||
Router router = pathChooser.chooseExitNodeForTargets(dummyTargets);
|
||||
routers.add(router);
|
||||
}
|
||||
|
||||
try {
|
||||
List<Circuit> circuits = getCircuits(timeoutValue, timeoutUnit, routers);
|
||||
|
||||
Collection<InetSocketAddress> addresses = lookupAddresses(timeoutValue, timeoutUnit, circuits);
|
||||
|
||||
if (addresses.size() < MINIMUM_ROUTER_COUNT)
|
||||
throw new PeerDiscoveryException("Unable to find enough peers via Tor - got " + addresses.size());
|
||||
ArrayList<InetSocketAddress> addressList = Lists.newArrayList();
|
||||
addressList.addAll(addresses);
|
||||
Collections.shuffle(addressList);
|
||||
return addressList.toArray(new InetSocketAddress[addressList.size()]);
|
||||
} catch (InterruptedException e) {
|
||||
throw new PeerDiscoveryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Circuit> getCircuits(long timeoutValue, TimeUnit timeoutUnit, Set<Router> routers) throws InterruptedException {
|
||||
createThreadPool(routers.size());
|
||||
|
||||
try {
|
||||
List<ListenableFuture<Circuit>> circuitFutures = Lists.newArrayList();
|
||||
for (final Router router : routers) {
|
||||
circuitFutures.add(threadPool.submit(new Callable<Circuit>() {
|
||||
public Circuit call() throws Exception {
|
||||
return torClient.getCircuitManager().openInternalCircuitTo(Lists.newArrayList(router));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
threadPool.awaitTermination(timeoutValue, timeoutUnit);
|
||||
for (ListenableFuture<Circuit> future : circuitFutures) {
|
||||
if (!future.isDone()) {
|
||||
log.warn("circuit timed out");
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
List<Circuit> circuits;
|
||||
try {
|
||||
circuits = Futures.successfulAsList(circuitFutures).get();
|
||||
// Any failures will result in null entries. Remove them.
|
||||
circuits.removeAll(Collections.singleton(null));
|
||||
return circuits;
|
||||
} catch (ExecutionException e) {
|
||||
// Cannot happen, successfulAsList accepts failures
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} finally {
|
||||
shutdownThreadPool();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<InetSocketAddress> lookupAddresses(long timeoutValue, TimeUnit timeoutUnit, List<Circuit> circuits) throws InterruptedException {
|
||||
createThreadPool(circuits.size() * hostNames.length);
|
||||
|
||||
try {
|
||||
List<ListenableFuture<Lookup>> lookupFutures = Lists.newArrayList();
|
||||
for (final Circuit circuit : circuits) {
|
||||
for (final String seed : hostNames) {
|
||||
lookupFutures.add(threadPool.submit(new Callable<Lookup>() {
|
||||
public Lookup call() throws Exception {
|
||||
return new Lookup(circuit.getFinalCircuitNode().getRouter(), lookup(circuit, seed));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
threadPool.awaitTermination(timeoutValue, timeoutUnit);
|
||||
for (ListenableFuture<Lookup> future : lookupFutures) {
|
||||
if (!future.isDone()) {
|
||||
log.warn("circuit timed out");
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
List<Lookup> lookups = Futures.successfulAsList(lookupFutures).get();
|
||||
// Any failures will result in null entries. Remove them.
|
||||
lookups.removeAll(Collections.singleton(null));
|
||||
|
||||
// Use a map to enforce one result per exit node
|
||||
// TODO: randomize result selection better
|
||||
Map<HexDigest, InetSocketAddress> lookupMap = Maps.newHashMap();
|
||||
|
||||
for (Lookup lookup : lookups) {
|
||||
InetSocketAddress address = new InetSocketAddress(lookup.address, netParams.getPort());
|
||||
lookupMap.put(lookup.router.getIdentityHash(), address);
|
||||
}
|
||||
|
||||
return lookupMap.values();
|
||||
} catch (ExecutionException e) {
|
||||
// Cannot happen, successfulAsList accepts failures
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} finally {
|
||||
shutdownThreadPool();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void shutdownThreadPool() {
|
||||
threadPool.shutdownNow();
|
||||
threadPool = null;
|
||||
}
|
||||
|
||||
private synchronized void createThreadPool(int size) {
|
||||
threadPool =
|
||||
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(size));
|
||||
}
|
||||
|
||||
private InetAddress lookup(Circuit circuit, String seed) throws UnknownHostException {
|
||||
// Send a resolve cell to the exit node
|
||||
RelayCell cell = circuit.createRelayCell(RelayCell.RELAY_RESOLVE, RESOLVE_STREAM_ID, circuit.getFinalCircuitNode());
|
||||
cell.putString(seed);
|
||||
circuit.sendRelayCell(cell);
|
||||
|
||||
// Wait a few cell timeout periods (3 * 20 sec) for replies, in case the path is slow
|
||||
for (int i = 0 ; i < RECEIVE_RETRIES; i++) {
|
||||
RelayCell res = circuit.receiveRelayCell();
|
||||
if (res != null) {
|
||||
while (res.cellBytesRemaining() > 0) {
|
||||
int type = res.getByte();
|
||||
int len = res.getByte();
|
||||
byte[] value = new byte[len];
|
||||
res.getByteArray(value);
|
||||
int ttl = res.getInt();
|
||||
|
||||
if (type == RESOLVE_CNAME || type >= RESOLVE_ERROR) {
|
||||
// TODO handle .onion CNAME replies
|
||||
throw new RuntimeException(new String(value));
|
||||
} else if (type == RESOLVE_IPV4 || type == RESOLVE_IPV6) {
|
||||
return InetAddress.getByAddress(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Could not look up " + seed);
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
if (threadPool != null) {
|
||||
shutdownThreadPool();
|
||||
}
|
||||
}
|
||||
}
|
5
orchid/.gitignore
vendored
Normal file
5
orchid/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
bin/
|
||||
orchid-*.jar
|
||||
orchid-*.zip
|
||||
build-revision
|
||||
lib/xmlrpc-*
|
25
orchid/LICENSE
Normal file
25
orchid/LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 2009-2011, Bruce Leidl
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
0
orchid/README
Normal file
0
orchid/README
Normal file
116
orchid/build.xml
Normal file
116
orchid/build.xml
Normal file
@ -0,0 +1,116 @@
|
||||
<project name="orchid" default="all">
|
||||
|
||||
<exec executable="git" outputproperty="orchid.gittag" logError="true" failifexecutionfails="false">
|
||||
<arg value="rev-parse"/>
|
||||
<arg value="--short"/>
|
||||
<arg value="HEAD"/>
|
||||
</exec>
|
||||
|
||||
<property name="orchid.version" value="1.0.0" />
|
||||
|
||||
<condition property="orchid.basename" value="orchid-${orchid.version}.${orchid.gittag}">
|
||||
<isset property="orchid.gittag"/>
|
||||
</condition>
|
||||
|
||||
<property name="orchid.basename" value="orchid-${orchid.version}" />
|
||||
|
||||
<property name="orchid.jarfile" value="${orchid.basename}.jar" />
|
||||
<property name="orchid.sourcefile" value="${orchid.basename}-src.zip" />
|
||||
|
||||
<path id="compile.classpath">
|
||||
<fileset dir="lib">
|
||||
<include name="*.jar"/>
|
||||
</fileset>
|
||||
</path>
|
||||
|
||||
<path id="test.classpath">
|
||||
<fileset dir="lib/testing">
|
||||
<include name="*.jar"/>
|
||||
</fileset>
|
||||
</path>
|
||||
|
||||
<path id="bin">
|
||||
<pathelement location="${basedir}/bin"/>
|
||||
</path>
|
||||
|
||||
<condition property="xmlrpc.present">
|
||||
<and>
|
||||
<available classname="org.apache.xmlrpc.client.XmlRpcTransportFactory" classpathref="compile.classpath"/>
|
||||
<available classname="org.apache.xmlrpc.XmlRpcException" classpathref="compile.classpath"/>
|
||||
</and>
|
||||
</condition>
|
||||
|
||||
<target name="all" depends="write-revision,compile,compile-xmlrpc,package,source"/>
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="${basedir}/bin" />
|
||||
</target>
|
||||
|
||||
<target name="write-revision">
|
||||
<echo message="${orchid.gittag}${line.separator}" file="build-revision" />
|
||||
</target>
|
||||
|
||||
<target name="init">
|
||||
<mkdir dir="${basedir}/bin" />
|
||||
</target>
|
||||
|
||||
<target name="compile" depends="init">
|
||||
<javac source="1.5" target="1.5" destdir="${basedir}/bin" includeantruntime="false">
|
||||
<src path="${basedir}/src" />
|
||||
<classpath refid="compile.classpath"/>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="compile-xmlrpc" depends="init" if="xmlrpc.present">
|
||||
<echo message="compiling optional xmlrpc classes"/>
|
||||
<javac source="1.5" target="1.5" srcdir="opt/xmlrpc" destdir="bin" classpathref="compile.classpath" includeantruntime="false"/>
|
||||
</target>
|
||||
|
||||
<target name="package">
|
||||
<jar destfile="${orchid.jarfile}">
|
||||
<manifest>
|
||||
<attribute name="Main-Class" value="com.subgraph.orchid.TorClient"/>
|
||||
</manifest>
|
||||
<fileset dir="${basedir}/bin" />
|
||||
<zipfileset dir="${basedir}/data" includes="GeoIP.dat" fullpath="data/GeoIP.dat" />
|
||||
<zipfileset dir="${basedir}" includes="build-revision" />
|
||||
</jar>
|
||||
</target>
|
||||
|
||||
<target name="source">
|
||||
<zip destfile="${orchid.sourcefile}">
|
||||
<zipfileset dir="src" prefix="${orchid.basename}/src" />
|
||||
<zipfileset dir="opt/xmlrpc" prefix="${orchid.basename}/src" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<target name="compile-test" depends="compile">
|
||||
<javac source="1.5" target="1.5" destdir="${basedir}/bin" includeantruntime="false">
|
||||
<src path="${basedir}/test"/>
|
||||
<classpath>
|
||||
<path refid="compile.classpath"/>
|
||||
<path refid="test.classpath"/>
|
||||
</classpath>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="test" depends="compile-test">
|
||||
<junit printsummary="on" fork="yes" forkmode="once">
|
||||
<assertions>
|
||||
<enable/>
|
||||
</assertions>
|
||||
<classpath>
|
||||
<path refid="compile.classpath"/>
|
||||
<path refid="test.classpath"/>
|
||||
<path refid="bin"/>
|
||||
</classpath>
|
||||
<test name="com.subgraph.orchid.TorConfigTest"/>
|
||||
<test name="com.subgraph.orchid.circuits.TorInputStreamTest"/>
|
||||
<test name="com.subgraph.orchid.circuits.path.ConfigNodeFilterTest"/>
|
||||
<test name="com.subgraph.orchid.circuits.path.ConfigNodeFilterTest"/>
|
||||
<test name="com.subgraph.orchid.crypto.ASN1ParserTest"/>
|
||||
<test name="com.subgraph.orchid.crypto.RSAKeyEncoderTest"/>
|
||||
<test name="com.subgraph.orchid.geoip.CountryCodeServiceTest"/>
|
||||
</junit>
|
||||
</target>
|
||||
</project>
|
BIN
orchid/data/GeoIP.dat
Normal file
BIN
orchid/data/GeoIP.dat
Normal file
Binary file not shown.
3
orchid/data/README
Normal file
3
orchid/data/README
Normal file
@ -0,0 +1,3 @@
|
||||
GeoIP.dat GeoLite Country database downloaded September, 2013
|
||||
|
||||
http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
|
58
orchid/doc/spec/address-spec.txt
Normal file
58
orchid/doc/spec/address-spec.txt
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
Special Hostnames in Tor
|
||||
Nick Mathewson
|
||||
|
||||
1. Overview
|
||||
|
||||
Most of the time, Tor treats user-specified hostnames as opaque: When
|
||||
the user connects to www.torproject.org, Tor picks an exit node and uses
|
||||
that node to connect to "www.torproject.org". Some hostnames, however,
|
||||
can be used to override Tor's default behavior and circuit-building
|
||||
rules.
|
||||
|
||||
These hostnames can be passed to Tor as the address part of a SOCKS4a or
|
||||
SOCKS5 request. If the application is connected to Tor using an IP-only
|
||||
method (such as SOCKS4, TransPort, or NatdPort), these hostnames can be
|
||||
substituted for certain IP addresses using the MapAddress configuration
|
||||
option or the MAPADDRESS control command.
|
||||
|
||||
2. .exit
|
||||
|
||||
SYNTAX: [hostname].[name-or-digest].exit
|
||||
[name-or-digest].exit
|
||||
|
||||
Hostname is a valid hostname; [name-or-digest] is either the nickname of a
|
||||
Tor node or the hex-encoded digest of that node's public key.
|
||||
|
||||
When Tor sees an address in this format, it uses the specified hostname as
|
||||
the exit node. If no "hostname" component is given, Tor defaults to the
|
||||
published IPv4 address of the exit node.
|
||||
|
||||
It is valid to try to resolve hostnames, and in fact upon success Tor
|
||||
will cache an internal mapaddress of the form
|
||||
"www.google.com.foo.exit=64.233.161.99.foo.exit" to speed subsequent
|
||||
lookups.
|
||||
|
||||
The .exit notation is disabled by default as of Tor 0.2.2.1-alpha, due
|
||||
to potential application-level attacks.
|
||||
|
||||
EXAMPLES:
|
||||
www.example.com.exampletornode.exit
|
||||
|
||||
Connect to www.example.com from the node called "exampletornode".
|
||||
|
||||
exampletornode.exit
|
||||
|
||||
Connect to the published IP address of "exampletornode" using
|
||||
"exampletornode" as the exit.
|
||||
|
||||
3. .onion
|
||||
|
||||
SYNTAX: [digest].onion
|
||||
|
||||
The digest is the first eighty bits of a SHA1 hash of the identity key for
|
||||
a hidden service, encoded in base32.
|
||||
|
||||
When Tor sees an address in this format, it tries to look up and connect to
|
||||
the specified hidden service. See rend-spec.txt for full details.
|
||||
|
249
orchid/doc/spec/bridges-spec.txt
Normal file
249
orchid/doc/spec/bridges-spec.txt
Normal file
@ -0,0 +1,249 @@
|
||||
|
||||
Tor bridges specification
|
||||
|
||||
0. Preface
|
||||
|
||||
This document describes the design decisions around support for bridge
|
||||
users, bridge relays, and bridge authorities. It acts as an overview
|
||||
of the bridge design and deployment for developers, and it also tries
|
||||
to point out limitations in the current design and implementation.
|
||||
|
||||
For more details on what all of these mean, look at blocking.tex in
|
||||
/doc/design-paper/
|
||||
|
||||
1. Bridge relays
|
||||
|
||||
Bridge relays are just like normal Tor relays except they don't publish
|
||||
their server descriptors to the main directory authorities.
|
||||
|
||||
1.1. PublishServerDescriptor
|
||||
|
||||
To configure your relay to be a bridge relay, just add
|
||||
BridgeRelay 1
|
||||
PublishServerDescriptor bridge
|
||||
to your torrc. This will cause your relay to publish its descriptor
|
||||
to the bridge authorities rather than to the default authorities.
|
||||
|
||||
Alternatively, you can say
|
||||
BridgeRelay 1
|
||||
PublishServerDescriptor 0
|
||||
which will cause your relay to not publish anywhere. This could be
|
||||
useful for private bridges.
|
||||
|
||||
1.2. Recommendations.
|
||||
|
||||
Bridge relays should use an exit policy of "reject *:*". This is
|
||||
because they only need to relay traffic between the bridge users
|
||||
and the rest of the Tor network, so there's no need to let people
|
||||
exit directly from them.
|
||||
|
||||
We invented the RelayBandwidth* options for this situation: Tor clients
|
||||
who want to allow relaying too. See proposal 111 for details. Relay
|
||||
operators should feel free to rate-limit their relayed traffic.
|
||||
|
||||
1.3. Implementation note.
|
||||
|
||||
Vidalia 0.0.15 has turned its "Relay" settings page into a tri-state
|
||||
"Don't relay" / "Relay for the Tor network" / "Help censored users".
|
||||
|
||||
If you click the third choice, it forces your exit policy to reject *:*.
|
||||
|
||||
If all the bridges end up on port 9001, that's not so good. On the
|
||||
other hand, putting the bridges on a low-numbered port in the Unix
|
||||
world requires jumping through extra hoops. The current compromise is
|
||||
that Vidalia makes the ORPort default to 443 on Windows, and 9001 on
|
||||
other platforms.
|
||||
|
||||
At the bottom of the relay config settings window, Vidalia displays
|
||||
the bridge identifier to the operator (see Section 3.1) so he can pass
|
||||
it on to bridge users.
|
||||
|
||||
2. Bridge authorities.
|
||||
|
||||
Bridge authorities are like normal v3 directory authorities, except
|
||||
they don't create their own network-status documents or votes. So if
|
||||
you ask a bridge authority for a network-status document or consensus,
|
||||
they behave like a directory mirror: they give you one from one of
|
||||
the main authorities. But if you ask the bridge authority for the
|
||||
descriptor corresponding to a particular identity fingerprint, it will
|
||||
happily give you the latest descriptor for that fingerprint.
|
||||
|
||||
To become a bridge authority, add these lines to your torrc:
|
||||
AuthoritativeDirectory 1
|
||||
BridgeAuthoritativeDir 1
|
||||
|
||||
Right now there's one bridge authority, running on the Tonga relay.
|
||||
|
||||
2.1. Exporting bridge-purpose descriptors
|
||||
|
||||
We've added a new purpose for server descriptors: the "bridge"
|
||||
purpose. With the new router-descriptors file format that includes
|
||||
annotations, it's easy to look through it and find the bridge-purpose
|
||||
descriptors.
|
||||
|
||||
Currently we export the bridge descriptors from Tonga to the
|
||||
BridgeDB server, so it can give them out according to the policies
|
||||
in blocking.pdf.
|
||||
|
||||
2.2. Reachability/uptime testing
|
||||
|
||||
Right now the bridge authorities do active reachability testing of
|
||||
bridges, so we know which ones to recommend for users.
|
||||
|
||||
But in the design document, we suggested that bridges should publish
|
||||
anonymously (i.e. via Tor) to the bridge authority, so somebody watching
|
||||
the bridge authority can't just enumerate all the bridges. But if we're
|
||||
doing active measurement, the game is up. Perhaps we should back off on
|
||||
this goal, or perhaps we should do our active measurement anonymously?
|
||||
|
||||
Answering this issue is scheduled for 0.2.1.x.
|
||||
|
||||
2.3. Future work: migrating to multiple bridge authorities
|
||||
|
||||
Having only one bridge authority is both a trust bottleneck (if you
|
||||
break into one place you learn about every single bridge we've got)
|
||||
and a robustness bottleneck (when it's down, bridge users become sad).
|
||||
|
||||
Right now if we put up a second bridge authority, all the bridges would
|
||||
publish to it, and (assuming the code works) bridge users would query
|
||||
a random bridge authority. This resolves the robustness bottleneck,
|
||||
but makes the trust bottleneck even worse.
|
||||
|
||||
In 0.2.2.x and later we should think about better ways to have multiple
|
||||
bridge authorities.
|
||||
|
||||
3. Bridge users.
|
||||
|
||||
Bridge users are like ordinary Tor users except they use encrypted
|
||||
directory connections by default, and they use bridge relays as both
|
||||
entry guards (their first hop) and directory guards (the source of
|
||||
all their directory information).
|
||||
|
||||
To become a bridge user, add the following line to your torrc:
|
||||
UseBridges 1
|
||||
|
||||
and then add at least one "Bridge" line to your torrc based on the
|
||||
format below.
|
||||
|
||||
3.1. Format of the bridge identifier.
|
||||
|
||||
The canonical format for a bridge identifier contains an IP address,
|
||||
an ORPort, and an identity fingerprint:
|
||||
bridge 128.31.0.34:9009 4C17 FB53 2E20 B2A8 AC19 9441 ECD2 B017 7B39 E4B1
|
||||
|
||||
However, the identity fingerprint can be left out, in which case the
|
||||
bridge user will connect to that relay and use it as a bridge regardless
|
||||
of what identity key it presents:
|
||||
bridge 128.31.0.34:9009
|
||||
This might be useful for cases where only short bridge identifiers
|
||||
can be communicated to bridge users.
|
||||
|
||||
In a future version we may also support bridge identifiers that are
|
||||
only a key fingerprint:
|
||||
bridge 4C17 FB53 2E20 B2A8 AC19 9441 ECD2 B017 7B39 E4B1
|
||||
and the bridge user can fetch the latest descriptor from the bridge
|
||||
authority (see Section 3.4).
|
||||
|
||||
3.2. Bridges as entry guards
|
||||
|
||||
For now, bridge users add their bridge relays to their list of "entry
|
||||
guards" (see path-spec.txt for background on entry guards). They are
|
||||
managed by the entry guard algorithms exactly as if they were a normal
|
||||
entry guard -- their keys and timing get cached in the "state" file,
|
||||
etc. This means that when the Tor user starts up with "UseBridges"
|
||||
disabled, he will skip past the bridge entries since they won't be
|
||||
listed as up and usable in his networkstatus consensus. But to be clear,
|
||||
the "entry_guards" list doesn't currently distinguish guards by purpose.
|
||||
|
||||
Internally, each bridge user keeps a smartlist of "bridge_info_t"
|
||||
that reflects the "bridge" lines from his torrc along with a download
|
||||
schedule (see Section 3.5 below). When he starts Tor, he attempts
|
||||
to fetch a descriptor for each configured bridge (see Section 3.4
|
||||
below). When he succeeds at getting a descriptor for one of the bridges
|
||||
in his list, he adds it directly to the entry guard list using the
|
||||
normal add_an_entry_guard() interface. Once a bridge descriptor has
|
||||
been added, should_delay_dir_fetches() will stop delaying further
|
||||
directory fetches, and the user begins to bootstrap his directory
|
||||
information from that bridge (see Section 3.3).
|
||||
|
||||
Currently bridge users cache their bridge descriptors to the
|
||||
"cached-descriptors" file (annotated with purpose "bridge"), but
|
||||
they don't make any attempt to reuse descriptors they find in this
|
||||
file. The theory is that either the bridge is available now, in which
|
||||
case you can get a fresh descriptor, or it's not, in which case an
|
||||
old descriptor won't do you much good.
|
||||
|
||||
We could disable writing out the bridge lines to the state file, if
|
||||
we think this is a problem.
|
||||
|
||||
As an exception, if we get an application request when we have one
|
||||
or more bridge descriptors but we believe none of them are running,
|
||||
we mark them all as running again. This is similar to the exception
|
||||
already in place to help long-idle Tor clients realize they should
|
||||
fetch fresh directory information rather than just refuse requests.
|
||||
|
||||
3.3. Bridges as directory guards
|
||||
|
||||
In addition to using bridges as the first hop in their circuits, bridge
|
||||
users also use them to fetch directory updates. Other than initial
|
||||
bootstrapping to find a working bridge descriptor (see Section 3.4
|
||||
below), all further non-anonymized directory fetches will be redirected
|
||||
to the bridge.
|
||||
|
||||
This means that bridge relays need to have cached answers for all
|
||||
questions the bridge user might ask. This makes the upgrade path
|
||||
tricky --- for example, if we migrate to a v4 directory design, the
|
||||
bridge user would need to keep using v3 so long as his bridge relays
|
||||
only knew how to answer v3 queries.
|
||||
|
||||
In a future design, for cases where the user has enough information
|
||||
to build circuits yet the chosen bridge doesn't know how to answer a
|
||||
given query, we might teach bridge users to make an anonymized request
|
||||
to a more suitable directory server.
|
||||
|
||||
3.4. How bridge users get their bridge descriptor
|
||||
|
||||
Bridge users can fetch bridge descriptors in two ways: by going directly
|
||||
to the bridge and asking for "/tor/server/authority", or by going to
|
||||
the bridge authority and asking for "/tor/server/fp/ID". By default,
|
||||
they will only try the direct queries. If the user sets
|
||||
UpdateBridgesFromAuthority 1
|
||||
in his config file, then he will try querying the bridge authority
|
||||
first for bridges where he knows a digest (if he only knows an IP
|
||||
address and ORPort, then his only option is a direct query).
|
||||
|
||||
If the user has at least one working bridge, then he will do further
|
||||
queries to the bridge authority through a full three-hop Tor circuit.
|
||||
But when bootstrapping, he will make a direct begin_dir-style connection
|
||||
to the bridge authority.
|
||||
|
||||
As of Tor 0.2.0.10-alpha, if the user attempts to fetch a descriptor
|
||||
from the bridge authority and it returns a 404 not found, the user
|
||||
will automatically fall back to trying a direct query. Therefore it is
|
||||
recommended that bridge users always set UpdateBridgesFromAuthority,
|
||||
since at worst it will delay their fetches a little bit and notify
|
||||
the bridge authority of the identity fingerprint (but not location)
|
||||
of their intended bridges.
|
||||
|
||||
3.5. Bridge descriptor retry schedule
|
||||
|
||||
Bridge users try to fetch a descriptor for each bridge (using the
|
||||
steps in Section 3.4 above) on startup. Whenever they receive a
|
||||
bridge descriptor, they reschedule a new descriptor download for 1
|
||||
hour from then.
|
||||
|
||||
If on the other hand it fails, they try again after 15 minutes for the
|
||||
first attempt, after 15 minutes for the second attempt, and after 60
|
||||
minutes for subsequent attempts.
|
||||
|
||||
In 0.2.2.x we should come up with some smarter retry schedules.
|
||||
|
||||
3.6. Implementation note.
|
||||
|
||||
Vidalia 0.1.0 has a new checkbox in its Network config window called
|
||||
"My ISP blocks connections to the Tor network." Users who click that
|
||||
box change their configuration to:
|
||||
UseBridges 1
|
||||
UpdateBridgesFromAuthority 1
|
||||
and should add at least one bridge identifier.
|
||||
|
1853
orchid/doc/spec/control-spec.txt
Normal file
1853
orchid/doc/spec/control-spec.txt
Normal file
File diff suppressed because it is too large
Load Diff
2132
orchid/doc/spec/dir-spec.txt
Normal file
2132
orchid/doc/spec/dir-spec.txt
Normal file
File diff suppressed because it is too large
Load Diff
437
orchid/doc/spec/path-spec.txt
Normal file
437
orchid/doc/spec/path-spec.txt
Normal file
@ -0,0 +1,437 @@
|
||||
|
||||
Tor Path Specification
|
||||
|
||||
Roger Dingledine
|
||||
Nick Mathewson
|
||||
|
||||
Note: This is an attempt to specify Tor as currently implemented. Future
|
||||
versions of Tor will implement improved algorithms.
|
||||
|
||||
This document tries to cover how Tor chooses to build circuits and assign
|
||||
streams to circuits. Other implementations MAY take other approaches, but
|
||||
implementors should be aware of the anonymity and load-balancing implications
|
||||
of their choices.
|
||||
|
||||
THIS SPEC ISN'T DONE YET.
|
||||
|
||||
1. General operation
|
||||
|
||||
Tor begins building circuits as soon as it has enough directory
|
||||
information to do so (see section 5 of dir-spec.txt). Some circuits are
|
||||
built preemptively because we expect to need them later (for user
|
||||
traffic), and some are built because of immediate need (for user traffic
|
||||
that no current circuit can handle, for testing the network or our
|
||||
reachability, and so on).
|
||||
|
||||
When a client application creates a new stream (by opening a SOCKS
|
||||
connection or launching a resolve request), we attach it to an appropriate
|
||||
open circuit if one exists, or wait if an appropriate circuit is
|
||||
in-progress. We launch a new circuit only
|
||||
if no current circuit can handle the request. We rotate circuits over
|
||||
time to avoid some profiling attacks.
|
||||
|
||||
To build a circuit, we choose all the nodes we want to use, and then
|
||||
construct the circuit. Sometimes, when we want a circuit that ends at a
|
||||
given hop, and we have an appropriate unused circuit, we "cannibalize" the
|
||||
existing circuit and extend it to the new terminus.
|
||||
|
||||
These processes are described in more detail below.
|
||||
|
||||
This document describes Tor's automatic path selection logic only; path
|
||||
selection can be overridden by a controller (with the EXTENDCIRCUIT and
|
||||
ATTACHSTREAM commands). Paths constructed through these means may
|
||||
violate some constraints given below.
|
||||
|
||||
1.1. Terminology
|
||||
|
||||
A "path" is an ordered sequence of nodes, not yet built as a circuit.
|
||||
|
||||
A "clean" circuit is one that has not yet been used for any traffic.
|
||||
|
||||
A "fast" or "stable" or "valid" node is one that has the 'Fast' or
|
||||
'Stable' or 'Valid' flag
|
||||
set respectively, based on our current directory information. A "fast"
|
||||
or "stable" circuit is one consisting only of "fast" or "stable" nodes.
|
||||
|
||||
In an "exit" circuit, the final node is chosen based on waiting stream
|
||||
requests if any, and in any case it avoids nodes with exit policy of
|
||||
"reject *:*". An "internal" circuit, on the other hand, is one where
|
||||
the final node is chosen just like a middle node (ignoring its exit
|
||||
policy).
|
||||
|
||||
A "request" is a client-side stream or DNS resolve that needs to be
|
||||
served by a circuit.
|
||||
|
||||
A "pending" circuit is one that we have started to build, but which has
|
||||
not yet completed.
|
||||
|
||||
A circuit or path "supports" a request if it is okay to use the
|
||||
circuit/path to fulfill the request, according to the rules given below.
|
||||
A circuit or path "might support" a request if some aspect of the request
|
||||
is unknown (usually its target IP), but we believe the path probably
|
||||
supports the request according to the rules given below.
|
||||
|
||||
1.1. A server's bandwidth
|
||||
|
||||
Old versions of Tor did not report bandwidths in network status
|
||||
documents, so clients had to learn them from the routers' advertised
|
||||
server descriptors.
|
||||
|
||||
For versions of Tor prior to 0.2.1.17-rc, everywhere below where we
|
||||
refer to a server's "bandwidth", we mean its clipped advertised
|
||||
bandwidth, computed by taking the smaller of the 'rate' and
|
||||
'observed' arguments to the "bandwidth" element in the server's
|
||||
descriptor. If a router's advertised bandwidth is greater than
|
||||
MAX_BELIEVABLE_BANDWIDTH (currently 10 MB/s), we clipped to that
|
||||
value.
|
||||
|
||||
For more recent versions of Tor, we take the bandwidth value declared
|
||||
in the consensus, and fall back to the clipped advertised bandwidth
|
||||
only if the consensus does not have bandwidths listed.
|
||||
|
||||
2. Building circuits
|
||||
|
||||
2.1. When we build
|
||||
|
||||
2.1.1. Clients build circuits preemptively
|
||||
|
||||
When running as a client, Tor tries to maintain at least a certain
|
||||
number of clean circuits, so that new streams can be handled
|
||||
quickly. To increase the likelihood of success, Tor tries to
|
||||
predict what circuits will be useful by choosing from among nodes
|
||||
that support the ports we have used in the recent past (by default
|
||||
one hour). Specifically, on startup Tor tries to maintain one clean
|
||||
fast exit circuit that allows connections to port 80, and at least
|
||||
two fast clean stable internal circuits in case we get a resolve
|
||||
request or hidden service request (at least three if we _run_ a
|
||||
hidden service).
|
||||
|
||||
After that, Tor will adapt the circuits that it preemptively builds
|
||||
based on the requests it sees from the user: it tries to have two fast
|
||||
clean exit circuits available for every port seen within the past hour
|
||||
(each circuit can be adequate for many predicted ports -- it doesn't
|
||||
need two separate circuits for each port), and it tries to have the
|
||||
above internal circuits available if we've seen resolves or hidden
|
||||
service activity within the past hour. If there are 12 or more clean
|
||||
circuits open, it doesn't open more even if it has more predictions.
|
||||
|
||||
Only stable circuits can "cover" a port that is listed in the
|
||||
LongLivedPorts config option. Similarly, hidden service requests
|
||||
to ports listed in LongLivedPorts make us create stable internal
|
||||
circuits.
|
||||
|
||||
Note that if there are no requests from the user for an hour, Tor
|
||||
will predict no use and build no preemptive circuits.
|
||||
|
||||
The Tor client SHOULD NOT store its list of predicted requests to a
|
||||
persistent medium.
|
||||
|
||||
2.1.2. Clients build circuits on demand
|
||||
|
||||
Additionally, when a client request exists that no circuit (built or
|
||||
pending) might support, we create a new circuit to support the request.
|
||||
For exit connections, we pick an exit node that will handle the
|
||||
most pending requests (choosing arbitrarily among ties), launch a
|
||||
circuit to end there, and repeat until every unattached request
|
||||
might be supported by a pending or built circuit. For internal
|
||||
circuits, we pick an arbitrary acceptable path, repeating as needed.
|
||||
|
||||
In some cases we can reuse an already established circuit if it's
|
||||
clean; see Section 2.3 (cannibalizing circuits) for details.
|
||||
|
||||
2.1.3. Servers build circuits for testing reachability and bandwidth
|
||||
|
||||
Tor servers test reachability of their ORPort once they have
|
||||
successfully built a circuit (on start and whenever their IP address
|
||||
changes). They build an ordinary fast internal circuit with themselves
|
||||
as the last hop. As soon as any testing circuit succeeds, the Tor
|
||||
server decides it's reachable and is willing to publish a descriptor.
|
||||
|
||||
We launch multiple testing circuits (one at a time), until we
|
||||
have NUM_PARALLEL_TESTING_CIRC (4) such circuits open. Then we
|
||||
do a "bandwidth test" by sending a certain number of relay drop
|
||||
cells down each circuit: BandwidthRate * 10 / CELL_NETWORK_SIZE
|
||||
total cells divided across the four circuits, but never more than
|
||||
CIRCWINDOW_START (1000) cells total. This exercises both outgoing and
|
||||
incoming bandwidth, and helps to jumpstart the observed bandwidth
|
||||
(see dir-spec.txt).
|
||||
|
||||
Tor servers also test reachability of their DirPort once they have
|
||||
established a circuit, but they use an ordinary exit circuit for
|
||||
this purpose.
|
||||
|
||||
2.1.4. Hidden-service circuits
|
||||
|
||||
See section 4 below.
|
||||
|
||||
2.1.5. Rate limiting of failed circuits
|
||||
|
||||
If we fail to build a circuit N times in a X second period (see Section
|
||||
2.3 for how this works), we stop building circuits until the X seconds
|
||||
have elapsed.
|
||||
XXXX
|
||||
|
||||
2.1.6. When to tear down circuits
|
||||
|
||||
XXXX
|
||||
|
||||
2.2. Path selection and constraints
|
||||
|
||||
We choose the path for each new circuit before we build it. We choose the
|
||||
exit node first, followed by the other nodes in the circuit. All paths
|
||||
we generate obey the following constraints:
|
||||
- We do not choose the same router twice for the same path.
|
||||
- We do not choose any router in the same family as another in the same
|
||||
path.
|
||||
- We do not choose more than one router in a given /16 subnet
|
||||
(unless EnforceDistinctSubnets is 0).
|
||||
- We don't choose any non-running or non-valid router unless we have
|
||||
been configured to do so. By default, we are configured to allow
|
||||
non-valid routers in "middle" and "rendezvous" positions.
|
||||
- If we're using Guard nodes, the first node must be a Guard (see 5
|
||||
below)
|
||||
- XXXX Choosing the length
|
||||
|
||||
For circuits that do not need to be "fast", when choosing among
|
||||
multiple candidates for a path element, we choose randomly.
|
||||
|
||||
For "fast" circuits, we pick a given router as an exit with probability
|
||||
proportional to its bandwidth.
|
||||
|
||||
For non-exit positions on "fast" circuits, we pick routers as above, but
|
||||
we weight the bandwidth of Exit-flagged nodes depending
|
||||
on the fraction of bandwidth available from non-Exit nodes. Call the
|
||||
total bandwidth for Exit nodes under consideration E,
|
||||
and the total bandwidth for all nodes under
|
||||
consideration T. If E<T/3, we do not consider Exit-flagged nodes.
|
||||
Otherwise, we weight their bandwidth with the factor (E-T/3)/E. This
|
||||
ensures that bandwidth is evenly distributed over nodes in 3-hop paths.
|
||||
|
||||
Similarly, guard nodes are weighted by the factor (G-T/3)/G, and not
|
||||
considered for non-guard positions if this value is less than 0.
|
||||
|
||||
Additionally, we may be building circuits with one or more requests in
|
||||
mind. Each kind of request puts certain constraints on paths:
|
||||
|
||||
- All service-side introduction circuits and all rendezvous paths
|
||||
should be Stable.
|
||||
- All connection requests for connections that we think will need to
|
||||
stay open a long time require Stable circuits. Currently, Tor decides
|
||||
this by examining the request's target port, and comparing it to a
|
||||
list of "long-lived" ports. (Default: 21, 22, 706, 1863, 5050,
|
||||
5190, 5222, 5223, 6667, 6697, 8300.)
|
||||
- DNS resolves require an exit node whose exit policy is not equivalent
|
||||
to "reject *:*".
|
||||
- Reverse DNS resolves require a version of Tor with advertised eventdns
|
||||
support (available in Tor 0.1.2.1-alpha-dev and later).
|
||||
- All connection requests require an exit node whose exit policy
|
||||
supports their target address and port (if known), or which "might
|
||||
support it" (if the address isn't known). See 2.2.1.
|
||||
- Rules for Fast? XXXXX
|
||||
|
||||
2.2.1. Choosing an exit
|
||||
|
||||
If we know what IP address we want to connect to or resolve, we can
|
||||
trivially tell whether a given router will support it by simulating
|
||||
its declared exit policy.
|
||||
|
||||
Because we often connect to addresses of the form hostname:port, we do not
|
||||
always know the target IP address when we select an exit node. In these
|
||||
cases, we need to pick an exit node that "might support" connections to a
|
||||
given address port with an unknown address. An exit node "might support"
|
||||
such a connection if any clause that accepts any connections to that port
|
||||
precedes all clauses (if any) that reject all connections to that port.
|
||||
|
||||
Unless requested to do so by the user, we never choose an exit server
|
||||
flagged as "BadExit" by more than half of the authorities who advertise
|
||||
themselves as listing bad exits.
|
||||
|
||||
2.2.2. User configuration
|
||||
|
||||
Users can alter the default behavior for path selection with configuration
|
||||
options.
|
||||
|
||||
- If "ExitNodes" is provided, then every request requires an exit node on
|
||||
the ExitNodes list. (If a request is supported by no nodes on that list,
|
||||
and StrictExitNodes is false, then Tor treats that request as if
|
||||
ExitNodes were not provided.)
|
||||
|
||||
- "EntryNodes" and "StrictEntryNodes" behave analogously.
|
||||
|
||||
- If a user tries to connect to or resolve a hostname of the form
|
||||
<target>.<servername>.exit, the request is rewritten to a request for
|
||||
<target>, and the request is only supported by the exit whose nickname
|
||||
or fingerprint is <servername>.
|
||||
|
||||
2.3. Cannibalizing circuits
|
||||
|
||||
If we need a circuit and have a clean one already established, in
|
||||
some cases we can adapt the clean circuit for our new
|
||||
purpose. Specifically,
|
||||
|
||||
For hidden service interactions, we can "cannibalize" a clean internal
|
||||
circuit if one is available, so we don't need to build those circuits
|
||||
from scratch on demand.
|
||||
|
||||
We can also cannibalize clean circuits when the client asks to exit
|
||||
at a given node -- either via the ".exit" notation or because the
|
||||
destination is running at the same location as an exit node.
|
||||
|
||||
|
||||
2.4. Handling failure
|
||||
|
||||
If an attempt to extend a circuit fails (either because the first create
|
||||
failed or a subsequent extend failed) then the circuit is torn down and is
|
||||
no longer pending. (XXXX really?) Requests that might have been
|
||||
supported by the pending circuit thus become unsupported, and a new
|
||||
circuit needs to be constructed.
|
||||
|
||||
If a stream "begin" attempt fails with an EXITPOLICY error, we
|
||||
decide that the exit node's exit policy is not correctly advertised,
|
||||
so we treat the exit node as if it were a non-exit until we retrieve
|
||||
a fresh descriptor for it.
|
||||
|
||||
XXXX
|
||||
|
||||
3. Attaching streams to circuits
|
||||
|
||||
When a circuit that might support a request is built, Tor tries to attach
|
||||
the request's stream to the circuit and sends a BEGIN, BEGIN_DIR,
|
||||
or RESOLVE relay
|
||||
cell as appropriate. If the request completes unsuccessfully, Tor
|
||||
considers the reason given in the CLOSE relay cell. [XXX yes, and?]
|
||||
|
||||
|
||||
After a request has remained unattached for SocksTimeout (2 minutes
|
||||
by default), Tor abandons the attempt and signals an error to the
|
||||
client as appropriate (e.g., by closing the SOCKS connection).
|
||||
|
||||
XXX Timeouts and when Tor auto-retries.
|
||||
* What stream-end-reasons are appropriate for retrying.
|
||||
|
||||
If no reply to BEGIN/RESOLVE, then the stream will timeout and fail.
|
||||
|
||||
4. Hidden-service related circuits
|
||||
|
||||
XXX Tracking expected hidden service use (client-side and hidserv-side)
|
||||
|
||||
5. Guard nodes
|
||||
|
||||
We use Guard nodes (also called "helper nodes" in the literature) to
|
||||
prevent certain profiling attacks. Here's the risk: if we choose entry and
|
||||
exit nodes at random, and an attacker controls C out of N servers
|
||||
(ignoring bandwidth), then the
|
||||
attacker will control the entry and exit node of any given circuit with
|
||||
probability (C/N)^2. But as we make many different circuits over time,
|
||||
then the probability that the attacker will see a sample of about (C/N)^2
|
||||
of our traffic goes to 1. Since statistical sampling works, the attacker
|
||||
can be sure of learning a profile of our behavior.
|
||||
|
||||
If, on the other hand, we picked an entry node and held it fixed, we would
|
||||
have probability C/N of choosing a bad entry and being profiled, and
|
||||
probability (N-C)/N of choosing a good entry and not being profiled.
|
||||
|
||||
When guard nodes are enabled, Tor maintains an ordered list of entry nodes
|
||||
as our chosen guards, and stores this list persistently to disk. If a Guard
|
||||
node becomes unusable, rather than replacing it, Tor adds new guards to the
|
||||
end of the list. When choosing the first hop of a circuit, Tor
|
||||
chooses at
|
||||
random from among the first NumEntryGuards (default 3) usable guards on the
|
||||
list. If there are not at least 2 usable guards on the list, Tor adds
|
||||
routers until there are, or until there are no more usable routers to add.
|
||||
|
||||
A guard is unusable if any of the following hold:
|
||||
- it is not marked as a Guard by the networkstatuses,
|
||||
- it is not marked Valid (and the user hasn't set AllowInvalid entry)
|
||||
- it is not marked Running
|
||||
- Tor couldn't reach it the last time it tried to connect
|
||||
|
||||
A guard is unusable for a particular circuit if any of the rules for path
|
||||
selection in 2.2 are not met. In particular, if the circuit is "fast"
|
||||
and the guard is not Fast, or if the circuit is "stable" and the guard is
|
||||
not Stable, or if the guard has already been chosen as the exit node in
|
||||
that circuit, Tor can't use it as a guard node for that circuit.
|
||||
|
||||
If the guard is excluded because of its status in the networkstatuses for
|
||||
over 30 days, Tor removes it from the list entirely, preserving order.
|
||||
|
||||
If Tor fails to connect to an otherwise usable guard, it retries
|
||||
periodically: every hour for six hours, every 4 hours for 3 days, every
|
||||
18 hours for a week, and every 36 hours thereafter. Additionally, Tor
|
||||
retries unreachable guards the first time it adds a new guard to the list,
|
||||
since it is possible that the old guards were only marked as unreachable
|
||||
because the network was unreachable or down.
|
||||
|
||||
Tor does not add a guard persistently to the list until the first time we
|
||||
have connected to it successfully.
|
||||
|
||||
6. Router descriptor purposes
|
||||
|
||||
There are currently three "purposes" supported for router descriptors:
|
||||
general, controller, and bridge. Most descriptors are of type general
|
||||
-- these are the ones listed in the consensus, and the ones fetched
|
||||
and used in normal cases.
|
||||
|
||||
Controller-purpose descriptors are those delivered by the controller
|
||||
and labelled as such: they will be kept around (and expire like
|
||||
normal descriptors), and they can be used by the controller in its
|
||||
CIRCUITEXTEND commands. Otherwise they are ignored by Tor when it
|
||||
chooses paths.
|
||||
|
||||
Bridge-purpose descriptors are for routers that are used as bridges. See
|
||||
doc/design-paper/blocking.pdf for more design explanation, or proposal
|
||||
125 for specific details. Currently bridge descriptors are used in place
|
||||
of normal entry guards, for Tor clients that have UseBridges enabled.
|
||||
|
||||
|
||||
X. Old notes
|
||||
|
||||
X.1. Do we actually do this?
|
||||
|
||||
How to deal with network down.
|
||||
- While all helpers are down/unreachable and there are no established
|
||||
or on-the-way testing circuits, launch a testing circuit. (Do this
|
||||
periodically in the same way we try to establish normal circuits
|
||||
when things are working normally.)
|
||||
(Testing circuits are a special type of circuit, that streams won't
|
||||
attach to by accident.)
|
||||
- When a testing circuit succeeds, mark all helpers up and hold
|
||||
the testing circuit open.
|
||||
- If a connection to a helper succeeds, close all testing circuits.
|
||||
Else mark that helper down and try another.
|
||||
- If the last helper is marked down and we already have a testing
|
||||
circuit established, then add the first hop of that testing circuit
|
||||
to the end of our helper node list, close that testing circuit,
|
||||
and go back to square one. (Actually, rather than closing the
|
||||
testing circuit, can we get away with converting it to a normal
|
||||
circuit and beginning to use it immediately?)
|
||||
|
||||
[Do we actually do any of the above? If so, let's spec it. If not, let's
|
||||
remove it. -NM]
|
||||
|
||||
X.2. A thing we could do to deal with reachability.
|
||||
|
||||
And as a bonus, it leads to an answer to Nick's attack ("If I pick
|
||||
my helper nodes all on 18.0.0.0:*, then I move, you'll know where I
|
||||
bootstrapped") -- the answer is to pick your original three helper nodes
|
||||
without regard for reachability. Then the above algorithm will add some
|
||||
more that are reachable for you, and if you move somewhere, it's more
|
||||
likely (though not certain) that some of the originals will become useful.
|
||||
Is that smart or just complex?
|
||||
|
||||
X.3. Some stuff that worries me about entry guards. 2006 Jun, Nickm.
|
||||
|
||||
It is unlikely for two users to have the same set of entry guards.
|
||||
Observing a user is sufficient to learn its entry guards. So, as we move
|
||||
around, entry guards make us linkable. If we want to change guards when
|
||||
our location (IP? subnet?) changes, we have two bad options. We could
|
||||
- Drop the old guards. But if we go back to our old location,
|
||||
we'll not use our old guards. For a laptop that sometimes gets used
|
||||
from work and sometimes from home, this is pretty fatal.
|
||||
- Remember the old guards as associated with the old location, and use
|
||||
them again if we ever go back to the old location. This would be
|
||||
nasty, since it would force us to record where we've been.
|
||||
|
||||
[Do we do any of this now? If not, this should move into 099-misc or
|
||||
098-todo. -NM]
|
||||
|
751
orchid/doc/spec/rend-spec.txt
Normal file
751
orchid/doc/spec/rend-spec.txt
Normal file
@ -0,0 +1,751 @@
|
||||
|
||||
Tor Rendezvous Specification
|
||||
|
||||
0. Overview and preliminaries
|
||||
|
||||
Read
|
||||
https://www.torproject.org/doc/design-paper/tor-design.html#sec:rendezvous
|
||||
before you read this specification. It will make more sense.
|
||||
|
||||
Rendezvous points provide location-hidden services (server
|
||||
anonymity) for the onion routing network. With rendezvous points,
|
||||
Bob can offer a TCP service (say, a webserver) via the onion
|
||||
routing network, without revealing the IP of that service.
|
||||
|
||||
Bob does this by anonymously advertising a public key for his
|
||||
service, along with a list of onion routers to act as "Introduction
|
||||
Points" for his service. He creates forward circuits to those
|
||||
introduction points, and tells them about his public key. To
|
||||
connect to Bob, Alice first builds a circuit to an OR to act as
|
||||
her "Rendezvous Point." She then connects to one of Bob's chosen
|
||||
introduction points, optionally provides authentication or
|
||||
authorization information, and asks it to tell him about her Rendezvous
|
||||
Point (RP). If Bob chooses to answer, he builds a circuit to her
|
||||
RP, and tells it to connect him to Alice. The RP joins their
|
||||
circuits together, and begins relaying cells. Alice's 'BEGIN'
|
||||
cells are received directly by Bob's OP, which passes data to
|
||||
and from the local server implementing Bob's service.
|
||||
|
||||
Below we describe a network-level specification of this service,
|
||||
along with interfaces to make this process transparent to Alice
|
||||
(so long as she is using an OP).
|
||||
|
||||
0.1. Notation, conventions and prerequisites
|
||||
|
||||
In the specifications below, we use the same notation and terminology
|
||||
as in "tor-spec.txt". The service specified here also requires the
|
||||
existence of an onion routing network as specified in that file.
|
||||
|
||||
H(x) is a SHA1 digest of x.
|
||||
PKSign(SK,x) is a PKCS.1-padded RSA signature of x with SK.
|
||||
PKEncrypt(SK,x) is a PKCS.1-padded RSA encryption of x with SK.
|
||||
Public keys are all RSA, and encoded in ASN.1.
|
||||
All integers are stored in network (big-endian) order.
|
||||
All symmetric encryption uses AES in counter mode, except where
|
||||
otherwise noted.
|
||||
|
||||
In all discussions, "Alice" will refer to a user connecting to a
|
||||
location-hidden service, and "Bob" will refer to a user running a
|
||||
location-hidden service.
|
||||
|
||||
An OP is (as defined elsewhere) an "Onion Proxy" or Tor client.
|
||||
|
||||
An OR is (as defined elsewhere) an "Onion Router" or Tor server.
|
||||
|
||||
An "Introduction point" is a Tor server chosen to be Bob's medium-term
|
||||
'meeting place'. A "Rendezvous point" is a Tor server chosen by Alice to
|
||||
be a short-term communication relay between her and Bob. All Tor servers
|
||||
potentially act as introduction and rendezvous points.
|
||||
|
||||
0.2. Protocol outline
|
||||
|
||||
1. Bob->Bob's OP: "Offer IP:Port as
|
||||
public-key-name:Port". [configuration]
|
||||
(We do not specify this step; it is left to the implementor of
|
||||
Bob's OP.)
|
||||
|
||||
2. Bob's OP generates keypair and rendezvous service descriptor:
|
||||
"Meet public-key X at introduction point A, B, or C." (signed)
|
||||
|
||||
3. Bob's OP->Introduction point via Tor: [introduction setup]
|
||||
"This pk is me."
|
||||
|
||||
4. Bob's OP->directory service via Tor: publishes Bob's service
|
||||
descriptor [advertisement]
|
||||
|
||||
5. Out of band, Alice receives a [x.y.]z.onion:port address.
|
||||
She opens a SOCKS connection to her OP, and requests
|
||||
x.y.z.onion:port.
|
||||
|
||||
6. Alice's OP retrieves Bob's descriptor via Tor. [descriptor lookup.]
|
||||
|
||||
7. Alice's OP chooses a rendezvous point, opens a circuit to that
|
||||
rendezvous point, and establishes a rendezvous circuit. [rendezvous
|
||||
setup.]
|
||||
|
||||
8. Alice connects to the Introduction point via Tor, and tells it about
|
||||
her rendezvous point and optional authentication/authorization
|
||||
information. (Encrypted to Bob.) [Introduction 1]
|
||||
|
||||
9. The Introduction point passes this on to Bob's OP via Tor, along the
|
||||
introduction circuit. [Introduction 2]
|
||||
|
||||
10. Bob's OP decides whether to connect to Alice, and if so, creates a
|
||||
circuit to Alice's RP via Tor. Establishes a shared circuit.
|
||||
[Rendezvous.]
|
||||
|
||||
11. Alice's OP sends begin cells to Bob's OP. [Connection]
|
||||
|
||||
0.3. Constants and new cell types
|
||||
|
||||
Relay cell types
|
||||
32 -- RELAY_ESTABLISH_INTRO
|
||||
33 -- RELAY_ESTABLISH_RENDEZVOUS
|
||||
34 -- RELAY_INTRODUCE1
|
||||
35 -- RELAY_INTRODUCE2
|
||||
36 -- RELAY_RENDEZVOUS1
|
||||
37 -- RELAY_RENDEZVOUS2
|
||||
38 -- RELAY_INTRO_ESTABLISHED
|
||||
39 -- RELAY_RENDEZVOUS_ESTABLISHED
|
||||
40 -- RELAY_COMMAND_INTRODUCE_ACK
|
||||
|
||||
0.4. Version overview
|
||||
|
||||
There are several parts in the hidden service protocol that have
|
||||
changed over time, each of them having its own version number, whereas
|
||||
other parts remained the same. The following list of potentially
|
||||
versioned protocol parts should help reduce some confusion:
|
||||
|
||||
- Hidden service descriptor: the binary-based v0 was the default for
|
||||
a long time, and an ascii-based v2 has been added by proposal
|
||||
114. See 1.2.
|
||||
|
||||
- Hidden service descriptor propagation mechanism: currently related to
|
||||
the hidden service descriptor version -- v0 publishes to the original
|
||||
hs directory authorities, whereas v2 publishes to a rotating subset
|
||||
of relays with the "hsdir" flag; see 1.4 and 1.6.
|
||||
|
||||
- Introduction protocol for how to generate an introduction cell:
|
||||
v0 specified a nickname for the rendezvous point and assumed the
|
||||
relay would know about it, whereas v2 now specifies IP address,
|
||||
port, and onion key so the relay doesn't need to already recognize
|
||||
it. See 1.8.
|
||||
|
||||
1. The Protocol
|
||||
|
||||
1.1. Bob configures his local OP.
|
||||
|
||||
We do not specify a format for the OP configuration file. However,
|
||||
OPs SHOULD allow Bob to provide more than one advertised service
|
||||
per OP, and MUST allow Bob to specify one or more virtual ports per
|
||||
service. Bob provides a mapping from each of these virtual ports
|
||||
to a local IP:Port pair.
|
||||
|
||||
1.2. Bob's OP generates service descriptors.
|
||||
|
||||
The first time the OP provides an advertised service, it generates
|
||||
a public/private keypair (stored locally).
|
||||
|
||||
Beginning with 0.2.0.10-alpha, Bob's OP encodes "V2" descriptors. The
|
||||
format of a "V2" descriptor is as follows:
|
||||
|
||||
"rendezvous-service-descriptor" descriptor-id NL
|
||||
|
||||
[At start, exactly once]
|
||||
|
||||
Indicates the beginning of the descriptor. "descriptor-id" is a
|
||||
periodically changing identifier of 160 bits formatted as 32 base32
|
||||
chars that is calculated by the hidden service and its clients. If
|
||||
the optional "descriptor-cookie" is used, this "descriptor-id"
|
||||
cannot be computed by anyone else. (Everyone can verify that this
|
||||
"descriptor-id" belongs to the rest of the descriptor, even without
|
||||
knowing the optional "descriptor-cookie", as described below.) The
|
||||
"descriptor-id" is calculated by performing the following operation:
|
||||
|
||||
descriptor-id =
|
||||
H(permanent-id | H(time-period | descriptor-cookie | replica))
|
||||
|
||||
"permanent-id" is the permanent identifier of the hidden service,
|
||||
consisting of 80 bits. It can be calculated by computing the hash value
|
||||
of the public hidden service key and truncating after the first 80 bits:
|
||||
|
||||
permanent-id = H(public-key)[:10]
|
||||
|
||||
"H(time-period | descriptor-cookie | replica)" is the (possibly
|
||||
secret) id part that is
|
||||
necessary to verify that the hidden service is the true originator
|
||||
of this descriptor. It can only be created by the hidden service
|
||||
and its clients, but the "signature" below can only be created by
|
||||
the service.
|
||||
|
||||
"descriptor-cookie" is an optional secret password of 128 bits that
|
||||
is shared between the hidden service provider and its clients.
|
||||
|
||||
"replica" denotes the number of the non-consecutive replica.
|
||||
|
||||
(Each descriptor is replicated on a number of _consecutive_ nodes
|
||||
in the identifier ring by making every storing node responsible
|
||||
for the identifier intervals starting from its 3rd predecessor's
|
||||
ID to its own ID. In addition to that, every service publishes
|
||||
multiple descriptors with different descriptor IDs in order to
|
||||
distribute them to different places on the ring. Therefore,
|
||||
"replica" chooses one of the _non-consecutive_ replicas. -KL)
|
||||
|
||||
The "time-period" changes periodically depending on the global time and
|
||||
as a function of "permanent-id". The current value for "time-period" can
|
||||
be calculated using the following formula:
|
||||
|
||||
time-period = (current-time + permanent-id-byte * 86400 / 256)
|
||||
/ 86400
|
||||
|
||||
"current-time" contains the current system time in seconds since
|
||||
1970-01-01 00:00, e.g. 1188241957. "permanent-id-byte" is the first
|
||||
(unsigned) byte of the permanent identifier (which is in network
|
||||
order), e.g. 143. Adding the product of "permanent-id-byte" and
|
||||
86400 (seconds per day), divided by 256, prevents "time-period" from
|
||||
changing for all descriptors at the same time of the day. The result
|
||||
of the overall operation is a (network-ordered) 32-bit integer, e.g.
|
||||
13753 or 0x000035B9 with the example values given above.
|
||||
|
||||
"version" version-number NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The version number of this descriptor's format. In this case: 2.
|
||||
|
||||
"permanent-key" NL a public key in PEM format
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The public key of the hidden service which is required to verify the
|
||||
"descriptor-id" and the "signature".
|
||||
|
||||
"secret-id-part" secret-id-part NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The result of the following operation as explained above, formatted as
|
||||
32 base32 chars. Using this secret id part, everyone can verify that
|
||||
the signed descriptor belongs to "descriptor-id".
|
||||
|
||||
secret-id-part = H(time-period | descriptor-cookie | replica)
|
||||
|
||||
"publication-time" YYYY-MM-DD HH:MM:SS NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
A timestamp when this descriptor has been created.
|
||||
|
||||
"protocol-versions" version-string NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
A comma-separated list of recognized and permitted version numbers
|
||||
for use in INTRODUCE cells; these versions are described in section
|
||||
1.8 below.
|
||||
|
||||
"introduction-points" NL encrypted-string
|
||||
|
||||
[At most once]
|
||||
|
||||
A list of introduction points. If the optional "descriptor-cookie" is
|
||||
used, this list is encrypted with AES in CTR mode with a random
|
||||
initialization vector of 128 bits that is written to
|
||||
the beginning of the encrypted string, and the "descriptor-cookie" as
|
||||
secret key of 128 bits length.
|
||||
|
||||
The string containing the introduction point data (either encrypted
|
||||
or not) is encoded in base64, and surrounded with
|
||||
"-----BEGIN MESSAGE-----" and "-----END MESSAGE-----".
|
||||
|
||||
The unencrypted string may begin with:
|
||||
|
||||
["service-authentication" auth-type NL auth-data ... reserved]
|
||||
|
||||
[At start, any number]
|
||||
|
||||
The service-specific authentication data can be used to perform
|
||||
client authentication. This data is independent of the selected
|
||||
introduction point as opposed to "intro-authentication" below.
|
||||
|
||||
Subsequently, an arbitrary number of introduction point entries may
|
||||
follow, each containing the following data:
|
||||
|
||||
"introduction-point" identifier NL
|
||||
|
||||
[At start, exactly once]
|
||||
|
||||
The identifier of this introduction point: the base-32 encoded
|
||||
hash of this introduction point's identity key.
|
||||
|
||||
"ip-address" ip-address NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The IP address of this introduction point.
|
||||
|
||||
"onion-port" port NL
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The TCP port on which the introduction point is listening for
|
||||
incoming onion requests.
|
||||
|
||||
"onion-key" NL a public key in PEM format
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The public key that can be used to encrypt messages to this
|
||||
introduction point.
|
||||
|
||||
"service-key" NL a public key in PEM format
|
||||
|
||||
[Exactly once]
|
||||
|
||||
The public key that can be used to encrypt messages to the hidden
|
||||
service.
|
||||
|
||||
["intro-authentication" auth-type NL auth-data ... reserved]
|
||||
|
||||
[Any number]
|
||||
|
||||
The introduction-point-specific authentication data can be used
|
||||
to perform client authentication. This data depends on the
|
||||
selected introduction point as opposed to "service-authentication"
|
||||
above.
|
||||
|
||||
(This ends the fields in the encrypted portion of the descriptor.)
|
||||
|
||||
[It's ok for Bob to advertise 0 introduction points. He might want
|
||||
to do that if he previously advertised some introduction points,
|
||||
and now he doesn't have any. -RD]
|
||||
|
||||
"signature" NL signature-string
|
||||
|
||||
[At end, exactly once]
|
||||
|
||||
A signature of all fields above with the private key of the hidden
|
||||
service.
|
||||
|
||||
1.2.1. Other descriptor formats we don't use.
|
||||
|
||||
Support for the V0 descriptor format was dropped in 0.2.2.0-alpha-dev:
|
||||
|
||||
KL Key length [2 octets]
|
||||
PK Bob's public key [KL octets]
|
||||
TS A timestamp [4 octets]
|
||||
NI Number of introduction points [2 octets]
|
||||
Ipt A list of NUL-terminated ORs [variable]
|
||||
SIG Signature of above fields [variable]
|
||||
|
||||
KL is the length of PK, in octets.
|
||||
TS is the number of seconds elapsed since Jan 1, 1970.
|
||||
|
||||
The members of Ipt may be either (a) nicknames, or (b) identity key
|
||||
digests, encoded in hex, and prefixed with a '$'.
|
||||
|
||||
The V1 descriptor format was understood and accepted from
|
||||
0.1.1.5-alpha-cvs to 0.2.0.6-alpha-dev, but no Tors generated it and
|
||||
it was removed:
|
||||
|
||||
V Format byte: set to 255 [1 octet]
|
||||
V Version byte: set to 1 [1 octet]
|
||||
KL Key length [2 octets]
|
||||
PK Bob's public key [KL octets]
|
||||
TS A timestamp [4 octets]
|
||||
PROTO Protocol versions: bitmask [2 octets]
|
||||
NI Number of introduction points [2 octets]
|
||||
For each introduction point: (as in INTRODUCE2 cells)
|
||||
IP Introduction point's address [4 octets]
|
||||
PORT Introduction point's OR port [2 octets]
|
||||
ID Introduction point identity ID [20 octets]
|
||||
KLEN Length of onion key [2 octets]
|
||||
KEY Introduction point onion key [KLEN octets]
|
||||
SIG Signature of above fields [variable]
|
||||
|
||||
A hypothetical "V1" descriptor, that has never been used but might
|
||||
be useful for historical reasons, contains:
|
||||
|
||||
V Format byte: set to 255 [1 octet]
|
||||
V Version byte: set to 1 [1 octet]
|
||||
KL Key length [2 octets]
|
||||
PK Bob's public key [KL octets]
|
||||
TS A timestamp [4 octets]
|
||||
PROTO Rendezvous protocol versions: bitmask [2 octets]
|
||||
NA Number of auth mechanisms accepted [1 octet]
|
||||
For each auth mechanism:
|
||||
AUTHT The auth type that is supported [2 octets]
|
||||
AUTHL Length of auth data [1 octet]
|
||||
AUTHD Auth data [variable]
|
||||
NI Number of introduction points [2 octets]
|
||||
For each introduction point: (as in INTRODUCE2 cells)
|
||||
ATYPE An address type (typically 4) [1 octet]
|
||||
ADDR Introduction point's IP address [4 or 16 octets]
|
||||
PORT Introduction point's OR port [2 octets]
|
||||
AUTHT The auth type that is supported [2 octets]
|
||||
AUTHL Length of auth data [1 octet]
|
||||
AUTHD Auth data [variable]
|
||||
ID Introduction point identity ID [20 octets]
|
||||
KLEN Length of onion key [2 octets]
|
||||
KEY Introduction point onion key [KLEN octets]
|
||||
SIG Signature of above fields [variable]
|
||||
|
||||
AUTHT specifies which authentication/authorization mechanism is
|
||||
required by the hidden service or the introduction point. AUTHD
|
||||
is arbitrary data that can be associated with an auth approach.
|
||||
Currently only AUTHT of [00 00] is supported, with an AUTHL of 0.
|
||||
See section 2 of this document for details on auth mechanisms.
|
||||
|
||||
1.3. Bob's OP establishes his introduction points.
|
||||
|
||||
The OP establishes a new introduction circuit to each introduction
|
||||
point. These circuits MUST NOT be used for anything but hidden service
|
||||
introduction. To establish the introduction, Bob sends a
|
||||
RELAY_ESTABLISH_INTRO cell, containing:
|
||||
|
||||
KL Key length [2 octets]
|
||||
PK Introduction public key [KL octets]
|
||||
HS Hash of session info [20 octets]
|
||||
SIG Signature of above information [variable]
|
||||
|
||||
[XXX011, need to add auth information here. -RD]
|
||||
|
||||
To prevent replay attacks, the HS field contains a SHA-1 hash based on the
|
||||
shared secret KH between Bob's OP and the introduction point, as
|
||||
follows:
|
||||
HS = H(KH | "INTRODUCE")
|
||||
That is:
|
||||
HS = H(KH | [49 4E 54 52 4F 44 55 43 45])
|
||||
(KH, as specified in tor-spec.txt, is H(g^xy | [00]) .)
|
||||
|
||||
Upon receiving such a cell, the OR first checks that the signature is
|
||||
correct with the included public key. If so, it checks whether HS is
|
||||
correct given the shared state between Bob's OP and the OR. If either
|
||||
check fails, the OP discards the cell; otherwise, it associates the
|
||||
circuit with Bob's public key, and dissociates any other circuits
|
||||
currently associated with PK. On success, the OR sends Bob a
|
||||
RELAY_INTRO_ESTABLISHED cell with an empty payload.
|
||||
|
||||
Bob's OP does not include its own public key in the RELAY_ESTABLISH_INTRO
|
||||
cell, but the public key of a freshly generated introduction key pair.
|
||||
The OP also includes these fresh public keys in the v2 hidden service
|
||||
descriptor together with the other introduction point information. The
|
||||
reason is that the introduction point does not need to and therefore
|
||||
should not know for which hidden service it works, so as to prevent it
|
||||
from tracking the hidden service's activity.
|
||||
|
||||
1.4. Bob's OP advertises his service descriptor(s).
|
||||
|
||||
Bob's OP opens a stream to each directory server's directory port via Tor.
|
||||
(He may re-use old circuits for this.) Over this stream, Bob's OP makes
|
||||
an HTTP 'POST' request, to a URL "/tor/rendezvous/publish" relative to the
|
||||
directory server's root, containing as its body Bob's service descriptor.
|
||||
|
||||
Bob should upload a service descriptor for each version format that
|
||||
is supported in the current Tor network.
|
||||
|
||||
Upon receiving a descriptor, the directory server checks the signature,
|
||||
and discards the descriptor if the signature does not match the enclosed
|
||||
public key. Next, the directory server checks the timestamp. If the
|
||||
timestamp is more than 24 hours in the past or more than 1 hour in the
|
||||
future, or the directory server already has a newer descriptor with the
|
||||
same public key, the server discards the descriptor. Otherwise, the
|
||||
server discards any older descriptors with the same public key and
|
||||
version format, and associates the new descriptor with the public key.
|
||||
The directory server remembers this descriptor for at least 24 hours
|
||||
after its timestamp. At least every 18 hours, Bob's OP uploads a
|
||||
fresh descriptor.
|
||||
|
||||
Bob's OP publishes v2 descriptors to a changing subset of all v2 hidden
|
||||
service directories. Therefore, Bob's OP opens a stream via Tor to each
|
||||
responsible hidden service directory. (He may re-use old circuits
|
||||
for this.) Over this stream, Bob's OP makes an HTTP 'POST' request to a
|
||||
URL "/tor/rendezvous2/publish" relative to the hidden service
|
||||
directory's root, containing as its body Bob's service descriptor.
|
||||
|
||||
At any time, there are 6 hidden service directories responsible for
|
||||
keeping replicas of a descriptor; they consist of 2 sets of 3 hidden
|
||||
service directories with consecutive onion IDs. Bob's OP learns about
|
||||
the complete list of hidden service directories by filtering the
|
||||
consensus status document received from the directory authorities. A
|
||||
hidden service directory is deemed responsible for all descriptor IDs in
|
||||
the interval from its direct predecessor, exclusive, to its own ID,
|
||||
inclusive; it further holds replicas for its 2 predecessors. A
|
||||
participant only trusts its own routing list and never learns about
|
||||
routing information from other parties.
|
||||
|
||||
Bob's OP publishes a new v2 descriptor once an hour or whenever its
|
||||
content changes. V2 descriptors can be found by clients within a given
|
||||
time period of 24 hours, after which they change their ID as described
|
||||
under 1.2. If a published descriptor would be valid for less than 60
|
||||
minutes (= 2 x 30 minutes to allow the server to be 30 minutes behind
|
||||
and the client 30 minutes ahead), Bob's OP publishes the descriptor
|
||||
under the ID of both, the current and the next publication period.
|
||||
|
||||
1.5. Alice receives a x.y.z.onion address.
|
||||
|
||||
When Alice receives a pointer to a location-hidden service, it is as a
|
||||
hostname of the form "z.onion" or "y.z.onion" or "x.y.z.onion", where
|
||||
z is a base-32 encoding of a 10-octet hash of Bob's service's public
|
||||
key, computed as follows:
|
||||
|
||||
1. Let H = H(PK).
|
||||
2. Let H' = the first 80 bits of H, considering each octet from
|
||||
most significant bit to least significant bit.
|
||||
2. Generate a 16-character encoding of H', using base32 as defined
|
||||
in RFC 3548.
|
||||
|
||||
(We only use 80 bits instead of the 160 bits from SHA1 because we
|
||||
don't need to worry about arbitrary collisions, and because it will
|
||||
make handling the url's more convenient.)
|
||||
|
||||
The string "x", if present, is the base-32 encoding of the
|
||||
authentication/authorization required by the introduction point.
|
||||
The string "y", if present, is the base-32 encoding of the
|
||||
authentication/authorization required by the hidden service.
|
||||
Omitting a string is taken to mean auth type [00 00].
|
||||
See section 2 of this document for details on auth mechanisms.
|
||||
|
||||
[Yes, numbers are allowed at the beginning. See RFC 1123. -NM]
|
||||
|
||||
1.6. Alice's OP retrieves a service descriptor.
|
||||
|
||||
Similarly to the description in section 1.4, Alice's OP fetches a v2
|
||||
descriptor from a randomly chosen hidden service directory out of the
|
||||
changing subset of 6 nodes. If the request is unsuccessful, Alice retries
|
||||
the other remaining responsible hidden service directories in a random
|
||||
order. Alice relies on Bob to care about a potential clock skew between
|
||||
the two by possibly storing two sets of descriptors (see end of section
|
||||
1.4).
|
||||
|
||||
Alice's OP opens a stream via Tor to the chosen v2 hidden service
|
||||
directory. (She may re-use old circuits for this.) Over this stream,
|
||||
Alice's OP makes an HTTP 'GET' request for the document
|
||||
"/tor/rendezvous2/<z>", where z is replaced with the encoding of the
|
||||
descriptor ID. The directory replies with a 404 HTTP response if it does
|
||||
not recognize <z>, and otherwise returns Bob's most recently uploaded
|
||||
service descriptor.
|
||||
|
||||
If Alice's OP receives a 404 response, it tries the other directory
|
||||
servers, and only fails the lookup if none recognize the public key hash.
|
||||
|
||||
Upon receiving a service descriptor, Alice verifies with the same process
|
||||
as the directory server uses, described above in section 1.4.
|
||||
|
||||
The directory server gives a 400 response if it cannot understand Alice's
|
||||
request.
|
||||
|
||||
Alice should cache the descriptor locally, but should not use
|
||||
descriptors that are more than 24 hours older than their timestamp.
|
||||
[Caching may make her partitionable, but she fetched it anonymously,
|
||||
and we can't very well *not* cache it. -RD]
|
||||
|
||||
1.7. Alice's OP establishes a rendezvous point.
|
||||
|
||||
When Alice requests a connection to a given location-hidden service,
|
||||
and Alice's OP does not have an established circuit to that service,
|
||||
the OP builds a rendezvous circuit. It does this by establishing
|
||||
a circuit to a randomly chosen OR, and sending a
|
||||
RELAY_ESTABLISH_RENDEZVOUS cell to that OR. The body of that cell
|
||||
contains:
|
||||
|
||||
RC Rendezvous cookie [20 octets]
|
||||
|
||||
[XXX011 this looks like an auth mechanism. should we generalize here? -RD]
|
||||
|
||||
The rendezvous cookie is an arbitrary 20-byte value, chosen randomly by
|
||||
Alice's OP.
|
||||
|
||||
Upon receiving a RELAY_ESTABLISH_RENDEZVOUS cell, the OR associates the
|
||||
RC with the circuit that sent it. It replies to Alice with an empty
|
||||
RELAY_RENDEZVOUS_ESTABLISHED cell to indicate success.
|
||||
|
||||
Alice's OP MUST NOT use the circuit which sent the cell for any purpose
|
||||
other than rendezvous with the given location-hidden service.
|
||||
|
||||
1.8. Introduction: from Alice's OP to Introduction Point
|
||||
|
||||
Alice builds a separate circuit to one of Bob's chosen introduction
|
||||
points, and sends it a RELAY_INTRODUCE1 cell containing:
|
||||
|
||||
Cleartext
|
||||
PK_ID Identifier for Bob's PK [20 octets]
|
||||
Encrypted to Bob's PK: (in the v0 intro protocol)
|
||||
RP Rendezvous point's nickname [20 octets]
|
||||
RC Rendezvous cookie [20 octets]
|
||||
g^x Diffie-Hellman data, part 1 [128 octets]
|
||||
OR (in the v1 intro protocol)
|
||||
VER Version byte: set to 1. [1 octet]
|
||||
RP Rendezvous point nick or ID [42 octets]
|
||||
RC Rendezvous cookie [20 octets]
|
||||
g^x Diffie-Hellman data, part 1 [128 octets]
|
||||
OR (in the v2 intro protocol)
|
||||
VER Version byte: set to 2. [1 octet]
|
||||
IP Rendezvous point's address [4 octets]
|
||||
PORT Rendezvous point's OR port [2 octets]
|
||||
ID Rendezvous point identity ID [20 octets]
|
||||
KLEN Length of onion key [2 octets]
|
||||
KEY Rendezvous point onion key [KLEN octets]
|
||||
RC Rendezvous cookie [20 octets]
|
||||
g^x Diffie-Hellman data, part 1 [128 octets]
|
||||
|
||||
PK_ID is the hash of Bob's public key. RP is NUL-padded and
|
||||
terminated. In version 0, it must contain a nickname. In version 1,
|
||||
it must contain EITHER a nickname or an identity key digest that is
|
||||
encoded in hex and prefixed with a '$'.
|
||||
|
||||
The hybrid encryption to Bob's PK works just like the hybrid
|
||||
encryption in CREATE cells (see tor-spec). Thus the payload of the
|
||||
version 0 RELAY_INTRODUCE1 cell on the wire will contain
|
||||
20+42+16+20+20+128=246 bytes, and the version 1 and version 2
|
||||
introduction formats have other sizes.
|
||||
|
||||
Through Tor 0.2.0.6-alpha, clients only generated the v0 introduction
|
||||
format, whereas hidden services have understood and accepted v0,
|
||||
v1, and v2 since 0.1.1.x. As of Tor 0.2.0.7-alpha and 0.1.2.18,
|
||||
clients switched to using the v2 intro format.
|
||||
|
||||
If Alice has downloaded a v2 descriptor, she uses the contained public
|
||||
key ("service-key") instead of Bob's public key to create the
|
||||
RELAY_INTRODUCE1 cell as described above.
|
||||
|
||||
1.8.1. Other introduction formats we don't use.
|
||||
|
||||
We briefly speculated about using the following format for the
|
||||
"encrypted to Bob's PK" part of the introduction, but no Tors have
|
||||
ever generated these.
|
||||
|
||||
VER Version byte: set to 3. [1 octet]
|
||||
ATYPE An address type (typically 4) [1 octet]
|
||||
ADDR Rendezvous point's IP address [4 or 16 octets]
|
||||
PORT Rendezvous point's OR port [2 octets]
|
||||
AUTHT The auth type that is supported [2 octets]
|
||||
AUTHL Length of auth data [1 octet]
|
||||
AUTHD Auth data [variable]
|
||||
ID Rendezvous point identity ID [20 octets]
|
||||
KLEN Length of onion key [2 octets]
|
||||
KEY Rendezvous point onion key [KLEN octets]
|
||||
RC Rendezvous cookie [20 octets]
|
||||
g^x Diffie-Hellman data, part 1 [128 octets]
|
||||
|
||||
1.9. Introduction: From the Introduction Point to Bob's OP
|
||||
|
||||
If the Introduction Point recognizes PK_ID as a public key which has
|
||||
established a circuit for introductions as in 1.3 above, it sends the body
|
||||
of the cell in a new RELAY_INTRODUCE2 cell down the corresponding circuit.
|
||||
(If the PK_ID is unrecognized, the RELAY_INTRODUCE1 cell is discarded.)
|
||||
|
||||
After sending the RELAY_INTRODUCE2 cell, the OR replies to Alice with an
|
||||
empty RELAY_COMMAND_INTRODUCE_ACK cell. If no RELAY_INTRODUCE2 cell can
|
||||
be sent, the OR replies to Alice with a non-empty cell to indicate an
|
||||
error. (The semantics of the cell body may be determined later; the
|
||||
current implementation sends a single '1' byte on failure.)
|
||||
|
||||
When Bob's OP receives the RELAY_INTRODUCE2 cell, it decrypts it with
|
||||
the private key for the corresponding hidden service, and extracts the
|
||||
rendezvous point's nickname, the rendezvous cookie, and the value of g^x
|
||||
chosen by Alice.
|
||||
|
||||
1.10. Rendezvous
|
||||
|
||||
Bob's OP builds a new Tor circuit ending at Alice's chosen rendezvous
|
||||
point, and sends a RELAY_RENDEZVOUS1 cell along this circuit, containing:
|
||||
RC Rendezvous cookie [20 octets]
|
||||
g^y Diffie-Hellman [128 octets]
|
||||
KH Handshake digest [20 octets]
|
||||
|
||||
(Bob's OP MUST NOT use this circuit for any other purpose.)
|
||||
|
||||
If the RP recognizes RC, it relays the rest of the cell down the
|
||||
corresponding circuit in a RELAY_RENDEZVOUS2 cell, containing:
|
||||
|
||||
g^y Diffie-Hellman [128 octets]
|
||||
KH Handshake digest [20 octets]
|
||||
|
||||
(If the RP does not recognize the RC, it discards the cell and
|
||||
tears down the circuit.)
|
||||
|
||||
When Alice's OP receives a RELAY_RENDEZVOUS2 cell on a circuit which
|
||||
has sent a RELAY_ESTABLISH_RENDEZVOUS cell but which has not yet received
|
||||
a reply, it uses g^y and H(g^xy) to complete the handshake as in the Tor
|
||||
circuit extend process: they establish a 60-octet string as
|
||||
K = SHA1(g^xy | [00]) | SHA1(g^xy | [01]) | SHA1(g^xy | [02])
|
||||
and generate
|
||||
KH = K[0..15]
|
||||
Kf = K[16..31]
|
||||
Kb = K[32..47]
|
||||
|
||||
Subsequently, the rendezvous point passes relay cells, unchanged, from
|
||||
each of the two circuits to the other. When Alice's OP sends
|
||||
RELAY cells along the circuit, it first encrypts them with the
|
||||
Kf, then with all of the keys for the ORs in Alice's side of the circuit;
|
||||
and when Alice's OP receives RELAY cells from the circuit, it decrypts
|
||||
them with the keys for the ORs in Alice's side of the circuit, then
|
||||
decrypts them with Kb. Bob's OP does the same, with Kf and Kb
|
||||
interchanged.
|
||||
|
||||
1.11. Creating streams
|
||||
|
||||
To open TCP connections to Bob's location-hidden service, Alice's OP sends
|
||||
a RELAY_BEGIN cell along the established circuit, using the special
|
||||
address "", and a chosen port. Bob's OP chooses a destination IP and
|
||||
port, based on the configuration of the service connected to the circuit,
|
||||
and opens a TCP stream. From then on, Bob's OP treats the stream as an
|
||||
ordinary exit connection.
|
||||
[ Except he doesn't include addr in the connected cell or the end
|
||||
cell. -RD]
|
||||
|
||||
Alice MAY send multiple RELAY_BEGIN cells along the circuit, to open
|
||||
multiple streams to Bob. Alice SHOULD NOT send RELAY_BEGIN cells for any
|
||||
other address along her circuit to Bob; if she does, Bob MUST reject them.
|
||||
|
||||
2. Authentication and authorization.
|
||||
|
||||
Foo.
|
||||
|
||||
3. Hidden service directory operation
|
||||
|
||||
This section has been introduced with the v2 hidden service descriptor
|
||||
format. It describes all operations of the v2 hidden service descriptor
|
||||
fetching and propagation mechanism that are required for the protocol
|
||||
described in section 1 to succeed with v2 hidden service descriptors.
|
||||
|
||||
3.1. Configuring as hidden service directory
|
||||
|
||||
Every onion router that has its directory port open can decide whether it
|
||||
wants to store and serve hidden service descriptors. An onion router which
|
||||
is configured as such includes the "hidden-service-dir" flag in its router
|
||||
descriptors that it sends to directory authorities.
|
||||
|
||||
The directory authorities include a new flag "HSDir" for routers that
|
||||
decided to provide storage for hidden service descriptors and that
|
||||
have been running for at least 24 hours.
|
||||
|
||||
3.2. Accepting publish requests
|
||||
|
||||
Hidden service directory nodes accept publish requests for v2 hidden service
|
||||
descriptors and store them to their local memory. (It is not necessary to
|
||||
make descriptors persistent, because after restarting, the onion router
|
||||
would not be accepted as a storing node anyway, because it has not been
|
||||
running for at least 24 hours.) All requests and replies are formatted as
|
||||
HTTP messages. Requests are initiated via BEGIN_DIR cells directed to
|
||||
the router's directory port, and formatted as HTTP POST requests to the URL
|
||||
"/tor/rendezvous2/publish" relative to the hidden service directory's root,
|
||||
containing as its body a v2 service descriptor.
|
||||
|
||||
A hidden service directory node parses every received descriptor and only
|
||||
stores it when it thinks that it is responsible for storing that descriptor
|
||||
based on its own routing table. See section 1.4 for more information on how
|
||||
to determine responsibility for a certain descriptor ID.
|
||||
|
||||
3.3. Processing fetch requests
|
||||
|
||||
Hidden service directory nodes process fetch requests for hidden service
|
||||
descriptors by looking them up in their local memory. (They do not need to
|
||||
determine if they are responsible for the passed ID, because it does no harm
|
||||
if they deliver a descriptor for which they are not (any more) responsible.)
|
||||
All requests and replies are formatted as HTTP messages. Requests are
|
||||
initiated via BEGIN_DIR cells directed to the router's directory port,
|
||||
and formatted as HTTP GET requests for the document "/tor/rendezvous2/<z>",
|
||||
where z is replaced with the encoding of the descriptor ID.
|
||||
|
78
orchid/doc/spec/socks-extensions.txt
Normal file
78
orchid/doc/spec/socks-extensions.txt
Normal file
@ -0,0 +1,78 @@
|
||||
Tor's extensions to the SOCKS protocol
|
||||
|
||||
1. Overview
|
||||
|
||||
The SOCKS protocol provides a generic interface for TCP proxies. Client
|
||||
software connects to a SOCKS server via TCP, and requests a TCP connection
|
||||
to another address and port. The SOCKS server establishes the connection,
|
||||
and reports success or failure to the client. After the connection has
|
||||
been established, the client application uses the TCP stream as usual.
|
||||
|
||||
Tor supports SOCKS4 as defined in [1], SOCKS4A as defined in [2], and
|
||||
SOCKS5 as defined in [3].
|
||||
|
||||
The stickiest issue for Tor in supporting clients, in practice, is forcing
|
||||
DNS lookups to occur at the OR side: if clients do their own DNS lookup,
|
||||
the DNS server can learn which addresses the client wants to reach.
|
||||
SOCKS4 supports addressing by IPv4 address; SOCKS4A is a kludge on top of
|
||||
SOCKS4 to allow addressing by hostname; SOCKS5 supports IPv4, IPv6, and
|
||||
hostnames.
|
||||
|
||||
1.1. Extent of support
|
||||
|
||||
Tor supports the SOCKS4, SOCKS4A, and SOCKS5 standards, except as follows:
|
||||
|
||||
BOTH:
|
||||
- The BIND command is not supported.
|
||||
|
||||
SOCKS4,4A:
|
||||
- SOCKS4 usernames are ignored.
|
||||
|
||||
SOCKS5:
|
||||
- The (SOCKS5) "UDP ASSOCIATE" command is not supported.
|
||||
- IPv6 is not supported in CONNECT commands.
|
||||
- Only the "NO AUTHENTICATION" (SOCKS5) authentication method [00] is
|
||||
supported.
|
||||
|
||||
2. Name lookup
|
||||
|
||||
As an extension to SOCKS4A and SOCKS5, Tor implements a new command value,
|
||||
"RESOLVE" [F0]. When Tor receives a "RESOLVE" SOCKS command, it initiates
|
||||
a remote lookup of the hostname provided as the target address in the SOCKS
|
||||
request. The reply is either an error (if the address couldn't be
|
||||
resolved) or a success response. In the case of success, the address is
|
||||
stored in the portion of the SOCKS response reserved for remote IP address.
|
||||
|
||||
(We support RESOLVE in SOCKS4 too, even though it is unnecessary.)
|
||||
|
||||
For SOCKS5 only, we support reverse resolution with a new command value,
|
||||
"RESOLVE_PTR" [F1]. In response to a "RESOLVE_PTR" SOCKS5 command with
|
||||
an IPv4 address as its target, Tor attempts to find the canonical
|
||||
hostname for that IPv4 record, and returns it in the "server bound
|
||||
address" portion of the reply.
|
||||
(This command was not supported before Tor 0.1.2.2-alpha.)
|
||||
|
||||
3. Other command extensions.
|
||||
|
||||
Tor 0.1.2.4-alpha added a new command value: "CONNECT_DIR" [F2].
|
||||
In this case, Tor will open an encrypted direct TCP connection to the
|
||||
directory port of the Tor server specified by address:port (the port
|
||||
specified should be the ORPort of the server). It uses a one-hop tunnel
|
||||
and a "BEGIN_DIR" relay cell to accomplish this secure connection.
|
||||
|
||||
The F2 command value was removed in Tor 0.2.0.10-alpha in favor of a
|
||||
new use_begindir flag in edge_connection_t.
|
||||
|
||||
4. HTTP-resistance
|
||||
|
||||
Tor checks the first byte of each SOCKS request to see whether it looks
|
||||
more like an HTTP request (that is, it starts with a "G", "H", or "P"). If
|
||||
so, Tor returns a small webpage, telling the user that his/her browser is
|
||||
misconfigured. This is helpful for the many users who mistakenly try to
|
||||
use Tor as an HTTP proxy instead of a SOCKS proxy.
|
||||
|
||||
References:
|
||||
[1] http://archive.socks.permeo.com/protocol/socks4.protocol
|
||||
[2] http://archive.socks.permeo.com/protocol/socks4a.protocol
|
||||
[3] SOCKS5: RFC1928
|
||||
|
992
orchid/doc/spec/tor-spec.txt
Normal file
992
orchid/doc/spec/tor-spec.txt
Normal file
@ -0,0 +1,992 @@
|
||||
|
||||
Tor Protocol Specification
|
||||
|
||||
Roger Dingledine
|
||||
Nick Mathewson
|
||||
|
||||
Note: This document aims to specify Tor as implemented in 0.2.1.x. Future
|
||||
versions of Tor may implement improved protocols, and compatibility is not
|
||||
guaranteed. Compatibility notes are given for versions 0.1.1.15-rc and
|
||||
later; earlier versions are not compatible with the Tor network as of this
|
||||
writing.
|
||||
|
||||
This specification is not a design document; most design criteria
|
||||
are not examined. For more information on why Tor acts as it does,
|
||||
see tor-design.pdf.
|
||||
|
||||
0. Preliminaries
|
||||
|
||||
0.1. Notation and encoding
|
||||
|
||||
PK -- a public key.
|
||||
SK -- a private key.
|
||||
K -- a key for a symmetric cypher.
|
||||
|
||||
a|b -- concatenation of 'a' and 'b'.
|
||||
|
||||
[A0 B1 C2] -- a three-byte sequence, containing the bytes with
|
||||
hexadecimal values A0, B1, and C2, in that order.
|
||||
|
||||
All numeric values are encoded in network (big-endian) order.
|
||||
|
||||
H(m) -- a cryptographic hash of m.
|
||||
|
||||
0.2. Security parameters
|
||||
|
||||
Tor uses a stream cipher, a public-key cipher, the Diffie-Hellman
|
||||
protocol, and a hash function.
|
||||
|
||||
KEY_LEN -- the length of the stream cipher's key, in bytes.
|
||||
|
||||
PK_ENC_LEN -- the length of a public-key encrypted message, in bytes.
|
||||
PK_PAD_LEN -- the number of bytes added in padding for public-key
|
||||
encryption, in bytes. (The largest number of bytes that can be encrypted
|
||||
in a single public-key operation is therefore PK_ENC_LEN-PK_PAD_LEN.)
|
||||
|
||||
DH_LEN -- the number of bytes used to represent a member of the
|
||||
Diffie-Hellman group.
|
||||
DH_SEC_LEN -- the number of bytes used in a Diffie-Hellman private key (x).
|
||||
|
||||
HASH_LEN -- the length of the hash function's output, in bytes.
|
||||
|
||||
PAYLOAD_LEN -- The longest allowable cell payload, in bytes. (509)
|
||||
|
||||
CELL_LEN -- The length of a Tor cell, in bytes.
|
||||
|
||||
0.3. Ciphers
|
||||
|
||||
For a stream cipher, we use 128-bit AES in counter mode, with an IV of all
|
||||
0 bytes.
|
||||
|
||||
For a public-key cipher, we use RSA with 1024-bit keys and a fixed
|
||||
exponent of 65537. We use OAEP-MGF1 padding, with SHA-1 as its digest
|
||||
function. We leave the optional "Label" parameter unset. (For OAEP
|
||||
padding, see ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf)
|
||||
|
||||
For Diffie-Hellman, we use a generator (g) of 2. For the modulus (p), we
|
||||
use the 1024-bit safe prime from rfc2409 section 6.2 whose hex
|
||||
representation is:
|
||||
|
||||
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08"
|
||||
"8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B"
|
||||
"302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9"
|
||||
"A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6"
|
||||
"49286651ECE65381FFFFFFFFFFFFFFFF"
|
||||
|
||||
As an optimization, implementations SHOULD choose DH private keys (x) of
|
||||
320 bits. Implementations that do this MUST never use any DH key more
|
||||
than once.
|
||||
[May other implementations reuse their DH keys?? -RD]
|
||||
[Probably not. Conceivably, you could get away with changing DH keys once
|
||||
per second, but there are too many oddball attacks for me to be
|
||||
comfortable that this is safe. -NM]
|
||||
|
||||
For a hash function, we use SHA-1.
|
||||
|
||||
KEY_LEN=16.
|
||||
DH_LEN=128; DH_SEC_LEN=40.
|
||||
PK_ENC_LEN=128; PK_PAD_LEN=42.
|
||||
HASH_LEN=20.
|
||||
|
||||
When we refer to "the hash of a public key", we mean the SHA-1 hash of the
|
||||
DER encoding of an ASN.1 RSA public key (as specified in PKCS.1).
|
||||
|
||||
All "random" values should be generated with a cryptographically strong
|
||||
random number generator, unless otherwise noted.
|
||||
|
||||
The "hybrid encryption" of a byte sequence M with a public key PK is
|
||||
computed as follows:
|
||||
1. If M is less than PK_ENC_LEN-PK_PAD_LEN, pad and encrypt M with PK.
|
||||
2. Otherwise, generate a KEY_LEN byte random key K.
|
||||
Let M1 = the first PK_ENC_LEN-PK_PAD_LEN-KEY_LEN bytes of M,
|
||||
and let M2 = the rest of M.
|
||||
Pad and encrypt K|M1 with PK. Encrypt M2 with our stream cipher,
|
||||
using the key K. Concatenate these encrypted values.
|
||||
[XXX Note that this "hybrid encryption" approach does not prevent
|
||||
an attacker from adding or removing bytes to the end of M. It also
|
||||
allows attackers to modify the bytes not covered by the OAEP --
|
||||
see Goldberg's PET2006 paper for details. We will add a MAC to this
|
||||
scheme one day. -RD]
|
||||
|
||||
0.4. Other parameter values
|
||||
|
||||
CELL_LEN=512
|
||||
|
||||
1. System overview
|
||||
|
||||
Tor is a distributed overlay network designed to anonymize
|
||||
low-latency TCP-based applications such as web browsing, secure shell,
|
||||
and instant messaging. Clients choose a path through the network and
|
||||
build a ``circuit'', in which each node (or ``onion router'' or ``OR'')
|
||||
in the path knows its predecessor and successor, but no other nodes in
|
||||
the circuit. Traffic flowing down the circuit is sent in fixed-size
|
||||
``cells'', which are unwrapped by a symmetric key at each node (like
|
||||
the layers of an onion) and relayed downstream.
|
||||
|
||||
1.1. Keys and names
|
||||
|
||||
Every Tor server has multiple public/private keypairs:
|
||||
|
||||
- A long-term signing-only "Identity key" used to sign documents and
|
||||
certificates, and used to establish server identity.
|
||||
- A medium-term "Onion key" used to decrypt onion skins when accepting
|
||||
circuit extend attempts. (See 5.1.) Old keys MUST be accepted for at
|
||||
least one week after they are no longer advertised. Because of this,
|
||||
servers MUST retain old keys for a while after they're rotated.
|
||||
- A short-term "Connection key" used to negotiate TLS connections.
|
||||
Tor implementations MAY rotate this key as often as they like, and
|
||||
SHOULD rotate this key at least once a day.
|
||||
|
||||
Tor servers are also identified by "nicknames"; these are specified in
|
||||
dir-spec.txt.
|
||||
|
||||
2. Connections
|
||||
|
||||
Connections between two Tor servers, or between a client and a server,
|
||||
use TLS/SSLv3 for link authentication and encryption. All
|
||||
implementations MUST support the SSLv3 ciphersuite
|
||||
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", and SHOULD support the TLS
|
||||
ciphersuite "TLS_DHE_RSA_WITH_AES_128_CBC_SHA" if it is available.
|
||||
|
||||
There are three acceptable ways to perform a TLS handshake when
|
||||
connecting to a Tor server: "certificates up-front", "renegotiation", and
|
||||
"backwards-compatible renegotiation". ("Backwards-compatible
|
||||
renegotiation" is, as the name implies, compatible with both other
|
||||
handshake types.)
|
||||
|
||||
Before Tor 0.2.0.21, only "certificates up-front" was supported. In Tor
|
||||
0.2.0.21 or later, "backwards-compatible renegotiation" is used.
|
||||
|
||||
In "certificates up-front", the connection initiator always sends a
|
||||
two-certificate chain, consisting of an X.509 certificate using a
|
||||
short-term connection public key and a second, self- signed X.509
|
||||
certificate containing its identity key. The other party sends a similar
|
||||
certificate chain. The initiator's ClientHello MUST NOT include any
|
||||
ciphersuites other than:
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
|
||||
SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA
|
||||
SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA
|
||||
|
||||
In "renegotiation", the connection initiator sends no certificates, and
|
||||
the responder sends a single connection certificate. Once the TLS
|
||||
handshake is complete, the initiator renegotiates the handshake, with each
|
||||
parties sending a two-certificate chain as in "certificates up-front".
|
||||
The initiator's ClientHello MUST include at least once ciphersuite not in
|
||||
the list above. The responder SHOULD NOT select any ciphersuite besides
|
||||
those in the list above.
|
||||
[The above "should not" is because some of the ciphers that
|
||||
clients list may be fake.]
|
||||
|
||||
In "backwards-compatible renegotiation", the connection initiator's
|
||||
ClientHello MUST include at least one ciphersuite other than those listed
|
||||
above. The connection responder examines the initiator's ciphersuite list
|
||||
to see whether it includes any ciphers other than those included in the
|
||||
list above. If extra ciphers are included, the responder proceeds as in
|
||||
"renegotiation": it sends a single certificate and does not request
|
||||
client certificates. Otherwise (in the case that no extra ciphersuites
|
||||
are included in the ClientHello) the responder proceeds as in
|
||||
"certificates up-front": it requests client certificates, and sends a
|
||||
two-certificate chain. In either case, once the responder has sent its
|
||||
certificate or certificates, the initiator counts them. If two
|
||||
certificates have been sent, it proceeds as in "certificates up-front";
|
||||
otherwise, it proceeds as in "renegotiation".
|
||||
|
||||
All new implementations of the Tor server protocol MUST support
|
||||
"backwards-compatible renegotiation"; clients SHOULD do this too. If
|
||||
this is not possible, new client implementations MUST support both
|
||||
"renegotiation" and "certificates up-front" and use the router's
|
||||
published link protocols list (see dir-spec.txt on the "protocols" entry)
|
||||
to decide which to use.
|
||||
|
||||
In all of the above handshake variants, certificates sent in the clear
|
||||
SHOULD NOT include any strings to identify the host as a Tor server. In
|
||||
the "renegotation" and "backwards-compatible renegotiation", the
|
||||
initiator SHOULD chose a list of ciphersuites and TLS extensions chosen
|
||||
to mimic one used by a popular web browser.
|
||||
|
||||
Responders MUST NOT select any TLS ciphersuite that lacks ephemeral keys,
|
||||
or whose symmetric keys are less then KEY_LEN bits, or whose digests are
|
||||
less than HASH_LEN bits. Responders SHOULD NOT select any SSLv3
|
||||
ciphersuite other than those listed above.
|
||||
|
||||
Even though the connection protocol is identical, we will think of the
|
||||
initiator as either an onion router (OR) if it is willing to relay
|
||||
traffic for other Tor users, or an onion proxy (OP) if it only handles
|
||||
local requests. Onion proxies SHOULD NOT provide long-term-trackable
|
||||
identifiers in their handshakes.
|
||||
|
||||
In all handshake variants, once all certificates are exchanged, all
|
||||
parties receiving certificates must confirm that the identity key is as
|
||||
expected. (When initiating a connection, the expected identity key is
|
||||
the one given in the directory; when creating a connection because of an
|
||||
EXTEND cell, the expected identity key is the one given in the cell.) If
|
||||
the key is not as expected, the party must close the connection.
|
||||
|
||||
When connecting to an OR, all parties SHOULD reject the connection if that
|
||||
OR has a malformed or missing certificate. When accepting an incoming
|
||||
connection, an OR SHOULD NOT reject incoming connections from parties with
|
||||
malformed or missing certificates. (However, an OR should not believe
|
||||
that an incoming connection is from another OR unless the certificates
|
||||
are present and well-formed.)
|
||||
|
||||
[Before version 0.1.2.8-rc, ORs rejected incoming connections from ORs and
|
||||
OPs alike if their certificates were missing or malformed.]
|
||||
|
||||
Once a TLS connection is established, the two sides send cells
|
||||
(specified below) to one another. Cells are sent serially. All
|
||||
cells are CELL_LEN bytes long. Cells may be sent embedded in TLS
|
||||
records of any size or divided across TLS records, but the framing
|
||||
of TLS records MUST NOT leak information about the type or contents
|
||||
of the cells.
|
||||
|
||||
TLS connections are not permanent. Either side MAY close a connection
|
||||
if there are no circuits running over it and an amount of time
|
||||
(KeepalivePeriod, defaults to 5 minutes) has passed since the last time
|
||||
any traffic was transmitted over the TLS connection. Clients SHOULD
|
||||
also hold a TLS connection with no circuits open, if it is likely that a
|
||||
circuit will be built soon using that connection.
|
||||
|
||||
(As an exception, directory servers may try to stay connected to all of
|
||||
the ORs -- though this will be phased out for the Tor 0.1.2.x release.)
|
||||
|
||||
To avoid being trivially distinguished from servers, client-only Tor
|
||||
instances are encouraged but not required to use a two-certificate chain
|
||||
as well. Clients SHOULD NOT keep using the same certificates when
|
||||
their IP address changes. Clients MAY send no certificates at all.
|
||||
|
||||
3. Cell Packet format
|
||||
|
||||
The basic unit of communication for onion routers and onion
|
||||
proxies is a fixed-width "cell".
|
||||
|
||||
On a version 1 connection, each cell contains the following
|
||||
fields:
|
||||
|
||||
CircID [2 bytes]
|
||||
Command [1 byte]
|
||||
Payload (padded with 0 bytes) [PAYLOAD_LEN bytes]
|
||||
|
||||
On a version 2 connection, all cells are as in version 1 connections,
|
||||
except for the initial VERSIONS cell, whose format is:
|
||||
|
||||
Circuit [2 octets; set to 0]
|
||||
Command [1 octet; set to 7 for VERSIONS]
|
||||
Length [2 octets; big-endian integer]
|
||||
Payload [Length bytes]
|
||||
|
||||
The CircID field determines which circuit, if any, the cell is
|
||||
associated with.
|
||||
|
||||
The 'Command' field holds one of the following values:
|
||||
0 -- PADDING (Padding) (See Sec 7.2)
|
||||
1 -- CREATE (Create a circuit) (See Sec 5.1)
|
||||
2 -- CREATED (Acknowledge create) (See Sec 5.1)
|
||||
3 -- RELAY (End-to-end data) (See Sec 5.5 and 6)
|
||||
4 -- DESTROY (Stop using a circuit) (See Sec 5.4)
|
||||
5 -- CREATE_FAST (Create a circuit, no PK) (See Sec 5.1)
|
||||
6 -- CREATED_FAST (Circuit created, no PK) (See Sec 5.1)
|
||||
7 -- VERSIONS (Negotiate proto version) (See Sec 4)
|
||||
8 -- NETINFO (Time and address info) (See Sec 4)
|
||||
9 -- RELAY_EARLY (End-to-end data; limited) (See sec 5.6)
|
||||
|
||||
The interpretation of 'Payload' depends on the type of the cell.
|
||||
PADDING: Payload is unused.
|
||||
CREATE: Payload contains the handshake challenge.
|
||||
CREATED: Payload contains the handshake response.
|
||||
RELAY: Payload contains the relay header and relay body.
|
||||
DESTROY: Payload contains a reason for closing the circuit.
|
||||
(see 5.4)
|
||||
Upon receiving any other value for the command field, an OR must
|
||||
drop the cell. Since more cell types may be added in the future, ORs
|
||||
should generally not warn when encountering unrecognized commands.
|
||||
|
||||
The payload is padded with 0 bytes.
|
||||
|
||||
PADDING cells are currently used to implement connection keepalive.
|
||||
If there is no other traffic, ORs and OPs send one another a PADDING
|
||||
cell every few minutes.
|
||||
|
||||
CREATE, CREATED, and DESTROY cells are used to manage circuits;
|
||||
see section 5 below.
|
||||
|
||||
RELAY cells are used to send commands and data along a circuit; see
|
||||
section 6 below.
|
||||
|
||||
VERSIONS and NETINFO cells are used to set up connections. See section 4
|
||||
below.
|
||||
|
||||
4. Negotiating and initializing connections
|
||||
|
||||
4.1. Negotiating versions with VERSIONS cells
|
||||
|
||||
There are multiple instances of the Tor link connection protocol. Any
|
||||
connection negotiated using the "certificates up front" handshake (see
|
||||
section 2 above) is "version 1". In any connection where both parties
|
||||
have behaved as in the "renegotiation" handshake, the link protocol
|
||||
version is 2 or higher.
|
||||
|
||||
To determine the version, in any connection where the "renegotiation"
|
||||
handshake was used (that is, where the server sent only one certificate
|
||||
at first and where the client did not send any certificates until
|
||||
renegotiation), both parties MUST send a VERSIONS cell immediately after
|
||||
the renegotiation is finished, before any other cells are sent. Parties
|
||||
MUST NOT send any other cells on a connection until they have received a
|
||||
VERSIONS cell.
|
||||
|
||||
The payload in a VERSIONS cell is a series of big-endian two-byte
|
||||
integers. Both parties MUST select as the link protocol version the
|
||||
highest number contained both in the VERSIONS cell they sent and in the
|
||||
versions cell they received. If they have no such version in common,
|
||||
they cannot communicate and MUST close the connection.
|
||||
|
||||
Since the version 1 link protocol does not use the "renegotiation"
|
||||
handshake, implementations MUST NOT list version 1 in their VERSIONS
|
||||
cell.
|
||||
|
||||
4.2. NETINFO cells
|
||||
|
||||
If version 2 or higher is negotiated, each party sends the other a
|
||||
NETINFO cell. The cell's payload is:
|
||||
|
||||
Timestamp [4 bytes]
|
||||
Other OR's address [variable]
|
||||
Number of addresses [1 byte]
|
||||
This OR's addresses [variable]
|
||||
|
||||
The address format is a type/length/value sequence as given in section
|
||||
6.4 below. The timestamp is a big-endian unsigned integer number of
|
||||
seconds since the unix epoch.
|
||||
|
||||
Implementations MAY use the timestamp value to help decide if their
|
||||
clocks are skewed. Initiators MAY use "other OR's address" to help
|
||||
learn which address their connections are originating from, if they do
|
||||
not know it. Initiators SHOULD use "this OR's address" to make sure
|
||||
that they have connected to another OR at its canonical address.
|
||||
|
||||
[As of 0.2.0.23-rc, implementations use none of the above values.]
|
||||
|
||||
|
||||
5. Circuit management
|
||||
|
||||
5.1. CREATE and CREATED cells
|
||||
|
||||
Users set up circuits incrementally, one hop at a time. To create a
|
||||
new circuit, OPs send a CREATE cell to the first node, with the
|
||||
first half of the DH handshake; that node responds with a CREATED
|
||||
cell with the second half of the DH handshake plus the first 20 bytes
|
||||
of derivative key data (see section 5.2). To extend a circuit past
|
||||
the first hop, the OP sends an EXTEND relay cell (see section 5)
|
||||
which instructs the last node in the circuit to send a CREATE cell
|
||||
to extend the circuit.
|
||||
|
||||
The payload for a CREATE cell is an 'onion skin', which consists
|
||||
of the first step of the DH handshake data (also known as g^x).
|
||||
This value is hybrid-encrypted (see 0.3) to Bob's onion key, giving
|
||||
an onion-skin of:
|
||||
PK-encrypted:
|
||||
Padding [PK_PAD_LEN bytes]
|
||||
Symmetric key [KEY_LEN bytes]
|
||||
First part of g^x [PK_ENC_LEN-PK_PAD_LEN-KEY_LEN bytes]
|
||||
Symmetrically encrypted:
|
||||
Second part of g^x [DH_LEN-(PK_ENC_LEN-PK_PAD_LEN-KEY_LEN)
|
||||
bytes]
|
||||
|
||||
The relay payload for an EXTEND relay cell consists of:
|
||||
Address [4 bytes]
|
||||
Port [2 bytes]
|
||||
Onion skin [DH_LEN+KEY_LEN+PK_PAD_LEN bytes]
|
||||
Identity fingerprint [HASH_LEN bytes]
|
||||
|
||||
The port and address field denote the IPV4 address and port of the next
|
||||
onion router in the circuit; the public key hash is the hash of the PKCS#1
|
||||
ASN1 encoding of the next onion router's identity (signing) key. (See 0.3
|
||||
above.) Including this hash allows the extending OR verify that it is
|
||||
indeed connected to the correct target OR, and prevents certain
|
||||
man-in-the-middle attacks.
|
||||
|
||||
The payload for a CREATED cell, or the relay payload for an
|
||||
EXTENDED cell, contains:
|
||||
DH data (g^y) [DH_LEN bytes]
|
||||
Derivative key data (KH) [HASH_LEN bytes] <see 5.2 below>
|
||||
|
||||
The CircID for a CREATE cell is an arbitrarily chosen 2-byte integer,
|
||||
selected by the node (OP or OR) that sends the CREATE cell. To prevent
|
||||
CircID collisions, when one node sends a CREATE cell to another, it chooses
|
||||
from only one half of the possible values based on the ORs' public
|
||||
identity keys: if the sending node has a lower key, it chooses a CircID with
|
||||
an MSB of 0; otherwise, it chooses a CircID with an MSB of 1.
|
||||
|
||||
(An OP with no public key MAY choose any CircID it wishes, since an OP
|
||||
never needs to process a CREATE cell.)
|
||||
|
||||
Public keys are compared numerically by modulus.
|
||||
|
||||
As usual with DH, x and y MUST be generated randomly.
|
||||
|
||||
5.1.1. CREATE_FAST/CREATED_FAST cells
|
||||
|
||||
When initializing the first hop of a circuit, the OP has already
|
||||
established the OR's identity and negotiated a secret key using TLS.
|
||||
Because of this, it is not always necessary for the OP to perform the
|
||||
public key operations to create a circuit. In this case, the
|
||||
OP MAY send a CREATE_FAST cell instead of a CREATE cell for the first
|
||||
hop only. The OR responds with a CREATED_FAST cell, and the circuit is
|
||||
created.
|
||||
|
||||
A CREATE_FAST cell contains:
|
||||
|
||||
Key material (X) [HASH_LEN bytes]
|
||||
|
||||
A CREATED_FAST cell contains:
|
||||
|
||||
Key material (Y) [HASH_LEN bytes]
|
||||
Derivative key data [HASH_LEN bytes] (See 5.2 below)
|
||||
|
||||
The values of X and Y must be generated randomly.
|
||||
|
||||
If an OR sees a circuit created with CREATE_FAST, the OR is sure to be the
|
||||
first hop of a circuit. ORs SHOULD reject attempts to create streams with
|
||||
RELAY_BEGIN exiting the circuit at the first hop: letting Tor be used as a
|
||||
single hop proxy makes exit nodes a more attractive target for compromise.
|
||||
|
||||
5.2. Setting circuit keys
|
||||
|
||||
Once the handshake between the OP and an OR is completed, both can
|
||||
now calculate g^xy with ordinary DH. Before computing g^xy, both client
|
||||
and server MUST verify that the received g^x or g^y value is not degenerate;
|
||||
that is, it must be strictly greater than 1 and strictly less than p-1
|
||||
where p is the DH modulus. Implementations MUST NOT complete a handshake
|
||||
with degenerate keys. Implementations MUST NOT discard other "weak"
|
||||
g^x values.
|
||||
|
||||
(Discarding degenerate keys is critical for security; if bad keys
|
||||
are not discarded, an attacker can substitute the server's CREATED
|
||||
cell's g^y with 0 or 1, thus creating a known g^xy and impersonating
|
||||
the server. Discarding other keys may allow attacks to learn bits of
|
||||
the private key.)
|
||||
|
||||
If CREATE or EXTEND is used to extend a circuit, the client and server
|
||||
base their key material on K0=g^xy, represented as a big-endian unsigned
|
||||
integer.
|
||||
|
||||
If CREATE_FAST is used, the client and server base their key material on
|
||||
K0=X|Y.
|
||||
|
||||
From the base key material K0, they compute KEY_LEN*2+HASH_LEN*3 bytes of
|
||||
derivative key data as
|
||||
K = H(K0 | [00]) | H(K0 | [01]) | H(K0 | [02]) | ...
|
||||
|
||||
The first HASH_LEN bytes of K form KH; the next HASH_LEN form the forward
|
||||
digest Df; the next HASH_LEN 41-60 form the backward digest Db; the next
|
||||
KEY_LEN 61-76 form Kf, and the final KEY_LEN form Kb. Excess bytes from K
|
||||
are discarded.
|
||||
|
||||
KH is used in the handshake response to demonstrate knowledge of the
|
||||
computed shared key. Df is used to seed the integrity-checking hash
|
||||
for the stream of data going from the OP to the OR, and Db seeds the
|
||||
integrity-checking hash for the data stream from the OR to the OP. Kf
|
||||
is used to encrypt the stream of data going from the OP to the OR, and
|
||||
Kb is used to encrypt the stream of data going from the OR to the OP.
|
||||
|
||||
5.3. Creating circuits
|
||||
|
||||
When creating a circuit through the network, the circuit creator
|
||||
(OP) performs the following steps:
|
||||
|
||||
1. Choose an onion router as an exit node (R_N), such that the onion
|
||||
router's exit policy includes at least one pending stream that
|
||||
needs a circuit (if there are any).
|
||||
|
||||
2. Choose a chain of (N-1) onion routers
|
||||
(R_1...R_N-1) to constitute the path, such that no router
|
||||
appears in the path twice.
|
||||
|
||||
3. If not already connected to the first router in the chain,
|
||||
open a new connection to that router.
|
||||
|
||||
4. Choose a circID not already in use on the connection with the
|
||||
first router in the chain; send a CREATE cell along the
|
||||
connection, to be received by the first onion router.
|
||||
|
||||
5. Wait until a CREATED cell is received; finish the handshake
|
||||
and extract the forward key Kf_1 and the backward key Kb_1.
|
||||
|
||||
6. For each subsequent onion router R (R_2 through R_N), extend
|
||||
the circuit to R.
|
||||
|
||||
To extend the circuit by a single onion router R_M, the OP performs
|
||||
these steps:
|
||||
|
||||
1. Create an onion skin, encrypted to R_M's public onion key.
|
||||
|
||||
2. Send the onion skin in a relay EXTEND cell along
|
||||
the circuit (see section 5).
|
||||
|
||||
3. When a relay EXTENDED cell is received, verify KH, and
|
||||
calculate the shared keys. The circuit is now extended.
|
||||
|
||||
When an onion router receives an EXTEND relay cell, it sends a CREATE
|
||||
cell to the next onion router, with the enclosed onion skin as its
|
||||
payload. As special cases, if the extend cell includes a digest of
|
||||
all zeroes, or asks to extend back to the relay that sent the extend
|
||||
cell, the circuit will fail and be torn down. The initiating onion
|
||||
router chooses some circID not yet used on the connection between the
|
||||
two onion routers. (But see section 5.1. above, concerning choosing
|
||||
circIDs based on lexicographic order of nicknames.)
|
||||
|
||||
When an onion router receives a CREATE cell, if it already has a
|
||||
circuit on the given connection with the given circID, it drops the
|
||||
cell. Otherwise, after receiving the CREATE cell, it completes the
|
||||
DH handshake, and replies with a CREATED cell. Upon receiving a
|
||||
CREATED cell, an onion router packs it payload into an EXTENDED relay
|
||||
cell (see section 5), and sends that cell up the circuit. Upon
|
||||
receiving the EXTENDED relay cell, the OP can retrieve g^y.
|
||||
|
||||
(As an optimization, OR implementations may delay processing onions
|
||||
until a break in traffic allows time to do so without harming
|
||||
network latency too greatly.)
|
||||
|
||||
5.3.1. Canonical connections
|
||||
|
||||
It is possible for an attacker to launch a man-in-the-middle attack
|
||||
against a connection by telling OR Alice to extend to OR Bob at some
|
||||
address X controlled by the attacker. The attacker cannot read the
|
||||
encrypted traffic, but the attacker is now in a position to count all
|
||||
bytes sent between Alice and Bob (assuming Alice was not already
|
||||
connected to Bob.)
|
||||
|
||||
To prevent this, when an OR we gets an extend request, it SHOULD use an
|
||||
existing OR connection if the ID matches, and ANY of the following
|
||||
conditions hold:
|
||||
- The IP matches the requested IP.
|
||||
- The OR knows that the IP of the connection it's using is canonical
|
||||
because it was listed in the NETINFO cell.
|
||||
- The OR knows that the IP of the connection it's using is canonical
|
||||
because it was listed in the server descriptor.
|
||||
|
||||
[This is not implemented in Tor 0.2.0.23-rc.]
|
||||
|
||||
5.4. Tearing down circuits
|
||||
|
||||
Circuits are torn down when an unrecoverable error occurs along
|
||||
the circuit, or when all streams on a circuit are closed and the
|
||||
circuit's intended lifetime is over. Circuits may be torn down
|
||||
either completely or hop-by-hop.
|
||||
|
||||
To tear down a circuit completely, an OR or OP sends a DESTROY
|
||||
cell to the adjacent nodes on that circuit, using the appropriate
|
||||
direction's circID.
|
||||
|
||||
Upon receiving an outgoing DESTROY cell, an OR frees resources
|
||||
associated with the corresponding circuit. If it's not the end of
|
||||
the circuit, it sends a DESTROY cell for that circuit to the next OR
|
||||
in the circuit. If the node is the end of the circuit, then it tears
|
||||
down any associated edge connections (see section 6.1).
|
||||
|
||||
After a DESTROY cell has been processed, an OR ignores all data or
|
||||
destroy cells for the corresponding circuit.
|
||||
|
||||
To tear down part of a circuit, the OP may send a RELAY_TRUNCATE cell
|
||||
signaling a given OR (Stream ID zero). That OR sends a DESTROY
|
||||
cell to the next node in the circuit, and replies to the OP with a
|
||||
RELAY_TRUNCATED cell.
|
||||
|
||||
When an unrecoverable error occurs along one connection in a
|
||||
circuit, the nodes on either side of the connection should, if they
|
||||
are able, act as follows: the node closer to the OP should send a
|
||||
RELAY_TRUNCATED cell towards the OP; the node farther from the OP
|
||||
should send a DESTROY cell down the circuit.
|
||||
|
||||
The payload of a RELAY_TRUNCATED or DESTROY cell contains a single octet,
|
||||
describing why the circuit is being closed or truncated. When sending a
|
||||
TRUNCATED or DESTROY cell because of another TRUNCATED or DESTROY cell,
|
||||
the error code should be propagated. The origin of a circuit always sets
|
||||
this error code to 0, to avoid leaking its version.
|
||||
|
||||
The error codes are:
|
||||
0 -- NONE (No reason given.)
|
||||
1 -- PROTOCOL (Tor protocol violation.)
|
||||
2 -- INTERNAL (Internal error.)
|
||||
3 -- REQUESTED (A client sent a TRUNCATE command.)
|
||||
4 -- HIBERNATING (Not currently operating; trying to save bandwidth.)
|
||||
5 -- RESOURCELIMIT (Out of memory, sockets, or circuit IDs.)
|
||||
6 -- CONNECTFAILED (Unable to reach server.)
|
||||
7 -- OR_IDENTITY (Connected to server, but its OR identity was not
|
||||
as expected.)
|
||||
8 -- OR_CONN_CLOSED (The OR connection that was carrying this circuit
|
||||
died.)
|
||||
9 -- FINISHED (The circuit has expired for being dirty or old.)
|
||||
10 -- TIMEOUT (Circuit construction took too long)
|
||||
11 -- DESTROYED (The circuit was destroyed w/o client TRUNCATE)
|
||||
12 -- NOSUCHSERVICE (Request for unknown hidden service)
|
||||
|
||||
5.5. Routing relay cells
|
||||
|
||||
When an OR receives a RELAY or RELAY_EARLY cell, it checks the cell's
|
||||
circID and determines whether it has a corresponding circuit along that
|
||||
connection. If not, the OR drops the cell.
|
||||
|
||||
Otherwise, if the OR is not at the OP edge of the circuit (that is,
|
||||
either an 'exit node' or a non-edge node), it de/encrypts the payload
|
||||
with the stream cipher, as follows:
|
||||
'Forward' relay cell (same direction as CREATE):
|
||||
Use Kf as key; decrypt.
|
||||
'Back' relay cell (opposite direction from CREATE):
|
||||
Use Kb as key; encrypt.
|
||||
Note that in counter mode, decrypt and encrypt are the same operation.
|
||||
|
||||
The OR then decides whether it recognizes the relay cell, by
|
||||
inspecting the payload as described in section 6.1 below. If the OR
|
||||
recognizes the cell, it processes the contents of the relay cell.
|
||||
Otherwise, it passes the decrypted relay cell along the circuit if
|
||||
the circuit continues. If the OR at the end of the circuit
|
||||
encounters an unrecognized relay cell, an error has occurred: the OR
|
||||
sends a DESTROY cell to tear down the circuit.
|
||||
|
||||
When a relay cell arrives at an OP, the OP decrypts the payload
|
||||
with the stream cipher as follows:
|
||||
OP receives data cell:
|
||||
For I=N...1,
|
||||
Decrypt with Kb_I. If the payload is recognized (see
|
||||
section 6..1), then stop and process the payload.
|
||||
|
||||
For more information, see section 6 below.
|
||||
|
||||
5.6. Handling relay_early cells
|
||||
|
||||
A RELAY_EARLY cell is designed to limit the length any circuit can reach.
|
||||
When an OR receives a RELAY_EARLY cell, and the next node in the circuit
|
||||
is speaking v2 of the link protocol or later, the OR relays the cell as a
|
||||
RELAY_EARLY cell. Otherwise, it relays it as a RELAY cell.
|
||||
|
||||
If a node ever receives more than 8 RELAY_EARLY cells on a given
|
||||
outbound circuit, it SHOULD close the circuit. (For historical reasons,
|
||||
we don't limit the number of inbound RELAY_EARLY cells; they should
|
||||
be harmless anyway because clients won't accept extend requests. See
|
||||
bug 1038.)
|
||||
|
||||
When speaking v2 of the link protocol or later, clients MUST only send
|
||||
EXTEND cells inside RELAY_EARLY cells. Clients SHOULD send the first ~8
|
||||
RELAY cells that are not targeted at the first hop of any circuit as
|
||||
RELAY_EARLY cells too, in order to partially conceal the circuit length.
|
||||
|
||||
[In a future version of Tor, servers will reject any EXTEND cell not
|
||||
received in a RELAY_EARLY cell. See proposal 110.]
|
||||
|
||||
6. Application connections and stream management
|
||||
|
||||
6.1. Relay cells
|
||||
|
||||
Within a circuit, the OP and the exit node use the contents of
|
||||
RELAY packets to tunnel end-to-end commands and TCP connections
|
||||
("Streams") across circuits. End-to-end commands can be initiated
|
||||
by either edge; streams are initiated by the OP.
|
||||
|
||||
The payload of each unencrypted RELAY cell consists of:
|
||||
Relay command [1 byte]
|
||||
'Recognized' [2 bytes]
|
||||
StreamID [2 bytes]
|
||||
Digest [4 bytes]
|
||||
Length [2 bytes]
|
||||
Data [CELL_LEN-14 bytes]
|
||||
|
||||
The relay commands are:
|
||||
1 -- RELAY_BEGIN [forward]
|
||||
2 -- RELAY_DATA [forward or backward]
|
||||
3 -- RELAY_END [forward or backward]
|
||||
4 -- RELAY_CONNECTED [backward]
|
||||
5 -- RELAY_SENDME [forward or backward] [sometimes control]
|
||||
6 -- RELAY_EXTEND [forward] [control]
|
||||
7 -- RELAY_EXTENDED [backward] [control]
|
||||
8 -- RELAY_TRUNCATE [forward] [control]
|
||||
9 -- RELAY_TRUNCATED [backward] [control]
|
||||
10 -- RELAY_DROP [forward or backward] [control]
|
||||
11 -- RELAY_RESOLVE [forward]
|
||||
12 -- RELAY_RESOLVED [backward]
|
||||
13 -- RELAY_BEGIN_DIR [forward]
|
||||
|
||||
32..40 -- Used for hidden services; see rend-spec.txt.
|
||||
|
||||
Commands labelled as "forward" must only be sent by the originator
|
||||
of the circuit. Commands labelled as "backward" must only be sent by
|
||||
other nodes in the circuit back to the originator. Commands marked
|
||||
as either can be sent either by the originator or other nodes.
|
||||
|
||||
The 'recognized' field in any unencrypted relay payload is always set
|
||||
to zero; the 'digest' field is computed as the first four bytes of
|
||||
the running digest of all the bytes that have been destined for
|
||||
this hop of the circuit or originated from this hop of the circuit,
|
||||
seeded from Df or Db respectively (obtained in section 5.2 above),
|
||||
and including this RELAY cell's entire payload (taken with the digest
|
||||
field set to zero).
|
||||
|
||||
When the 'recognized' field of a RELAY cell is zero, and the digest
|
||||
is correct, the cell is considered "recognized" for the purposes of
|
||||
decryption (see section 5.5 above).
|
||||
|
||||
(The digest does not include any bytes from relay cells that do
|
||||
not start or end at this hop of the circuit. That is, it does not
|
||||
include forwarded data. Therefore if 'recognized' is zero but the
|
||||
digest does not match, the running digest at that node should
|
||||
not be updated, and the cell should be forwarded on.)
|
||||
|
||||
All RELAY cells pertaining to the same tunneled stream have the
|
||||
same stream ID. StreamIDs are chosen arbitrarily by the OP. RELAY
|
||||
cells that affect the entire circuit rather than a particular
|
||||
stream use a StreamID of zero -- they are marked in the table above
|
||||
as "[control]" style cells. (Sendme cells are marked as "sometimes
|
||||
control" because they can take include a StreamID or not depending
|
||||
on their purpose -- see Section 7.)
|
||||
|
||||
The 'Length' field of a relay cell contains the number of bytes in
|
||||
the relay payload which contain real payload data. The remainder of
|
||||
the payload is padded with NUL bytes.
|
||||
|
||||
If the RELAY cell is recognized but the relay command is not
|
||||
understood, the cell must be dropped and ignored. Its contents
|
||||
still count with respect to the digests, though.
|
||||
|
||||
6.2. Opening streams and transferring data
|
||||
|
||||
To open a new anonymized TCP connection, the OP chooses an open
|
||||
circuit to an exit that may be able to connect to the destination
|
||||
address, selects an arbitrary StreamID not yet used on that circuit,
|
||||
and constructs a RELAY_BEGIN cell with a payload encoding the address
|
||||
and port of the destination host. The payload format is:
|
||||
|
||||
ADDRESS | ':' | PORT | [00]
|
||||
|
||||
where ADDRESS can be a DNS hostname, or an IPv4 address in
|
||||
dotted-quad format, or an IPv6 address surrounded by square brackets;
|
||||
and where PORT is a decimal integer between 1 and 65535, inclusive.
|
||||
|
||||
[What is the [00] for? -NM]
|
||||
[It's so the payload is easy to parse out with string funcs -RD]
|
||||
|
||||
Upon receiving this cell, the exit node resolves the address as
|
||||
necessary, and opens a new TCP connection to the target port. If the
|
||||
address cannot be resolved, or a connection can't be established, the
|
||||
exit node replies with a RELAY_END cell. (See 6.4 below.)
|
||||
Otherwise, the exit node replies with a RELAY_CONNECTED cell, whose
|
||||
payload is in one of the following formats:
|
||||
The IPv4 address to which the connection was made [4 octets]
|
||||
A number of seconds (TTL) for which the address may be cached [4 octets]
|
||||
or
|
||||
Four zero-valued octets [4 octets]
|
||||
An address type (6) [1 octet]
|
||||
The IPv6 address to which the connection was made [16 octets]
|
||||
A number of seconds (TTL) for which the address may be cached [4 octets]
|
||||
[XXXX No version of Tor currently generates the IPv6 format.]
|
||||
|
||||
[Tor servers before 0.1.2.0 set the TTL field to a fixed value. Later
|
||||
versions set the TTL to the last value seen from a DNS server, and expire
|
||||
their own cached entries after a fixed interval. This prevents certain
|
||||
attacks.]
|
||||
|
||||
The OP waits for a RELAY_CONNECTED cell before sending any data.
|
||||
Once a connection has been established, the OP and exit node
|
||||
package stream data in RELAY_DATA cells, and upon receiving such
|
||||
cells, echo their contents to the corresponding TCP stream.
|
||||
RELAY_DATA cells sent to unrecognized streams are dropped.
|
||||
|
||||
Relay RELAY_DROP cells are long-range dummies; upon receiving such
|
||||
a cell, the OR or OP must drop it.
|
||||
|
||||
6.2.1. Opening a directory stream
|
||||
|
||||
If a Tor server is a directory server, it should respond to a
|
||||
RELAY_BEGIN_DIR cell as if it had received a BEGIN cell requesting a
|
||||
connection to its directory port. RELAY_BEGIN_DIR cells ignore exit
|
||||
policy, since the stream is local to the Tor process.
|
||||
|
||||
If the Tor server is not running a directory service, it should respond
|
||||
with a REASON_NOTDIRECTORY RELAY_END cell.
|
||||
|
||||
Clients MUST generate an all-zero payload for RELAY_BEGIN_DIR cells,
|
||||
and servers MUST ignore the payload.
|
||||
|
||||
[RELAY_BEGIN_DIR was not supported before Tor 0.1.2.2-alpha; clients
|
||||
SHOULD NOT send it to routers running earlier versions of Tor.]
|
||||
|
||||
6.3. Closing streams
|
||||
|
||||
When an anonymized TCP connection is closed, or an edge node
|
||||
encounters error on any stream, it sends a 'RELAY_END' cell along the
|
||||
circuit (if possible) and closes the TCP connection immediately. If
|
||||
an edge node receives a 'RELAY_END' cell for any stream, it closes
|
||||
the TCP connection completely, and sends nothing more along the
|
||||
circuit for that stream.
|
||||
|
||||
The payload of a RELAY_END cell begins with a single 'reason' byte to
|
||||
describe why the stream is closing, plus optional data (depending on
|
||||
the reason.) The values are:
|
||||
|
||||
1 -- REASON_MISC (catch-all for unlisted reasons)
|
||||
2 -- REASON_RESOLVEFAILED (couldn't look up hostname)
|
||||
3 -- REASON_CONNECTREFUSED (remote host refused connection) [*]
|
||||
4 -- REASON_EXITPOLICY (OR refuses to connect to host or port)
|
||||
5 -- REASON_DESTROY (Circuit is being destroyed)
|
||||
6 -- REASON_DONE (Anonymized TCP connection was closed)
|
||||
7 -- REASON_TIMEOUT (Connection timed out, or OR timed out
|
||||
while connecting)
|
||||
8 -- (unallocated) [**]
|
||||
9 -- REASON_HIBERNATING (OR is temporarily hibernating)
|
||||
10 -- REASON_INTERNAL (Internal error at the OR)
|
||||
11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request)
|
||||
12 -- REASON_CONNRESET (Connection was unexpectedly reset)
|
||||
13 -- REASON_TORPROTOCOL (Sent when closing connection because of
|
||||
Tor protocol violations.)
|
||||
14 -- REASON_NOTDIRECTORY (Client sent RELAY_BEGIN_DIR to a
|
||||
non-directory server.)
|
||||
|
||||
(With REASON_EXITPOLICY, the 4-byte IPv4 address or 16-byte IPv6 address
|
||||
forms the optional data, along with a 4-byte TTL; no other reason
|
||||
currently has extra data.)
|
||||
|
||||
OPs and ORs MUST accept reasons not on the above list, since future
|
||||
versions of Tor may provide more fine-grained reasons.
|
||||
|
||||
Tors SHOULD NOT send any reason except REASON_MISC for a stream that they
|
||||
have originated.
|
||||
|
||||
[*] Older versions of Tor also send this reason when connections are
|
||||
reset.
|
||||
[**] Due to a bug in versions of Tor through 0095, error reason 8 must
|
||||
remain allocated until that version is obsolete.
|
||||
|
||||
--- [The rest of this section describes unimplemented functionality.]
|
||||
|
||||
Because TCP connections can be half-open, we follow an equivalent
|
||||
to TCP's FIN/FIN-ACK/ACK protocol to close streams.
|
||||
|
||||
An exit connection can have a TCP stream in one of three states:
|
||||
'OPEN', 'DONE_PACKAGING', and 'DONE_DELIVERING'. For the purposes
|
||||
of modeling transitions, we treat 'CLOSED' as a fourth state,
|
||||
although connections in this state are not, in fact, tracked by the
|
||||
onion router.
|
||||
|
||||
A stream begins in the 'OPEN' state. Upon receiving a 'FIN' from
|
||||
the corresponding TCP connection, the edge node sends a 'RELAY_FIN'
|
||||
cell along the circuit and changes its state to 'DONE_PACKAGING'.
|
||||
Upon receiving a 'RELAY_FIN' cell, an edge node sends a 'FIN' to
|
||||
the corresponding TCP connection (e.g., by calling
|
||||
shutdown(SHUT_WR)) and changing its state to 'DONE_DELIVERING'.
|
||||
|
||||
When a stream in already in 'DONE_DELIVERING' receives a 'FIN', it
|
||||
also sends a 'RELAY_FIN' along the circuit, and changes its state
|
||||
to 'CLOSED'. When a stream already in 'DONE_PACKAGING' receives a
|
||||
'RELAY_FIN' cell, it sends a 'FIN' and changes its state to
|
||||
'CLOSED'.
|
||||
|
||||
If an edge node encounters an error on any stream, it sends a
|
||||
'RELAY_END' cell (if possible) and closes the stream immediately.
|
||||
|
||||
6.4. Remote hostname lookup
|
||||
|
||||
To find the address associated with a hostname, the OP sends a
|
||||
RELAY_RESOLVE cell containing the hostname to be resolved with a nul
|
||||
terminating byte. (For a reverse lookup, the OP sends a RELAY_RESOLVE
|
||||
cell containing an in-addr.arpa address.) The OR replies with a
|
||||
RELAY_RESOLVED cell containing a status byte, and any number of
|
||||
answers. Each answer is of the form:
|
||||
Type (1 octet)
|
||||
Length (1 octet)
|
||||
Value (variable-width)
|
||||
TTL (4 octets)
|
||||
"Length" is the length of the Value field.
|
||||
"Type" is one of:
|
||||
0x00 -- Hostname
|
||||
0x04 -- IPv4 address
|
||||
0x06 -- IPv6 address
|
||||
0xF0 -- Error, transient
|
||||
0xF1 -- Error, nontransient
|
||||
|
||||
If any answer has a type of 'Error', then no other answer may be given.
|
||||
|
||||
The RELAY_RESOLVE cell must use a nonzero, distinct streamID; the
|
||||
corresponding RELAY_RESOLVED cell must use the same streamID. No stream
|
||||
is actually created by the OR when resolving the name.
|
||||
|
||||
7. Flow control
|
||||
|
||||
7.1. Link throttling
|
||||
|
||||
Each client or relay should do appropriate bandwidth throttling to
|
||||
keep its user happy.
|
||||
|
||||
Communicants rely on TCP's default flow control to push back when they
|
||||
stop reading.
|
||||
|
||||
The mainline Tor implementation uses token buckets (one for reads,
|
||||
one for writes) for the rate limiting.
|
||||
|
||||
Since 0.2.0.x, Tor has let the user specify an additional pair of
|
||||
token buckets for "relayed" traffic, so people can deploy a Tor relay
|
||||
with strict rate limiting, but also use the same Tor as a client. To
|
||||
avoid partitioning concerns we combine both classes of traffic over a
|
||||
given OR connection, and keep track of the last time we read or wrote
|
||||
a high-priority (non-relayed) cell. If it's been less than N seconds
|
||||
(currently N=30), we give the whole connection high priority, else we
|
||||
give the whole connection low priority. We also give low priority
|
||||
to reads and writes for connections that are serving directory
|
||||
information. See proposal 111 for details.
|
||||
|
||||
7.2. Link padding
|
||||
|
||||
Link padding can be created by sending PADDING cells along the
|
||||
connection; relay cells of type "DROP" can be used for long-range
|
||||
padding.
|
||||
|
||||
Currently nodes are not required to do any sort of link padding or
|
||||
dummy traffic. Because strong attacks exist even with link padding,
|
||||
and because link padding greatly increases the bandwidth requirements
|
||||
for running a node, we plan to leave out link padding until this
|
||||
tradeoff is better understood.
|
||||
|
||||
7.3. Circuit-level flow control
|
||||
|
||||
To control a circuit's bandwidth usage, each OR keeps track of two
|
||||
'windows', consisting of how many RELAY_DATA cells it is allowed to
|
||||
originate (package for transmission), and how many RELAY_DATA cells
|
||||
it is willing to consume (receive for local streams). These limits
|
||||
do not apply to cells that the OR receives from one host and relays
|
||||
to another.
|
||||
|
||||
Each 'window' value is initially set to 1000 data cells
|
||||
in each direction (cells that are not data cells do not affect
|
||||
the window). When an OR is willing to deliver more cells, it sends a
|
||||
RELAY_SENDME cell towards the OP, with Stream ID zero. When an OR
|
||||
receives a RELAY_SENDME cell with stream ID zero, it increments its
|
||||
packaging window.
|
||||
|
||||
Each of these cells increments the corresponding window by 100.
|
||||
|
||||
The OP behaves identically, except that it must track a packaging
|
||||
window and a delivery window for every OR in the circuit.
|
||||
|
||||
An OR or OP sends cells to increment its delivery window when the
|
||||
corresponding window value falls under some threshold (900).
|
||||
|
||||
If a packaging window reaches 0, the OR or OP stops reading from
|
||||
TCP connections for all streams on the corresponding circuit, and
|
||||
sends no more RELAY_DATA cells until receiving a RELAY_SENDME cell.
|
||||
[this stuff is badly worded; copy in the tor-design section -RD]
|
||||
|
||||
7.4. Stream-level flow control
|
||||
|
||||
Edge nodes use RELAY_SENDME cells to implement end-to-end flow
|
||||
control for individual connections across circuits. Similarly to
|
||||
circuit-level flow control, edge nodes begin with a window of cells
|
||||
(500) per stream, and increment the window by a fixed value (50)
|
||||
upon receiving a RELAY_SENDME cell. Edge nodes initiate RELAY_SENDME
|
||||
cells when both a) the window is <= 450, and b) there are less than
|
||||
ten cell payloads remaining to be flushed at that edge.
|
||||
|
||||
A.1. Differences between spec and implementation
|
||||
|
||||
- The current specification requires all ORs to have IPv4 addresses, but
|
||||
allows servers to exit and resolve to IPv6 addresses, and to declare IPv6
|
||||
addresses in their exit policies. The current codebase has no IPv6
|
||||
support at all.
|
||||
|
8
orchid/logging.properties
Normal file
8
orchid/logging.properties
Normal file
@ -0,0 +1,8 @@
|
||||
handlers=java.util.logging.ConsoleHandler
|
||||
.level = INFO
|
||||
|
||||
java.util.logging.ConsoleHandler.level = FINEST
|
||||
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
|
||||
java.util.logging.SimpleFormatter.format =[%1$tT] %4$s: %5$s%6$s%n
|
||||
|
||||
# com.subgraph.orchid.circuits.level=FINE
|
@ -0,0 +1,309 @@
|
||||
package com.subgraph.orchid.xmlrpc;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.apache.xmlrpc.XmlRpcException;
|
||||
import org.apache.xmlrpc.XmlRpcRequest;
|
||||
import org.apache.xmlrpc.client.XmlRpcClient;
|
||||
import org.apache.xmlrpc.client.XmlRpcClientException;
|
||||
import org.apache.xmlrpc.client.XmlRpcHttpClientConfig;
|
||||
import org.apache.xmlrpc.client.XmlRpcHttpTransport;
|
||||
import org.apache.xmlrpc.client.XmlRpcHttpTransportException;
|
||||
import org.apache.xmlrpc.client.XmlRpcLiteHttpTransport;
|
||||
import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig;
|
||||
import org.apache.xmlrpc.util.HttpUtil;
|
||||
import org.apache.xmlrpc.util.LimitedInputStream;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.subgraph.orchid.Tor;
|
||||
import com.subgraph.orchid.sockets.AndroidSSLSocketFactory;
|
||||
|
||||
public class OrchidXmlRpcTransport extends XmlRpcHttpTransport {
|
||||
|
||||
private final static Logger logger = Logger.getLogger(OrchidXmlRpcTransport.class.getName());
|
||||
|
||||
private final SocketFactory socketFactory;
|
||||
private final SSLContext sslContext;
|
||||
|
||||
private SSLSocketFactory sslSocketFactory;
|
||||
|
||||
public OrchidXmlRpcTransport(XmlRpcClient pClient, SocketFactory socketFactory, SSLContext sslContext) {
|
||||
super(pClient, userAgent);
|
||||
this.socketFactory = socketFactory;
|
||||
this.sslContext = sslContext;
|
||||
}
|
||||
|
||||
public synchronized SSLSocketFactory getSSLSocketFactory() {
|
||||
if(sslSocketFactory == null) {
|
||||
sslSocketFactory = createSSLSocketFactory();
|
||||
}
|
||||
return sslSocketFactory;
|
||||
}
|
||||
|
||||
private SSLSocketFactory createSSLSocketFactory() {
|
||||
if(Tor.isAndroidRuntime()) {
|
||||
return createAndroidSSLSocketFactory();
|
||||
}
|
||||
if(sslContext == null) {
|
||||
return (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
} else {
|
||||
return sslContext.getSocketFactory();
|
||||
}
|
||||
}
|
||||
|
||||
private SSLSocketFactory createAndroidSSLSocketFactory() {
|
||||
if(sslContext == null) {
|
||||
try {
|
||||
return new AndroidSSLSocketFactory();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.severe("Failed to create default ssl context");
|
||||
System.exit(1);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return new AndroidSSLSocketFactory(sslContext);
|
||||
}
|
||||
}
|
||||
|
||||
protected Socket newSocket(boolean pSSL, String pHostName, int pPort) throws UnknownHostException, IOException {
|
||||
final Socket s = socketFactory.createSocket(pHostName, pPort);
|
||||
if(pSSL) {
|
||||
return getSSLSocketFactory().createSocket(s, pHostName, pPort, true);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String userAgent = USER_AGENT + " (Lite HTTP Transport)";
|
||||
private boolean ssl;
|
||||
private String hostname;
|
||||
private String host;
|
||||
private int port;
|
||||
private String uri;
|
||||
private Socket socket;
|
||||
private OutputStream output;
|
||||
private InputStream input;
|
||||
private final Map<String, Object> headers = new HashMap<String, Object>();
|
||||
private boolean responseGzipCompressed = false;
|
||||
private XmlRpcHttpClientConfig config;
|
||||
|
||||
|
||||
public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException {
|
||||
config = (XmlRpcHttpClientConfig) pRequest.getConfig();
|
||||
URL url = config.getServerURL();
|
||||
ssl = "https".equals(url.getProtocol());
|
||||
hostname = url.getHost();
|
||||
int p = url.getPort();
|
||||
port = p < 1 ? 80 : p;
|
||||
String u = url.getFile();
|
||||
uri = (u == null || "".equals(u)) ? "/" : u;
|
||||
host = port == 80 ? hostname : hostname + ":" + port;
|
||||
headers.put("Host", host);
|
||||
return super.sendRequest(pRequest);
|
||||
}
|
||||
|
||||
protected void setRequestHeader(String pHeader, String pValue) {
|
||||
Object value = headers.get(pHeader);
|
||||
if (value == null) {
|
||||
headers.put(pHeader, pValue);
|
||||
} else {
|
||||
List<Object> list;
|
||||
if (value instanceof String) {
|
||||
list = new ArrayList<Object>();
|
||||
list.add((String)value);
|
||||
headers.put(pHeader, list);
|
||||
} else {
|
||||
list = (List<Object>) value;
|
||||
}
|
||||
list.add(pValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected void close() throws XmlRpcClientException {
|
||||
IOException e = null;
|
||||
if (input != null) {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException ex) {
|
||||
e = ex;
|
||||
}
|
||||
}
|
||||
if (output != null) {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException ex) {
|
||||
if (e != null) {
|
||||
e = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ex) {
|
||||
if (e != null) {
|
||||
e = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e != null) {
|
||||
throw new XmlRpcClientException("Failed to close connection: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getOutputStream() throws XmlRpcException {
|
||||
try {
|
||||
final int retries = 3;
|
||||
final int delayMillis = 100;
|
||||
|
||||
for (int tries = 0; ; tries++) {
|
||||
try {
|
||||
socket = newSocket(ssl, hostname, port);
|
||||
output = new BufferedOutputStream(socket.getOutputStream()){
|
||||
/** Closing the output stream would close the whole socket, which we don't want,
|
||||
* because the don't want until the request is processed completely.
|
||||
* A close will later occur within
|
||||
* {@link XmlRpcLiteHttpTransport#close()}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
flush();
|
||||
if(!(socket instanceof SSLSocket)) {
|
||||
socket.shutdownOutput();
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
} catch (ConnectException e) {
|
||||
if (tries >= retries) {
|
||||
throw new XmlRpcException("Failed to connect to "
|
||||
+ hostname + ":" + port + ": " + e.getMessage(), e);
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(delayMillis);
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sendRequestHeaders(output);
|
||||
return output;
|
||||
} catch (IOException e) {
|
||||
throw new XmlRpcException("Failed to open connection to "
|
||||
+ hostname + ":" + port + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private byte[] toHTTPBytes(String pValue) throws UnsupportedEncodingException {
|
||||
return pValue.getBytes("US-ASCII");
|
||||
}
|
||||
|
||||
private void sendHeader(OutputStream pOut, String pKey, String pValue) throws IOException {
|
||||
pOut.write(toHTTPBytes(pKey + ": " + pValue + "\r\n"));
|
||||
}
|
||||
|
||||
private void sendRequestHeaders(OutputStream pOut) throws IOException {
|
||||
pOut.write(("POST " + uri + " HTTP/1.0\r\n").getBytes("US-ASCII"));
|
||||
for (Iterator iter = headers.entrySet().iterator(); iter.hasNext(); ) {
|
||||
Map.Entry entry = (Map.Entry) iter.next();
|
||||
String key = (String) entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
sendHeader(pOut, key, (String) value);
|
||||
} else {
|
||||
List list = (List) value;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
sendHeader(pOut, key, (String) list.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
pOut.write(toHTTPBytes("\r\n"));
|
||||
}
|
||||
|
||||
protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) {
|
||||
return responseGzipCompressed;
|
||||
}
|
||||
|
||||
protected InputStream getInputStream() throws XmlRpcException {
|
||||
final byte[] buffer = new byte[2048];
|
||||
try {
|
||||
// If reply timeout specified, set the socket timeout accordingly
|
||||
if (config.getReplyTimeout() != 0)
|
||||
socket.setSoTimeout(config.getReplyTimeout());
|
||||
input = new BufferedInputStream(socket.getInputStream());
|
||||
// start reading server response headers
|
||||
String line = HttpUtil.readLine(input, buffer);
|
||||
StringTokenizer tokens = new StringTokenizer(line);
|
||||
tokens.nextToken(); // Skip HTTP version
|
||||
String statusCode = tokens.nextToken();
|
||||
String statusMsg = tokens.nextToken("\n\r");
|
||||
final int code;
|
||||
try {
|
||||
code = Integer.parseInt(statusCode);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new XmlRpcClientException("Server returned invalid status code: "
|
||||
+ statusCode + " " + statusMsg, null);
|
||||
}
|
||||
if (code < 200 || code > 299) {
|
||||
throw new XmlRpcHttpTransportException(code, statusMsg);
|
||||
}
|
||||
int contentLength = -1;
|
||||
for (;;) {
|
||||
line = HttpUtil.readLine(input, buffer);
|
||||
if (line == null || "".equals(line)) {
|
||||
break;
|
||||
}
|
||||
line = line.toLowerCase();
|
||||
if (line.startsWith("content-length:")) {
|
||||
contentLength = Integer.parseInt(line.substring("content-length:".length()).trim());
|
||||
} else if (line.startsWith("content-encoding:")) {
|
||||
responseGzipCompressed = HttpUtil.isUsingGzipEncoding(line.substring("content-encoding:".length()));
|
||||
}
|
||||
}
|
||||
InputStream result;
|
||||
if (contentLength == -1) {
|
||||
result = input;
|
||||
} else {
|
||||
result = new LimitedInputStream(input, contentLength);
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new XmlRpcClientException("Failed to read server response: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isUsingByteArrayOutput(XmlRpcHttpClientConfig pConfig) {
|
||||
boolean result = super.isUsingByteArrayOutput(pConfig);
|
||||
if (!result) {
|
||||
throw new IllegalStateException("The Content-Length header is required with HTTP/1.0, and HTTP/1.1 is unsupported by the Lite HTTP Transport.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void writeRequest(ReqWriter pWriter) throws XmlRpcException, IOException, SAXException {
|
||||
pWriter.write(getOutputStream());
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.subgraph.orchid.xmlrpc;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.xmlrpc.client.XmlRpcClient;
|
||||
import org.apache.xmlrpc.client.XmlRpcTransport;
|
||||
import org.apache.xmlrpc.client.XmlRpcTransportFactory;
|
||||
import com.subgraph.orchid.TorClient;
|
||||
import com.subgraph.orchid.sockets.OrchidSocketFactory;
|
||||
|
||||
public class OrchidXmlRpcTransportFactory implements XmlRpcTransportFactory {
|
||||
private final XmlRpcClient client;
|
||||
private final SSLContext sslContext;
|
||||
private final SocketFactory socketFactory;
|
||||
|
||||
public OrchidXmlRpcTransportFactory(XmlRpcClient client, TorClient torClient) {
|
||||
this(client, torClient, null);
|
||||
}
|
||||
|
||||
public OrchidXmlRpcTransportFactory(XmlRpcClient client, TorClient torClient, SSLContext sslContext) {
|
||||
this.client = client;
|
||||
this.socketFactory = new OrchidSocketFactory(torClient);
|
||||
this.sslContext = sslContext;
|
||||
}
|
||||
|
||||
public XmlRpcTransport getTransport() {
|
||||
return new OrchidXmlRpcTransport(client, socketFactory, sslContext);
|
||||
}
|
||||
}
|
99
orchid/pom.xml
Normal file
99
orchid/pom.xml
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.subgraph</groupId>
|
||||
<artifactId>orchid</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<name>Orchid</name>
|
||||
<description>Tor library</description>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.0</version>
|
||||
<configuration>
|
||||
<source>1.6</source>
|
||||
<target>1.6</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Surefire plugin specified for Maven2 compatibility -->
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.9</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.1.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<configuration>
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<filters>
|
||||
<filter>
|
||||
<!-- exclude signatures, the bundling process breaks them for some reason -->
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>bundled</shadedClassifierName>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
8
orchid/src/com/subgraph/orchid/BridgeRouter.java
Normal file
8
orchid/src/com/subgraph/orchid/BridgeRouter.java
Normal file
@ -0,0 +1,8 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
|
||||
public interface BridgeRouter extends Router {
|
||||
void setIdentity(HexDigest identity);
|
||||
void setDescriptor(RouterDescriptor descriptor);
|
||||
}
|
230
orchid/src/com/subgraph/orchid/Cell.java
Normal file
230
orchid/src/com/subgraph/orchid/Cell.java
Normal file
@ -0,0 +1,230 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
public interface Cell {
|
||||
/** Command constant for a PADDING type cell. */
|
||||
final static int PADDING = 0;
|
||||
|
||||
/** Command constant for a CREATE type cell. */
|
||||
final static int CREATE = 1;
|
||||
|
||||
/** Command constant for a CREATED type cell. */
|
||||
final static int CREATED = 2;
|
||||
|
||||
/** Command constant for a RELAY type cell. */
|
||||
final static int RELAY = 3;
|
||||
|
||||
/** Command constant for a DESTROY type cell. */
|
||||
final static int DESTROY = 4;
|
||||
|
||||
/** Command constant for a CREATE_FAST type cell. */
|
||||
final static int CREATE_FAST = 5;
|
||||
|
||||
/** Command constant for a CREATED_FAST type cell. */
|
||||
final static int CREATED_FAST = 6;
|
||||
|
||||
/** Command constant for a VERSIONS type cell. */
|
||||
final static int VERSIONS = 7;
|
||||
|
||||
/** Command constant for a NETINFO type cell. */
|
||||
final static int NETINFO = 8;
|
||||
|
||||
/** Command constant for a RELAY_EARLY type cell. */
|
||||
final static int RELAY_EARLY = 9;
|
||||
|
||||
final static int VPADDING = 128;
|
||||
final static int CERTS = 129;
|
||||
final static int AUTH_CHALLENGE = 130;
|
||||
final static int AUTHENTICATE = 131;
|
||||
final static int AUTHORIZE = 132;
|
||||
|
||||
final static int ERROR_NONE = 0;
|
||||
final static int ERROR_PROTOCOL = 1;
|
||||
final static int ERROR_INTERNAL = 2;
|
||||
final static int ERROR_REQUESTED = 3;
|
||||
final static int ERROR_HIBERNATING = 4;
|
||||
final static int ERROR_RESOURCELIMIT = 5;
|
||||
final static int ERROR_CONNECTFAILED = 6;
|
||||
final static int ERROR_OR_IDENTITY = 7;
|
||||
final static int ERROR_OR_CONN_CLOSED = 8;
|
||||
final static int ERROR_FINISHED = 9;
|
||||
final static int ERROR_TIMEOUT = 10;
|
||||
final static int ERROR_DESTROYED = 11;
|
||||
final static int ERROR_NOSUCHSERVICE = 12;
|
||||
|
||||
final static int ADDRESS_TYPE_HOSTNAME = 0x00;
|
||||
final static int ADDRESS_TYPE_IPV4 = 0x04;
|
||||
final static int ADRESS_TYPE_IPV6 = 0x06;
|
||||
|
||||
/**
|
||||
* The fixed size of a standard cell.
|
||||
*/
|
||||
final static int CELL_LEN = 512;
|
||||
|
||||
/**
|
||||
* The length of a standard cell header.
|
||||
*/
|
||||
final static int CELL_HEADER_LEN = 3;
|
||||
|
||||
/**
|
||||
* The header length for a variable length cell (ie: VERSIONS)
|
||||
*/
|
||||
final static int CELL_VAR_HEADER_LEN = 5;
|
||||
|
||||
/**
|
||||
* The length of the payload space in a standard cell.
|
||||
*/
|
||||
final static int CELL_PAYLOAD_LEN = CELL_LEN - CELL_HEADER_LEN;
|
||||
|
||||
/**
|
||||
* Return the circuit id field from this cell.
|
||||
*
|
||||
* @return The circuit id field of this cell.
|
||||
*/
|
||||
int getCircuitId();
|
||||
|
||||
/**
|
||||
* Return the command field from this cell.
|
||||
*
|
||||
* @return The command field of this cell.
|
||||
*/
|
||||
int getCommand();
|
||||
|
||||
/**
|
||||
* Set the internal pointer to the first byte after the cell header.
|
||||
*/
|
||||
void resetToPayload();
|
||||
|
||||
/**
|
||||
* Return the next byte from the cell and increment the internal pointer by one byte.
|
||||
*
|
||||
* @return The byte at the current pointer location.
|
||||
*/
|
||||
int getByte();
|
||||
|
||||
/**
|
||||
* Return the byte at the specified offset into the cell.
|
||||
*
|
||||
* @param index The cell offset.
|
||||
* @return The byte at the specified offset.
|
||||
*/
|
||||
int getByteAt(int index);
|
||||
|
||||
/**
|
||||
* Return the next 16-bit big endian value from the cell and increment the internal pointer by two bytes.
|
||||
*
|
||||
* @return The 16-bit short value at the current pointer location.
|
||||
*/
|
||||
int getShort();
|
||||
|
||||
/**
|
||||
* Return the 16-bit big endian value at the specified offset into the cell.
|
||||
*
|
||||
* @param index The cell offset.
|
||||
* @return The 16-bit short value at the specified offset.
|
||||
*/
|
||||
int getShortAt(int index);
|
||||
|
||||
/**
|
||||
* Return the next 32-bit big endian value from the cell and increment the internal pointer by four bytes.
|
||||
*
|
||||
* @return The 32-bit integer value at the current pointer location.
|
||||
*/
|
||||
int getInt();
|
||||
|
||||
/**
|
||||
* Copy <code>buffer.length</code> bytes from the cell into <code>buffer</code>. The data is copied starting
|
||||
* from the current internal pointer location and afterwards the internal pointer is incremented by <code>buffer.length</code>
|
||||
* bytes.
|
||||
*
|
||||
* @param buffer The array of bytes to copy the cell data into.
|
||||
*/
|
||||
void getByteArray(byte[] buffer);
|
||||
|
||||
/**
|
||||
* Return the number of bytes already packed (for outgoing cells) or unpacked (for incoming cells). This is
|
||||
* equivalent to the internal pointer position.
|
||||
*
|
||||
* @return The number of bytes already consumed from this cell.
|
||||
*/
|
||||
int cellBytesConsumed();
|
||||
|
||||
/**
|
||||
* Return the number of bytes remaining between the current internal pointer and the end of the cell. If fields
|
||||
* are being added to a new cell for transmission then this value indicates the remaining space in bytes for
|
||||
* adding new data. If fields are being read from a received cell then this value describes the number of bytes
|
||||
* which can be read without overflowing the cell.
|
||||
*
|
||||
* @return The number of payload bytes remaining in this cell.
|
||||
*/
|
||||
int cellBytesRemaining();
|
||||
|
||||
/**
|
||||
* Store a byte at the current pointer location and increment the pointer by one byte.
|
||||
*
|
||||
* @param value The byte value to store.
|
||||
*/
|
||||
void putByte(int value);
|
||||
|
||||
/**
|
||||
* Store a byte at the specified offset into the cell.
|
||||
*
|
||||
* @param index The offset in bytes into the cell.
|
||||
* @param value The byte value to store.
|
||||
*/
|
||||
void putByteAt(int index, int value);
|
||||
|
||||
/**
|
||||
* Store a 16-bit short value in big endian order at the current pointer location and
|
||||
* increment the pointer by two bytes.
|
||||
*
|
||||
* @param value The 16-bit short value to store.
|
||||
*/
|
||||
void putShort(int value);
|
||||
|
||||
/**
|
||||
* Store a 16-bit short value in big endian byte order at the specified offset into the cell
|
||||
* and increment the pointer by two bytes.
|
||||
*
|
||||
* @param index The offset in bytes into the cell.
|
||||
* @param value The 16-bit short value to store.
|
||||
*/
|
||||
void putShortAt(int index, int value);
|
||||
|
||||
/**
|
||||
* Store a 32-bit integer value in big endian order at the current pointer location and
|
||||
* increment the pointer by 4 bytes.
|
||||
*
|
||||
* @param value The 32-bit integer value to store.
|
||||
*/
|
||||
void putInt(int value);
|
||||
|
||||
/**
|
||||
* Store the entire array <code>data</code> at the current pointer location and increment
|
||||
* the pointer by <code>data.length</code> bytes.
|
||||
*
|
||||
* @param data The array of bytes to store in the cell.
|
||||
*/
|
||||
void putByteArray(byte[] data);
|
||||
|
||||
/**
|
||||
* Store <code>length</code> bytes of the byte array <code>data</code> starting from
|
||||
* <code>offset</code> into the array at the current pointer location and increment
|
||||
* the pointer by <code>length</code> bytes.
|
||||
*
|
||||
* @param data The source array of bytes.
|
||||
* @param offset The offset into the source array.
|
||||
* @param length The number of bytes from the source array to store.
|
||||
*/
|
||||
void putByteArray(byte[] data, int offset, int length);
|
||||
|
||||
/**
|
||||
* Return the entire cell data as a raw array of bytes. For all cells except
|
||||
* <code>VERSIONS</code>, this array will be exactly <code>CELL_LEN</code> bytes long.
|
||||
*
|
||||
* @return The cell data as an array of bytes.
|
||||
*/
|
||||
byte[] getCellBytes();
|
||||
|
||||
void putString(String string);
|
||||
}
|
95
orchid/src/com/subgraph/orchid/Circuit.java
Normal file
95
orchid/src/com/subgraph/orchid/Circuit.java
Normal file
@ -0,0 +1,95 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A Circuit represents a logical path through multiple ORs. Circuits are described in
|
||||
* section 5 of tor-spec.txt.
|
||||
*
|
||||
*/
|
||||
public interface Circuit {
|
||||
|
||||
/**
|
||||
* Return <code>true</code> if the circuit is presently in the connected state or
|
||||
* <code>false</code> otherwise.
|
||||
*
|
||||
* @return Returns <code>true</code> if the circuit is presently connected, or
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
boolean isPending();
|
||||
|
||||
boolean isClean();
|
||||
|
||||
boolean isMarkedForClose();
|
||||
|
||||
int getSecondsDirty();
|
||||
|
||||
/**
|
||||
* Returns the entry router <code>Connection</code> object of this Circuit. Throws
|
||||
* a TorException if the circuit is not currently open.
|
||||
*
|
||||
* @return The Connection object for the network connection to the entry router of this
|
||||
* circuit.
|
||||
* @throws TorException If this circuit is not currently connected.
|
||||
*/
|
||||
Connection getConnection();
|
||||
|
||||
/**
|
||||
* Returns the curcuit id value for this circuit.
|
||||
*
|
||||
* @return The circuit id value for this circuit.
|
||||
*/
|
||||
int getCircuitId();
|
||||
|
||||
/**
|
||||
* Create a new relay cell which is configured for delivery to the specified
|
||||
* circuit <code>targetNode</code> with command value <code>relayCommand</code>
|
||||
* and a stream id value of <code>streamId</code>. The returned <code>RelayCell</code>
|
||||
* can then be used to populate the payload of the cell before delivering it.
|
||||
*
|
||||
* @param relayCommand The command value to send in the relay cell header.
|
||||
* @param streamId The stream id value to send in the relay cell header.
|
||||
* @param targetNode The target circuit node to encrypt this cell for.
|
||||
* @return A newly created relay cell object.
|
||||
*/
|
||||
RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode);
|
||||
|
||||
/**
|
||||
* Returns the next relay response cell received on this circuit. If no response is
|
||||
* received within <code>CIRCUIT_RELAY_RESPONSE_TIMEOUT</code> milliseconds, <code>null</code>
|
||||
* is returned.
|
||||
*
|
||||
* @return The next relay response cell received on this circuit or <code>null</code> if
|
||||
* a timeout is reached before the next relay cell arrives.
|
||||
*/
|
||||
RelayCell receiveRelayCell();
|
||||
|
||||
/**
|
||||
* Encrypt and deliver the relay cell <code>cell</code>.
|
||||
*
|
||||
* @param cell The relay cell to deliver over this circuit.
|
||||
*/
|
||||
void sendRelayCell(RelayCell cell);
|
||||
|
||||
/**
|
||||
* Return the last node or 'hop' in this circuit.
|
||||
*
|
||||
* @return The final 'hop' or node of this circuit.
|
||||
*/
|
||||
CircuitNode getFinalCircuitNode();
|
||||
|
||||
|
||||
void destroyCircuit();
|
||||
|
||||
void deliverRelayCell(Cell cell);
|
||||
|
||||
void deliverControlCell(Cell cell);
|
||||
|
||||
List<Stream> getActiveStreams();
|
||||
|
||||
void markForClose();
|
||||
|
||||
void appendNode(CircuitNode node);
|
||||
}
|
60
orchid/src/com/subgraph/orchid/CircuitBuildHandler.java
Normal file
60
orchid/src/com/subgraph/orchid/CircuitBuildHandler.java
Normal file
@ -0,0 +1,60 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
/**
|
||||
* This callback interface is used for reporting progress when
|
||||
* opening a new circuit. An instance of this interface is passed
|
||||
* to the {@link Circuit#openCircuit(java.util.List, CircuitBuildHandler)}
|
||||
* method.
|
||||
*
|
||||
* The normal sequence of callbacks which are fired when a circuit is opened
|
||||
* successfully is {@link #connectionCompleted(Connection)} for the initial
|
||||
* connection to the entry router, followed by one or more
|
||||
* {@link #nodeAdded(CircuitNode)} as the circuit is extended with new nodes.
|
||||
* When all requested nodes in the path have been added successfully to the
|
||||
* circuit {@link #circuitBuildCompleted(Circuit)} is called and passed the
|
||||
* newly constructed circuit.
|
||||
*
|
||||
* @see Circuit#openCircuit()
|
||||
*
|
||||
*/
|
||||
public interface CircuitBuildHandler {
|
||||
/**
|
||||
* Called when a network connection to the entry node has completed
|
||||
* successfully or if a network connection to the specified entry router
|
||||
* already exists.
|
||||
*
|
||||
* @param connection The completed connection instance.
|
||||
*/
|
||||
void connectionCompleted(Connection connection);
|
||||
|
||||
/**
|
||||
* The circuit build has failed because the network connection to the
|
||||
* entry node failed. No further callback methods will be called after
|
||||
* this failure has been reported.
|
||||
*
|
||||
* @param reason A description of the reason for failing to connect to
|
||||
* the entry node.
|
||||
*/
|
||||
void connectionFailed(String reason);
|
||||
|
||||
/**
|
||||
* A node or 'hop' has been added to the circuit which is being created.
|
||||
*
|
||||
* @param node The newly added circuit node.
|
||||
*/
|
||||
void nodeAdded(CircuitNode node);
|
||||
|
||||
/**
|
||||
* The circuit has been successfully built and is ready for use.
|
||||
*
|
||||
* @param circuit The newly constructed circuit.
|
||||
*/
|
||||
void circuitBuildCompleted(Circuit circuit);
|
||||
|
||||
/**
|
||||
* Called if the circuit build fails after connecting to the entry node.
|
||||
*
|
||||
* @param reason A description of the reason the circuit build has failed.
|
||||
*/
|
||||
void circuitBuildFailed(String reason);
|
||||
}
|
51
orchid/src/com/subgraph/orchid/CircuitManager.java
Normal file
51
orchid/src/com/subgraph/orchid/CircuitManager.java
Normal file
@ -0,0 +1,51 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
|
||||
|
||||
public interface CircuitManager {
|
||||
|
||||
static int DIRECTORY_PURPOSE_CONSENSUS = 1;
|
||||
static int DIRECTORY_PURPOSE_CERTIFICATES = 2;
|
||||
static int DIRECTORY_PURPOSE_DESCRIPTORS = 3;
|
||||
|
||||
/**
|
||||
* Begin automatically building new circuits in the background.
|
||||
*/
|
||||
void startBuildingCircuits();
|
||||
void stopBuildingCircuits(boolean killCircuits);
|
||||
/**
|
||||
* Attempt to open an exit stream to the specified destination <code>hostname</code> and
|
||||
* <code>port</code>.
|
||||
*
|
||||
* @param hostname The name of the host to open an exit connection to.
|
||||
* @param port The port to open an exit connection to.
|
||||
* @return The status response result of attempting to open the exit connection.
|
||||
*/
|
||||
Stream openExitStreamTo(String hostname, int port) throws InterruptedException, TimeoutException, OpenFailedException;
|
||||
|
||||
/**
|
||||
* Attempt to open an exit stream to the destination specified by <code>address</code> and
|
||||
* <code>port</code>.
|
||||
*
|
||||
* @param address The address to open an exit connection to.
|
||||
* @param port The port to open an exit connection to.
|
||||
* @return The status response result of attempting the open the exit connection.
|
||||
*/
|
||||
Stream openExitStreamTo(IPv4Address address, int port) throws InterruptedException, TimeoutException, OpenFailedException;
|
||||
|
||||
|
||||
Stream openDirectoryStream(int purpose) throws InterruptedException, TimeoutException, OpenFailedException;
|
||||
|
||||
Stream openDirectoryStream() throws InterruptedException, TimeoutException, OpenFailedException;
|
||||
|
||||
DirectoryCircuit openDirectoryCircuit() throws OpenFailedException;
|
||||
Circuit getCleanInternalCircuit() throws InterruptedException;
|
||||
|
||||
ExitCircuit openExitCircuitTo(List<Router> path) throws OpenFailedException;
|
||||
InternalCircuit openInternalCircuitTo(List<Router> path) throws OpenFailedException;
|
||||
DirectoryCircuit openDirectoryCircuitTo(List<Router> path) throws OpenFailedException;
|
||||
}
|
90
orchid/src/com/subgraph/orchid/CircuitNode.java
Normal file
90
orchid/src/com/subgraph/orchid/CircuitNode.java
Normal file
@ -0,0 +1,90 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Represents the state of a single onion router hop in a connected or connecting {@link Circuit}
|
||||
*/
|
||||
public interface CircuitNode {
|
||||
/**
|
||||
* Return the {@link Router} associated with this node.
|
||||
*
|
||||
* @return The {@link Router} for this hop of the circuit chain.
|
||||
*/
|
||||
Router getRouter();
|
||||
|
||||
/**
|
||||
* Update the 'forward' cryptographic digest state for this
|
||||
* node with the contents of <code>cell</code>
|
||||
*
|
||||
* @param cell The {@link RelayCell} to add to the digest.
|
||||
*/
|
||||
void updateForwardDigest(RelayCell cell);
|
||||
|
||||
/**
|
||||
* Return the current 'forward' running digest value for this
|
||||
* node as an array of <code>TOR_DIGEST_SIZE</code> bytes.
|
||||
*
|
||||
* @return The current 'forward' running digest value for this node.
|
||||
*/
|
||||
byte[] getForwardDigestBytes();
|
||||
|
||||
/**
|
||||
* Encrypt a {@link RelayCell} for this node with the current
|
||||
* 'forward' cipher state.
|
||||
*
|
||||
* @param cell The {@link RelayCell} to encrypt.
|
||||
*/
|
||||
void encryptForwardCell(RelayCell cell);
|
||||
|
||||
/**
|
||||
* Return the {@link CircuitNode} which immediately preceeds this
|
||||
* one in the circuit node chain or <code>null</code> if this is
|
||||
* the first hop.
|
||||
*
|
||||
* @return The previous {@link CircuitNode} in the chain or <code>
|
||||
* null</code> if this is the first node.
|
||||
*/
|
||||
CircuitNode getPreviousNode();
|
||||
|
||||
/**
|
||||
* Return immediately if the packaging window for this node is open (ie: greater than 0), otherwise
|
||||
* block until the circuit is destroyed or the window is incremented by receiving a RELAY_SENDME cell
|
||||
* from this node.
|
||||
*/
|
||||
void waitForSendWindow();
|
||||
|
||||
/**
|
||||
* If the packaging window for this node is open (ie: greater than 0) this method
|
||||
* decrements the packaging window by 1 and returns immediately, otherwise it will
|
||||
* block until the circuit is destroyed or the window is incremented by receiving
|
||||
* a RELAY_SENDME cell from this node. This method will always decrement the packaging
|
||||
* window before returning unless the circuit has been destroyed.
|
||||
*/
|
||||
void waitForSendWindowAndDecrement();
|
||||
|
||||
/**
|
||||
* This method is called to signal that a RELAY_SENDME cell has been received from this
|
||||
* node and the packaging window should be incremented. This will also wake up any threads
|
||||
* that are waiting for the packaging window to open.
|
||||
*/
|
||||
void incrementSendWindow();
|
||||
|
||||
/**
|
||||
* This method is called when a RELAY_DATA cell is received from this node to decrement
|
||||
* the deliver window counter.
|
||||
*/
|
||||
void decrementDeliverWindow();
|
||||
|
||||
/**
|
||||
* Examines the delivery window and determines if it would be an appropriate time to
|
||||
* send a RELAY_SENDME cell. If this method returns true, it increments the delivery
|
||||
* window assuming that a RELAY_SENDME cell will be transmitted.
|
||||
*
|
||||
* @return Returns true if the deliver window is small enough that sending a RELAY_SENDME
|
||||
* cell would be appropriate.
|
||||
*/
|
||||
boolean considerSendingSendme();
|
||||
|
||||
boolean decryptBackwardCell(Cell cell);
|
||||
}
|
47
orchid/src/com/subgraph/orchid/Connection.java
Normal file
47
orchid/src/com/subgraph/orchid/Connection.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
/**
|
||||
* A network connection to a Tor onion router.
|
||||
*/
|
||||
public interface Connection {
|
||||
/**
|
||||
* Return the {@link Router} associated with this connection.
|
||||
*
|
||||
* @return The entry router this connection represents.
|
||||
*/
|
||||
Router getRouter();
|
||||
|
||||
/**
|
||||
* Return <code>true</code> if the socket for this connection has been closed. Otherwise, <code>false</code>.
|
||||
*
|
||||
* @return <code>true</code> if this connection is closed or <code>false</code> otherwise.
|
||||
*/
|
||||
boolean isClosed();
|
||||
/**
|
||||
* Send a protocol {@link Cell} on this connection.
|
||||
*
|
||||
* @param cell The {@link Cell} to transfer.
|
||||
* @throws ConnectionIOException If the cell could not be send because the connection is not connected
|
||||
* or if an error occured while sending the cell data.
|
||||
*/
|
||||
void sendCell(Cell cell) throws ConnectionIOException;
|
||||
|
||||
/**
|
||||
* Remove a Circuit which has been bound to this Connection by a previous call to {@link #bindCircuit(Circuit) bindCircuit}.
|
||||
* After removing a Circuit, any further received incoming cells for the Circuit will be discarded.
|
||||
*
|
||||
* @param circuit The Circuit to remove.
|
||||
*/
|
||||
void removeCircuit(Circuit circuit);
|
||||
|
||||
/**
|
||||
* Choose an available circuit id value and bind this Circuit to that id value, returning the id value.
|
||||
* Once bound, any incoming relay cells will be delivered to the Circuit with {@link Circuit#deliverRelayCell(Cell)}
|
||||
* and other cells will be delivered with {@link Circuit#deliverControlCell(Cell)}.
|
||||
*
|
||||
* @param circuit The Circuit to bind to this connection.
|
||||
* @return the circuit id value for this binding.
|
||||
*/
|
||||
int bindCircuit(Circuit circuit);
|
||||
}
|
21
orchid/src/com/subgraph/orchid/ConnectionCache.java
Normal file
21
orchid/src/com/subgraph/orchid/ConnectionCache.java
Normal file
@ -0,0 +1,21 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
public interface ConnectionCache {
|
||||
/**
|
||||
* Returns a completed connection to the specified router. If an open connection
|
||||
* to the requested router already exists it is returned, otherwise a new connection
|
||||
* is opened.
|
||||
*
|
||||
* @param router The router to which a connection is requested.
|
||||
* @param isDirectoryConnection Is this going to be used as a directory connection.
|
||||
* @return a completed connection to the specified router.
|
||||
* @throws InterruptedException if thread is interrupted while waiting for connection to complete.
|
||||
* @throws ConnectionTimeoutException if timeout expires before connection completes.
|
||||
* @throws ConnectionFailedException if connection fails due to I/O error
|
||||
* @throws ConnectionHandshakeException if connection fails because an error occurred during handshake phase
|
||||
*/
|
||||
Connection getConnectionTo(Router router, boolean isDirectoryConnection) throws InterruptedException, ConnectionTimeoutException, ConnectionFailedException, ConnectionHandshakeException;
|
||||
|
||||
void close();
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class ConnectionFailedException extends ConnectionIOException {
|
||||
|
||||
private static final long serialVersionUID = -4484347156587613574L;
|
||||
|
||||
public ConnectionFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class ConnectionHandshakeException extends ConnectionIOException {
|
||||
|
||||
private static final long serialVersionUID = -2544633445932967966L;
|
||||
|
||||
public ConnectionHandshakeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
14
orchid/src/com/subgraph/orchid/ConnectionIOException.java
Normal file
14
orchid/src/com/subgraph/orchid/ConnectionIOException.java
Normal file
@ -0,0 +1,14 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class ConnectionIOException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5537650738995969203L;
|
||||
|
||||
public ConnectionIOException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConnectionIOException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class ConnectionTimeoutException extends ConnectionIOException {
|
||||
|
||||
private static final long serialVersionUID = -6098661610150140151L;
|
||||
|
||||
public ConnectionTimeoutException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConnectionTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
44
orchid/src/com/subgraph/orchid/ConsensusDocument.java
Normal file
44
orchid/src/com/subgraph/orchid/ConsensusDocument.java
Normal file
@ -0,0 +1,44 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.Timestamp;
|
||||
|
||||
public interface ConsensusDocument extends Document {
|
||||
enum ConsensusFlavor { NS, MICRODESC };
|
||||
enum SignatureStatus { STATUS_VERIFIED, STATUS_FAILED, STATUS_NEED_CERTS };
|
||||
|
||||
interface RequiredCertificate {
|
||||
int getDownloadFailureCount();
|
||||
void incrementDownloadFailureCount();
|
||||
HexDigest getAuthorityIdentity();
|
||||
HexDigest getSigningKey();
|
||||
}
|
||||
|
||||
ConsensusFlavor getFlavor();
|
||||
Timestamp getValidAfterTime();
|
||||
Timestamp getFreshUntilTime();
|
||||
Timestamp getValidUntilTime();
|
||||
int getConsensusMethod();
|
||||
int getVoteSeconds();
|
||||
int getDistSeconds();
|
||||
Set<String> getClientVersions();
|
||||
Set<String> getServerVersions();
|
||||
boolean isLive();
|
||||
List<RouterStatus> getRouterStatusEntries();
|
||||
|
||||
SignatureStatus verifySignatures();
|
||||
Set<RequiredCertificate> getRequiredCertificates();
|
||||
|
||||
HexDigest getSigningHash();
|
||||
HexDigest getSigningHash256();
|
||||
|
||||
int getCircWindowParameter();
|
||||
int getWeightScaleParameter();
|
||||
|
||||
int getBandwidthWeight(String tag);
|
||||
|
||||
boolean getUseNTorHandshake();
|
||||
}
|
65
orchid/src/com/subgraph/orchid/Descriptor.java
Normal file
65
orchid/src/com/subgraph/orchid/Descriptor.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
|
||||
public interface Descriptor extends Document {
|
||||
enum CacheLocation { NOT_CACHED, CACHED_CACHEFILE, CACHED_JOURNAL }
|
||||
|
||||
HexDigest getDescriptorDigest();
|
||||
void setLastListed(long timestamp);
|
||||
long getLastListed();
|
||||
void setCacheLocation(CacheLocation location);
|
||||
CacheLocation getCacheLocation();
|
||||
int getBodyLength();
|
||||
|
||||
/**
|
||||
* Return the public key used to encrypt EXTEND cells while establishing
|
||||
* a circuit through this router.
|
||||
*
|
||||
* @return The onion routing protocol key for this router.
|
||||
*/
|
||||
TorPublicKey getOnionKey();
|
||||
byte[] getNTorOnionKey();
|
||||
|
||||
/**
|
||||
* Return the IPv4 address of this router.
|
||||
*
|
||||
* @return The IPv4 address of this router.
|
||||
*/
|
||||
IPv4Address getAddress();
|
||||
|
||||
/**
|
||||
* Return the port on which this node accepts TLS connections
|
||||
* for the main OR protocol, or 0 if no router service is advertised.
|
||||
*
|
||||
* @return The onion routing port, or 0 if not a router.
|
||||
*/
|
||||
int getRouterPort();
|
||||
Set<String> getFamilyMembers();
|
||||
|
||||
/**
|
||||
* Return true if the exit policy of this router permits connections
|
||||
* to the specified destination endpoint.
|
||||
*
|
||||
* @param address The IPv4 address of the destination.
|
||||
* @param port The destination port.
|
||||
*
|
||||
* @return True if an exit connection to the specified destination is allowed
|
||||
* or false otherwise.
|
||||
*/
|
||||
boolean exitPolicyAccepts(IPv4Address address, int port);
|
||||
|
||||
/**
|
||||
* Return true if the exit policy of this router accepts most connections
|
||||
* to the specified destination port.
|
||||
*
|
||||
* @param port The destination port.
|
||||
* @return True if an exit connection to the specified destination port is generally allowed
|
||||
* or false otherwise.
|
||||
*/
|
||||
boolean exitPolicyAccepts(int port);
|
||||
}
|
48
orchid/src/com/subgraph/orchid/Directory.java
Normal file
48
orchid/src/com/subgraph/orchid/Directory.java
Normal file
@ -0,0 +1,48 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.ConsensusDocument.RequiredCertificate;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.events.EventHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* Main interface for accessing directory information and interacting
|
||||
* with directory authorities and caches.
|
||||
*
|
||||
*/
|
||||
public interface Directory {
|
||||
boolean haveMinimumRouterInfo();
|
||||
void loadFromStore();
|
||||
void close();
|
||||
void waitUntilLoaded();
|
||||
void storeCertificates();
|
||||
|
||||
Collection<DirectoryServer> getDirectoryAuthorities();
|
||||
DirectoryServer getRandomDirectoryAuthority();
|
||||
void addCertificate(KeyCertificate certificate);
|
||||
Set<RequiredCertificate> getRequiredCertificates();
|
||||
void addRouterMicrodescriptors(List<RouterMicrodescriptor> microdescriptors);
|
||||
void addRouterDescriptors(List<RouterDescriptor> descriptors);
|
||||
void addConsensusDocument(ConsensusDocument consensus, boolean fromCache);
|
||||
ConsensusDocument getCurrentConsensusDocument();
|
||||
boolean hasPendingConsensus();
|
||||
void registerConsensusChangedHandler(EventHandler handler);
|
||||
void unregisterConsensusChangedHandler(EventHandler handler);
|
||||
Router getRouterByName(String name);
|
||||
Router getRouterByIdentity(HexDigest identity);
|
||||
List<Router> getRouterListByNames(List<String> names);
|
||||
List<Router> getRoutersWithDownloadableDescriptors();
|
||||
List<Router> getAllRouters();
|
||||
|
||||
RouterMicrodescriptor getMicrodescriptorFromCache(HexDigest descriptorDigest);
|
||||
RouterDescriptor getBasicDescriptorFromCache(HexDigest descriptorDigest);
|
||||
|
||||
GuardEntry createGuardEntryFor(Router router);
|
||||
List<GuardEntry> getGuardEntries();
|
||||
void removeGuardEntry(GuardEntry entry);
|
||||
void addGuardEntry(GuardEntry entry);
|
||||
}
|
16
orchid/src/com/subgraph/orchid/DirectoryCircuit.java
Normal file
16
orchid/src/com/subgraph/orchid/DirectoryCircuit.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public interface DirectoryCircuit extends Circuit {
|
||||
/**
|
||||
* Open an anonymous connection to the directory service running on the
|
||||
* final node in this circuit.
|
||||
*
|
||||
* @param timeout in milliseconds
|
||||
* @param autoclose if set to true, closing stream also marks this circuit for close
|
||||
*
|
||||
* @return The status response returned by trying to open the stream.
|
||||
*/
|
||||
Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException;
|
||||
}
|
27
orchid/src/com/subgraph/orchid/DirectoryDownloader.java
Normal file
27
orchid/src/com/subgraph/orchid/DirectoryDownloader.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.ConsensusDocument.RequiredCertificate;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
|
||||
|
||||
public interface DirectoryDownloader {
|
||||
void start(Directory directory);
|
||||
void stop();
|
||||
|
||||
RouterDescriptor downloadBridgeDescriptor(Router bridge) throws DirectoryRequestFailedException;
|
||||
|
||||
ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors) throws DirectoryRequestFailedException;
|
||||
ConsensusDocument downloadCurrentConsensus(boolean useMicrodescriptors, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
|
||||
|
||||
List<KeyCertificate> downloadKeyCertificates(Set<RequiredCertificate> required) throws DirectoryRequestFailedException;
|
||||
List<KeyCertificate> downloadKeyCertificates(Set<RequiredCertificate> required, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
|
||||
|
||||
List<RouterDescriptor> downloadRouterDescriptors(Set<HexDigest> fingerprints) throws DirectoryRequestFailedException;
|
||||
List<RouterDescriptor> downloadRouterDescriptors(Set<HexDigest> fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
|
||||
|
||||
List<RouterMicrodescriptor> downloadRouterMicrodescriptors(Set<HexDigest> fingerprints) throws DirectoryRequestFailedException;
|
||||
List<RouterMicrodescriptor> downloadRouterMicrodescriptors(Set<HexDigest> fingerprints, DirectoryCircuit circuit) throws DirectoryRequestFailedException;
|
||||
}
|
22
orchid/src/com/subgraph/orchid/DirectoryServer.java
Normal file
22
orchid/src/com/subgraph/orchid/DirectoryServer.java
Normal file
@ -0,0 +1,22 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
|
||||
/**
|
||||
* Represents a directory authority server or a directory cache.
|
||||
*/
|
||||
public interface DirectoryServer extends Router {
|
||||
int getDirectoryPort();
|
||||
boolean isV2Authority();
|
||||
boolean isV3Authority();
|
||||
HexDigest getV3Identity();
|
||||
boolean isHiddenServiceAuthority();
|
||||
boolean isBridgeAuthority();
|
||||
boolean isExtraInfoCache();
|
||||
|
||||
KeyCertificate getCertificateByFingerprint(HexDigest fingerprint);
|
||||
List<KeyCertificate> getCertificates();
|
||||
void addCertificate(KeyCertificate certificate);
|
||||
}
|
36
orchid/src/com/subgraph/orchid/DirectoryStore.java
Normal file
36
orchid/src/com/subgraph/orchid/DirectoryStore.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
public interface DirectoryStore {
|
||||
enum CacheFile {
|
||||
CERTIFICATES("certificates"),
|
||||
CONSENSUS("consensus"),
|
||||
CONSENSUS_MICRODESC("consensus-microdesc"),
|
||||
MICRODESCRIPTOR_CACHE("cached-microdescs"),
|
||||
MICRODESCRIPTOR_JOURNAL("cached-microdescs.new"),
|
||||
DESCRIPTOR_CACHE("cached-descriptors"),
|
||||
DESCRIPTOR_JOURNAL("cached-descriptors.new"),
|
||||
STATE("state");
|
||||
|
||||
final private String filename;
|
||||
|
||||
CacheFile(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer loadCacheFile(CacheFile cacheFile);
|
||||
void writeData(CacheFile cacheFile, ByteBuffer data);
|
||||
void writeDocument(CacheFile cacheFile, Document document);
|
||||
void writeDocumentList(CacheFile cacheFile, List<? extends Document> documents);
|
||||
void appendDocumentList(CacheFile cacheFile, List<? extends Document> documents);
|
||||
|
||||
void removeCacheFile(CacheFile cacheFile);
|
||||
void removeAllCacheFiles();
|
||||
}
|
9
orchid/src/com/subgraph/orchid/Document.java
Normal file
9
orchid/src/com/subgraph/orchid/Document.java
Normal file
@ -0,0 +1,9 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public interface Document {
|
||||
ByteBuffer getRawDocumentBytes();
|
||||
String getRawDocumentData();
|
||||
boolean isValidDocument();
|
||||
}
|
50
orchid/src/com/subgraph/orchid/ExitCircuit.java
Normal file
50
orchid/src/com/subgraph/orchid/ExitCircuit.java
Normal file
@ -0,0 +1,50 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
|
||||
public interface ExitCircuit extends Circuit {
|
||||
|
||||
/**
|
||||
* Open an exit stream from the final node in this circuit to the
|
||||
* specified target address and port.
|
||||
*
|
||||
* @param address The network address of the exit target.
|
||||
* @param port The port of the exit target.
|
||||
* @return The status response returned by trying to open the stream.
|
||||
*/
|
||||
Stream openExitStream(IPv4Address address, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
|
||||
|
||||
/**
|
||||
* Open an exit stream from the final node in this circuit to the
|
||||
* specified target hostname and port.
|
||||
*
|
||||
* @param hostname The network hostname of the exit target.
|
||||
* @param port The port of the exit target.
|
||||
* @return The status response returned by trying to open the stream.
|
||||
*/
|
||||
Stream openExitStream(String hostname, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
|
||||
|
||||
/**
|
||||
* Return true if the final node of this circuit is believed to be able to connect to
|
||||
* the specified <code>ExitTarget</code>. Returns false if the target destination is
|
||||
* not permitted by the exit policy of the final node in this circuit or if the target
|
||||
* has been previously recorded to have failed through this circuit.
|
||||
*
|
||||
* @param target The exit destination.
|
||||
* @return Return true if is likely that the final node of this circuit can connect to the specified exit target.
|
||||
*/
|
||||
boolean canHandleExitTo(ExitTarget target);
|
||||
|
||||
boolean canHandleExitToPort(int port);
|
||||
/**
|
||||
* Records the specified <code>ExitTarget</code> as a failed connection so that {@link #canHandleExitTo(ExitTarget)} will
|
||||
* no longer return true for this exit destination.
|
||||
*
|
||||
* @param target The <code>ExitTarget</code> to which a connection has failed through this circuit.
|
||||
*/
|
||||
public void recordFailedExitTarget(ExitTarget target);
|
||||
|
||||
}
|
18
orchid/src/com/subgraph/orchid/GuardEntry.java
Normal file
18
orchid/src/com/subgraph/orchid/GuardEntry.java
Normal file
@ -0,0 +1,18 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public interface GuardEntry {
|
||||
boolean isAdded();
|
||||
void markAsDown();
|
||||
void clearDownSince();
|
||||
String getNickname();
|
||||
String getIdentity();
|
||||
String getVersion();
|
||||
Date getCreatedTime();
|
||||
Date getDownSince();
|
||||
Date getLastConnectAttempt();
|
||||
Date getUnlistedSince();
|
||||
boolean testCurrentlyUsable();
|
||||
Router getRouterForEntry();
|
||||
}
|
8
orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java
Normal file
8
orchid/src/com/subgraph/orchid/HiddenServiceCircuit.java
Normal file
@ -0,0 +1,8 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
|
||||
public interface HiddenServiceCircuit extends Circuit {
|
||||
Stream openStream(int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException;
|
||||
}
|
7
orchid/src/com/subgraph/orchid/InternalCircuit.java
Normal file
7
orchid/src/com/subgraph/orchid/InternalCircuit.java
Normal file
@ -0,0 +1,7 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public interface InternalCircuit extends Circuit {
|
||||
DirectoryCircuit cannibalizeToDirectory(Router target);
|
||||
Circuit cannibalizeToIntroductionPoint(Router target);
|
||||
HiddenServiceCircuit connectHiddenService(CircuitNode node);
|
||||
}
|
78
orchid/src/com/subgraph/orchid/KeyCertificate.java
Normal file
78
orchid/src/com/subgraph/orchid/KeyCertificate.java
Normal file
@ -0,0 +1,78 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.Timestamp;
|
||||
|
||||
/**
|
||||
* This class represents a key certificate document as specified in
|
||||
* dir-spec.txt (section 3.1). These documents are published by
|
||||
* directory authorities and bind a long-term identity key to a
|
||||
* more temporary signing key.
|
||||
*/
|
||||
public interface KeyCertificate extends Document {
|
||||
/**
|
||||
* Return the network address of this directory authority
|
||||
* or <code>null</code> if no address was specified in the certificate.
|
||||
*
|
||||
* @return The network address of the directory authority this certificate
|
||||
* belongs to, or <code>null</code> if not available.
|
||||
*/
|
||||
IPv4Address getDirectoryAddress();
|
||||
|
||||
/**
|
||||
* Return the port on which this directory authority answers
|
||||
* directory requests or 0 if no port was specified in the certificate.
|
||||
*
|
||||
* @return The port of this directory authority listens on or 0 if
|
||||
* no port was specified in the certificate.
|
||||
*/
|
||||
int getDirectoryPort();
|
||||
|
||||
/**
|
||||
* Return fingerprint of the authority identity key as specified in
|
||||
* the certificate.
|
||||
*
|
||||
* @return The authority identity key fingerprint.
|
||||
*/
|
||||
HexDigest getAuthorityFingerprint();
|
||||
|
||||
/**
|
||||
* Return the authority identity public key from the certificate.
|
||||
*
|
||||
* @return The authority identity public key.
|
||||
*/
|
||||
TorPublicKey getAuthorityIdentityKey();
|
||||
|
||||
/**
|
||||
* Return the authority signing public key from the certificate.
|
||||
*
|
||||
* @return The authority signing public key.
|
||||
*/
|
||||
TorPublicKey getAuthoritySigningKey();
|
||||
|
||||
/**
|
||||
* Return the time when this document and corresponding keys were
|
||||
* generated.
|
||||
*
|
||||
* @return The time this document was generated and published.
|
||||
*/
|
||||
Timestamp getKeyPublishedTime();
|
||||
|
||||
/**
|
||||
* Return the time after which this document and signing key are
|
||||
* no longer valid.
|
||||
*
|
||||
* @return The expiry time of this document and signing key.
|
||||
*/
|
||||
Timestamp getKeyExpiryTime();
|
||||
|
||||
/**
|
||||
* Return <code>true</code> if the current time is past the key
|
||||
* expiry time of this certificate.
|
||||
*
|
||||
* @return True if this certificate is currently expired.
|
||||
*/
|
||||
boolean isExpired();
|
||||
}
|
13
orchid/src/com/subgraph/orchid/OpenFailedException.java
Normal file
13
orchid/src/com/subgraph/orchid/OpenFailedException.java
Normal file
@ -0,0 +1,13 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class OpenFailedException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1989001056577214666L;
|
||||
|
||||
public OpenFailedException() {
|
||||
}
|
||||
|
||||
public OpenFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
65
orchid/src/com/subgraph/orchid/RelayCell.java
Normal file
65
orchid/src/com/subgraph/orchid/RelayCell.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
|
||||
|
||||
public interface RelayCell extends Cell {
|
||||
|
||||
final static int LENGTH_OFFSET = 12;
|
||||
final static int RECOGNIZED_OFFSET = 4;
|
||||
final static int DIGEST_OFFSET = 8;
|
||||
final static int HEADER_SIZE = 14;
|
||||
|
||||
final static int RELAY_BEGIN = 1;
|
||||
final static int RELAY_DATA = 2;
|
||||
final static int RELAY_END = 3;
|
||||
final static int RELAY_CONNECTED = 4;
|
||||
final static int RELAY_SENDME = 5;
|
||||
final static int RELAY_EXTEND = 6;
|
||||
final static int RELAY_EXTENDED = 7;
|
||||
final static int RELAY_TRUNCATE = 8;
|
||||
final static int RELAY_TRUNCATED = 9;
|
||||
final static int RELAY_DROP = 10;
|
||||
final static int RELAY_RESOLVE = 11;
|
||||
final static int RELAY_RESOLVED = 12;
|
||||
final static int RELAY_BEGIN_DIR = 13;
|
||||
final static int RELAY_EXTEND2 = 14;
|
||||
final static int RELAY_EXTENDED2 = 15;
|
||||
|
||||
final static int RELAY_COMMAND_ESTABLISH_INTRO = 32;
|
||||
final static int RELAY_COMMAND_ESTABLISH_RENDEZVOUS = 33;
|
||||
final static int RELAY_COMMAND_INTRODUCE1 = 34;
|
||||
final static int RELAY_COMMAND_INTRODUCE2 = 35;
|
||||
final static int RELAY_COMMAND_RENDEZVOUS1 = 36;
|
||||
final static int RELAY_COMMAND_RENDEZVOUS2 = 37;
|
||||
final static int RELAY_COMMAND_INTRO_ESTABLISHED = 38;
|
||||
final static int RELAY_COMMAND_RENDEZVOUS_ESTABLISHED = 39;
|
||||
final static int RELAY_COMMAND_INTRODUCE_ACK = 40;
|
||||
|
||||
final static int REASON_MISC = 1;
|
||||
final static int REASON_RESOLVEFAILED = 2;
|
||||
final static int REASON_CONNECTREFUSED = 3;
|
||||
final static int REASON_EXITPOLICY = 4;
|
||||
final static int REASON_DESTROY = 5;
|
||||
final static int REASON_DONE = 6;
|
||||
final static int REASON_TIMEOUT = 7;
|
||||
final static int REASON_NOROUTE = 8;
|
||||
final static int REASON_HIBERNATING = 9;
|
||||
final static int REASON_INTERNAL = 10;
|
||||
final static int REASON_RESOURCELIMIT = 11;
|
||||
final static int REASON_CONNRESET = 12;
|
||||
final static int REASON_TORPROTOCOL = 13;
|
||||
final static int REASON_NOTDIRECTORY = 14;
|
||||
|
||||
int getStreamId();
|
||||
int getRelayCommand();
|
||||
/**
|
||||
* Return the circuit node this cell was received from for outgoing cells or the destination circuit node
|
||||
* for outgoing cells.
|
||||
*/
|
||||
CircuitNode getCircuitNode();
|
||||
ByteBuffer getPayloadBuffer();
|
||||
void setLength();
|
||||
void setDigest(byte[] digest);
|
||||
}
|
35
orchid/src/com/subgraph/orchid/Revision.java
Normal file
35
orchid/src/com/subgraph/orchid/Revision.java
Normal file
@ -0,0 +1,35 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Revision {
|
||||
private final static String REVISION_FILE_PATH = "/build-revision";
|
||||
|
||||
public static String getBuildRevision() {
|
||||
final InputStream input = tryResourceOpen();
|
||||
if(input == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return readFirstLine(input);
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream tryResourceOpen() {
|
||||
return Revision.class.getResourceAsStream(REVISION_FILE_PATH);
|
||||
}
|
||||
|
||||
private static String readFirstLine(InputStream input) throws IOException {
|
||||
try {
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
|
||||
return reader.readLine();
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
}
|
47
orchid/src/com/subgraph/orchid/Router.java
Normal file
47
orchid/src/com/subgraph/orchid/Router.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
|
||||
public interface Router {
|
||||
|
||||
String getNickname();
|
||||
String getCountryCode();
|
||||
IPv4Address getAddress();
|
||||
int getOnionPort();
|
||||
int getDirectoryPort();
|
||||
TorPublicKey getIdentityKey();
|
||||
HexDigest getIdentityHash();
|
||||
boolean isDescriptorDownloadable();
|
||||
|
||||
String getVersion();
|
||||
Descriptor getCurrentDescriptor();
|
||||
HexDigest getDescriptorDigest();
|
||||
HexDigest getMicrodescriptorDigest();
|
||||
|
||||
TorPublicKey getOnionKey();
|
||||
byte[] getNTorOnionKey();
|
||||
|
||||
boolean hasBandwidth();
|
||||
int getEstimatedBandwidth();
|
||||
int getMeasuredBandwidth();
|
||||
|
||||
Set<String> getFamilyMembers();
|
||||
int getAverageBandwidth();
|
||||
int getBurstBandwidth();
|
||||
int getObservedBandwidth();
|
||||
boolean isHibernating();
|
||||
boolean isRunning();
|
||||
boolean isValid();
|
||||
boolean isBadExit();
|
||||
boolean isPossibleGuard();
|
||||
boolean isExit();
|
||||
boolean isFast();
|
||||
boolean isStable();
|
||||
boolean isHSDirectory();
|
||||
boolean exitPolicyAccepts(IPv4Address address, int port);
|
||||
boolean exitPolicyAccepts(int port);
|
||||
}
|
164
orchid/src/com/subgraph/orchid/RouterDescriptor.java
Normal file
164
orchid/src/com/subgraph/orchid/RouterDescriptor.java
Normal file
@ -0,0 +1,164 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.Timestamp;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitPolicy;
|
||||
|
||||
/**
|
||||
* Directory information about a single onion router. This interface
|
||||
* provides access to the fields of a router descriptor document which
|
||||
* has been published through to Tor directory system.
|
||||
*/
|
||||
public interface RouterDescriptor extends Descriptor {
|
||||
/**
|
||||
* Returns the nickname of this router.
|
||||
*
|
||||
* @return The nickname of this router.
|
||||
*/
|
||||
String getNickname();
|
||||
|
||||
|
||||
/**
|
||||
* Return the port on which this router provides directory related
|
||||
* HTTP connections, or 0 if this node does not provide directory
|
||||
* services.
|
||||
*
|
||||
* @return The directory service port, or 0 if not a directory server.
|
||||
*/
|
||||
int getDirectoryPort();
|
||||
|
||||
/**
|
||||
* Returns the volume of traffic in bytes per second that this router
|
||||
* is willing to sustain over long periods.
|
||||
*
|
||||
* @return The average bandwidth of this router in bytes per second.
|
||||
*/
|
||||
int getAverageBandwidth();
|
||||
|
||||
/**
|
||||
* Returns the volume of traffic in bytes per second that this router
|
||||
* is willing to sustain in very short intervals.
|
||||
*
|
||||
* @return The burst bandwidth of this router in bytes per second.
|
||||
*/
|
||||
int getBurstBandwidth();
|
||||
|
||||
/**
|
||||
* Returns the volume of traffic in bytes per second that this router
|
||||
* is estimated to be able to sustain.
|
||||
*
|
||||
* @return The observed bandwidth capacity of this router in bytes per second.
|
||||
*/
|
||||
int getObservedBandwidth();
|
||||
|
||||
/**
|
||||
* Return a human-readable string describing the system on which this router
|
||||
* is running, including possibly the operating system version and Tor
|
||||
* implementation version.
|
||||
*
|
||||
* @return A string describing the platform this router is running on.
|
||||
*/
|
||||
String getPlatform();
|
||||
|
||||
/**
|
||||
* Return the time this descriptor was generated.
|
||||
*
|
||||
* @return The time this descriptor was generated.
|
||||
*/
|
||||
Timestamp getPublishedTime();
|
||||
|
||||
/**
|
||||
* Return a fingerprint of the public key of this router. The fingerprint
|
||||
* is an optional field, so this method may return null if the descriptor
|
||||
* of the router did not include the 'fingerprint' field.
|
||||
*
|
||||
* @return The fingerprint of this router, or null if no fingerprint is available.
|
||||
*/
|
||||
HexDigest getFingerprint();
|
||||
|
||||
/**
|
||||
* Return the number of seconds this router has been running.
|
||||
*
|
||||
* @return The number of seconds this router has been running.
|
||||
*/
|
||||
int getUptime();
|
||||
|
||||
/**
|
||||
* Return the long-term identity and signing public key for this
|
||||
* router.
|
||||
*
|
||||
* @return The long-term identity and signing public key for this router.
|
||||
*/
|
||||
TorPublicKey getIdentityKey();
|
||||
|
||||
/**
|
||||
* Return a string which describes how to contact the server's administrator.
|
||||
* This is an optional field, so this method will return null if the descriptor
|
||||
* of this router did not include the 'contact' field.
|
||||
*
|
||||
* @return The contact information for this router, or null if not available.
|
||||
*/
|
||||
String getContact();
|
||||
|
||||
/**
|
||||
* Return true if this router is currently hibernating and not suitable for
|
||||
* building new circuits.
|
||||
*
|
||||
* @return True if this router is currently hibernating.
|
||||
*/
|
||||
boolean isHibernating();
|
||||
|
||||
/**
|
||||
* Returns true if this router stores and serves hidden service descriptors.
|
||||
*
|
||||
* @return True if this router is a hidden service directory.
|
||||
*/
|
||||
boolean isHiddenServiceDirectory();
|
||||
|
||||
/**
|
||||
* Return true if this router is running a version of Tor which supports the
|
||||
* newer enhanced DNS logic. If false, this router should be used for reverse
|
||||
* hostname lookups.
|
||||
*
|
||||
* @return True if this router supports newer enhanced DNS logic.
|
||||
*/
|
||||
boolean supportsEventDNS();
|
||||
|
||||
/**
|
||||
* Returns true if this router is a directory cache that provides extra-info
|
||||
* documents.
|
||||
*
|
||||
* @return True if this router provides an extra-info document directory service.
|
||||
*/
|
||||
boolean cachesExtraInfo();
|
||||
|
||||
/**
|
||||
* Return a digest of this router's extra-info document, or null if not
|
||||
* available. This is an optional field and will only be present if the
|
||||
* 'extra-info-digest' field was present in the original router descriptor.
|
||||
*
|
||||
* @return The digest of the router extra-info-document, or null if not available.
|
||||
*/
|
||||
HexDigest getExtraInfoDigest();
|
||||
|
||||
/**
|
||||
* Return true if this router allows single-hop circuits to make exit connections.
|
||||
*
|
||||
* @return True if this router allows single-hop circuits to make exit connections.
|
||||
*/
|
||||
boolean allowsSingleHopExits();
|
||||
|
||||
/**
|
||||
* Compare two router descriptors and return true if this router descriptor was published
|
||||
* at a later time than the <code>other</code> descriptor.
|
||||
*
|
||||
* @param other Another router descriptor to compare.
|
||||
* @return True if this descriptor was published later than <code>other</code>
|
||||
*/
|
||||
boolean isNewerThan(RouterDescriptor other);
|
||||
|
||||
ExitPolicy getExitPolicy();
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
public interface RouterMicrodescriptor extends Descriptor {
|
||||
|
||||
}
|
24
orchid/src/com/subgraph/orchid/RouterStatus.java
Normal file
24
orchid/src/com/subgraph/orchid/RouterStatus.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.Timestamp;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitPorts;
|
||||
|
||||
public interface RouterStatus {
|
||||
String getNickname();
|
||||
HexDigest getIdentity();
|
||||
HexDigest getDescriptorDigest();
|
||||
HexDigest getMicrodescriptorDigest();
|
||||
Timestamp getPublicationTime();
|
||||
IPv4Address getAddress();
|
||||
int getRouterPort();
|
||||
boolean isDirectory();
|
||||
int getDirectoryPort();
|
||||
boolean hasFlag(String flag);
|
||||
String getVersion();
|
||||
boolean hasBandwidth();
|
||||
int getEstimatedBandwidth();
|
||||
int getMeasuredBandwidth();
|
||||
ExitPorts getExitPorts();
|
||||
}
|
6
orchid/src/com/subgraph/orchid/SocksPortListener.java
Normal file
6
orchid/src/com/subgraph/orchid/SocksPortListener.java
Normal file
@ -0,0 +1,6 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public interface SocksPortListener {
|
||||
void addListeningPort(int port);
|
||||
void stop();
|
||||
}
|
49
orchid/src/com/subgraph/orchid/Stream.java
Normal file
49
orchid/src/com/subgraph/orchid/Stream.java
Normal file
@ -0,0 +1,49 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface Stream {
|
||||
/**
|
||||
* Returns the {@link Circuit} this stream belongs to.
|
||||
*
|
||||
* @return The {@link Circuit} this stream belongs to.
|
||||
*/
|
||||
Circuit getCircuit();
|
||||
|
||||
/**
|
||||
* Returns the stream id value of this stream.
|
||||
*
|
||||
* @return The stream id value of this stream.
|
||||
*/
|
||||
int getStreamId();
|
||||
|
||||
|
||||
CircuitNode getTargetNode();
|
||||
|
||||
/**
|
||||
* Close this stream.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Returns an {@link InputStream} for sending data on this stream.
|
||||
*
|
||||
* @return An {@link InputStream} for transferring data on this stream.
|
||||
*/
|
||||
InputStream getInputStream();
|
||||
|
||||
/**
|
||||
* Returns an {@link OutputStream} for receiving data from this stream.
|
||||
*
|
||||
* @return An {@link OutputStream} for receiving data from this stream.
|
||||
*/
|
||||
OutputStream getOutputStream();
|
||||
|
||||
/**
|
||||
* If the circuit and stream level packaging windows are open for this stream
|
||||
* this method returns immediately, otherwise it blocks until both windows are
|
||||
* open or the stream is closed.
|
||||
*/
|
||||
void waitForSendWindow();
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
public class StreamConnectFailedException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 8103571310659595097L;
|
||||
private final int reason;
|
||||
|
||||
public StreamConnectFailedException(int reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public boolean isReasonRetryable() {
|
||||
return isRetryableReason(reason);
|
||||
}
|
||||
|
||||
/* Copied from edge_reason_is_retriable() since this is not specified */
|
||||
private static boolean isRetryableReason(int reasonCode) {
|
||||
switch(reasonCode) {
|
||||
case RelayCell.REASON_HIBERNATING:
|
||||
case RelayCell.REASON_RESOURCELIMIT:
|
||||
case RelayCell.REASON_RESOLVEFAILED:
|
||||
case RelayCell.REASON_EXITPOLICY:
|
||||
case RelayCell.REASON_MISC:
|
||||
case RelayCell.REASON_NOROUTE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
167
orchid/src/com/subgraph/orchid/Tor.java
Normal file
167
orchid/src/com/subgraph/orchid/Tor.java
Normal file
@ -0,0 +1,167 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.circuits.CircuitManagerImpl;
|
||||
import com.subgraph.orchid.circuits.TorInitializationTracker;
|
||||
import com.subgraph.orchid.config.TorConfigProxy;
|
||||
import com.subgraph.orchid.connections.ConnectionCacheImpl;
|
||||
import com.subgraph.orchid.directory.DirectoryImpl;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
|
||||
import com.subgraph.orchid.socks.SocksPortListenerImpl;
|
||||
|
||||
/**
|
||||
* The <code>Tor</code> class is a collection of static methods for instantiating
|
||||
* various subsystem modules.
|
||||
*/
|
||||
public class Tor {
|
||||
private final static Logger logger = Logger.getLogger(Tor.class.getName());
|
||||
|
||||
public final static int BOOTSTRAP_STATUS_STARTING = 0;
|
||||
public final static int BOOTSTRAP_STATUS_CONN_DIR = 5;
|
||||
public final static int BOOTSTRAP_STATUS_HANDSHAKE_DIR = 10;
|
||||
public final static int BOOTSTRAP_STATUS_ONEHOP_CREATE = 15;
|
||||
public final static int BOOTSTRAP_STATUS_REQUESTING_STATUS = 20;
|
||||
public final static int BOOTSTRAP_STATUS_LOADING_STATUS = 25;
|
||||
public final static int BOOTSTRAP_STATUS_REQUESTING_KEYS = 35;
|
||||
public final static int BOOTSTRAP_STATUS_LOADING_KEYS = 40;
|
||||
public final static int BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS = 45;
|
||||
public final static int BOOTSTRAP_STATUS_LOADING_DESCRIPTORS = 50;
|
||||
public final static int BOOTSTRAP_STATUS_CONN_OR = 80;
|
||||
public final static int BOOTSTRAP_STATUS_HANDSHAKE_OR = 85;
|
||||
public final static int BOOTSTRAP_STATUS_CIRCUIT_CREATE = 90;
|
||||
public final static int BOOTSTRAP_STATUS_DONE = 100;
|
||||
|
||||
|
||||
private final static String implementation = "Orchid";
|
||||
private final static String version = "1.0.0";
|
||||
|
||||
private final static Charset defaultCharset = createDefaultCharset();
|
||||
|
||||
private static Charset createDefaultCharset() {
|
||||
return Charset.forName("ISO-8859-1");
|
||||
}
|
||||
|
||||
public static Charset getDefaultCharset() {
|
||||
return defaultCharset;
|
||||
}
|
||||
|
||||
public static String getBuildRevision() {
|
||||
return Revision.getBuildRevision();
|
||||
}
|
||||
|
||||
public static String getImplementation() {
|
||||
return implementation;
|
||||
}
|
||||
|
||||
public static String getFullVersion() {
|
||||
final String revision = getBuildRevision();
|
||||
if(revision == null || revision.isEmpty()) {
|
||||
return getVersion();
|
||||
} else {
|
||||
return getVersion() + "." + revision;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string describing the version of this software.
|
||||
*
|
||||
* @return A string representation of the software version.
|
||||
*/
|
||||
public static String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if running on Android by inspecting java.runtime.name property.
|
||||
*
|
||||
* @return True if running on Android.
|
||||
*/
|
||||
public static boolean isAndroidRuntime() {
|
||||
final String runtime = System.getProperty("java.runtime.name");
|
||||
return runtime != null && runtime.equals("Android Runtime");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new <code>TorConfig</code> instance.
|
||||
*
|
||||
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
|
||||
* before calling this method to create a <code>TorConfig</code>
|
||||
* @return A new <code>TorConfig</code> instance.
|
||||
* @see TorConfig
|
||||
*/
|
||||
static public TorConfig createConfig() {
|
||||
final TorConfig config = (TorConfig) Proxy.newProxyInstance(TorConfigProxy.class.getClassLoader(), new Class[] { TorConfig.class }, new TorConfigProxy());
|
||||
if(isAndroidRuntime()) {
|
||||
logger.warning("Android Runtime detected, disabling V2 Link protocol");
|
||||
config.setHandshakeV2Enabled(false);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
static public TorInitializationTracker createInitalizationTracker() {
|
||||
return new TorInitializationTracker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new <code>Directory</code> instance.
|
||||
*
|
||||
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
|
||||
* before creating a <code>Directory</code>.
|
||||
* @param config This is a required dependency. You must create a <code>TorConfig</code> before
|
||||
* calling this method to create a <code>Directory</code>
|
||||
* @return A new <code>Directory</code> instance.
|
||||
* @see Directory
|
||||
*/
|
||||
static public Directory createDirectory(TorConfig config, DirectoryStore customDirectoryStore) {
|
||||
return new DirectoryImpl(config, customDirectoryStore);
|
||||
}
|
||||
|
||||
static public ConnectionCache createConnectionCache(TorConfig config, TorInitializationTracker tracker) {
|
||||
return new ConnectionCacheImpl(config, tracker);
|
||||
}
|
||||
/**
|
||||
* Create and return a new <code>CircuitManager</code> instance.
|
||||
*
|
||||
* @return A new <code>CircuitManager</code> instance.
|
||||
* @see CircuitManager
|
||||
*/
|
||||
static public CircuitManager createCircuitManager(TorConfig config, DirectoryDownloaderImpl directoryDownloader, Directory directory, ConnectionCache connectionCache, TorInitializationTracker tracker) {
|
||||
return new CircuitManagerImpl(config, directoryDownloader, directory, connectionCache, tracker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new <code>SocksPortListener</code> instance.
|
||||
*
|
||||
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
|
||||
* before calling this method to create a <code>SocksPortListener</code>.
|
||||
* @param circuitManager This is a required dependency. You must create a <code>CircuitManager</code>
|
||||
* before calling this method to create a <code>SocksPortListener</code>.
|
||||
* @return A new <code>SocksPortListener</code> instance.
|
||||
* @see SocksPortListener
|
||||
*/
|
||||
static public SocksPortListener createSocksPortListener(TorConfig config, CircuitManager circuitManager) {
|
||||
return new SocksPortListenerImpl(config, circuitManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new <code>DirectoryDownloader</code> instance.
|
||||
*
|
||||
* @param logManager This is a required dependency. You must create a <code>LogManager</code>
|
||||
* before calling this method to create a <code>DirectoryDownloader</code>.
|
||||
|
||||
* @param directory This is a required dependency. You must create a <code>Directory</code>
|
||||
* before calling this method to create a <code>DirectoryDownloader</code>
|
||||
*
|
||||
* @param circuitManager This is a required dependency. You must create a <code>CircuitManager</code>
|
||||
* before calling this method to create a <code>DirectoryDownloader</code>.
|
||||
*
|
||||
* @return A new <code>DirectoryDownloader</code> instance.
|
||||
* @see DirectoryDownloaderImpl
|
||||
*/
|
||||
static public DirectoryDownloaderImpl createDirectoryDownloader(TorConfig config, TorInitializationTracker initializationTracker) {
|
||||
return new DirectoryDownloaderImpl(config, initializationTracker);
|
||||
}
|
||||
}
|
217
orchid/src/com/subgraph/orchid/TorClient.java
Normal file
217
orchid/src/com/subgraph/orchid/TorClient.java
Normal file
@ -0,0 +1,217 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import com.subgraph.orchid.circuits.TorInitializationTracker;
|
||||
import com.subgraph.orchid.crypto.PRNGFixes;
|
||||
import com.subgraph.orchid.dashboard.Dashboard;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
|
||||
import com.subgraph.orchid.sockets.OrchidSocketFactory;
|
||||
|
||||
/**
|
||||
* This class is the main entry-point for running a Tor proxy
|
||||
* or client.
|
||||
*/
|
||||
public class TorClient {
|
||||
private final static Logger logger = Logger.getLogger(TorClient.class.getName());
|
||||
private final TorConfig config;
|
||||
private final Directory directory;
|
||||
private final TorInitializationTracker initializationTracker;
|
||||
private final ConnectionCache connectionCache;
|
||||
private final CircuitManager circuitManager;
|
||||
private final SocksPortListener socksListener;
|
||||
private final DirectoryDownloaderImpl directoryDownloader;
|
||||
private final Dashboard dashboard;
|
||||
|
||||
private boolean isStarted = false;
|
||||
private boolean isStopped = false;
|
||||
|
||||
private final CountDownLatch readyLatch;
|
||||
|
||||
public TorClient() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TorClient(DirectoryStore customDirectoryStore) {
|
||||
if(Tor.isAndroidRuntime()) {
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
config = Tor.createConfig();
|
||||
directory = Tor.createDirectory(config, customDirectoryStore);
|
||||
initializationTracker = Tor.createInitalizationTracker();
|
||||
initializationTracker.addListener(createReadyFlagInitializationListener());
|
||||
connectionCache = Tor.createConnectionCache(config, initializationTracker);
|
||||
directoryDownloader = Tor.createDirectoryDownloader(config, initializationTracker);
|
||||
circuitManager = Tor.createCircuitManager(config, directoryDownloader, directory, connectionCache, initializationTracker);
|
||||
socksListener = Tor.createSocksPortListener(config, circuitManager);
|
||||
readyLatch = new CountDownLatch(1);
|
||||
dashboard = new Dashboard();
|
||||
dashboard.addRenderables(circuitManager, directoryDownloader, socksListener);
|
||||
}
|
||||
|
||||
public TorConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public SocketFactory getSocketFactory() {
|
||||
return new OrchidSocketFactory(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start running the Tor client service.
|
||||
*/
|
||||
public synchronized void start() {
|
||||
if(isStarted) {
|
||||
return;
|
||||
}
|
||||
if(isStopped) {
|
||||
throw new IllegalStateException("Cannot restart a TorClient instance. Create a new instance instead.");
|
||||
}
|
||||
logger.info("Starting Orchid (version: "+ Tor.getFullVersion() +")");
|
||||
verifyUnlimitedStrengthPolicyInstalled();
|
||||
directoryDownloader.start(directory);
|
||||
circuitManager.startBuildingCircuits();
|
||||
if(dashboard.isEnabledByProperty()) {
|
||||
dashboard.startListening();
|
||||
}
|
||||
isStarted = true;
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if(!isStarted || isStopped) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socksListener.stop();
|
||||
if(dashboard.isListening()) {
|
||||
dashboard.stopListening();
|
||||
}
|
||||
directoryDownloader.stop();
|
||||
circuitManager.stopBuildingCircuits(true);
|
||||
directory.close();
|
||||
connectionCache.close();
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Unexpected exception while shutting down TorClient instance: "+ e, e);
|
||||
} finally {
|
||||
isStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
public Directory getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public ConnectionCache getConnectionCache() {
|
||||
return connectionCache;
|
||||
}
|
||||
|
||||
public CircuitManager getCircuitManager() {
|
||||
return circuitManager;
|
||||
}
|
||||
|
||||
public void waitUntilReady() throws InterruptedException {
|
||||
readyLatch.await();
|
||||
}
|
||||
|
||||
public void waitUntilReady(long timeout) throws InterruptedException, TimeoutException {
|
||||
if(!readyLatch.await(timeout, TimeUnit.MILLISECONDS)) {
|
||||
throw new TimeoutException();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream openExitStreamTo(String hostname, int port) throws InterruptedException, TimeoutException, OpenFailedException {
|
||||
ensureStarted();
|
||||
return circuitManager.openExitStreamTo(hostname, port);
|
||||
}
|
||||
|
||||
private synchronized void ensureStarted() {
|
||||
if(!isStarted) {
|
||||
throw new IllegalStateException("Must call start() first");
|
||||
}
|
||||
}
|
||||
|
||||
public void enableSocksListener(int port) {
|
||||
socksListener.addListeningPort(port);
|
||||
}
|
||||
|
||||
public void enableSocksListener() {
|
||||
enableSocksListener(9150);
|
||||
}
|
||||
|
||||
public void enableDashboard() {
|
||||
if(!dashboard.isListening()) {
|
||||
dashboard.startListening();
|
||||
}
|
||||
}
|
||||
|
||||
public void enableDashboard(int port) {
|
||||
dashboard.setListeningPort(port);
|
||||
enableDashboard();
|
||||
}
|
||||
|
||||
public void disableDashboard() {
|
||||
if(dashboard.isListening()) {
|
||||
dashboard.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
public void addInitializationListener(TorInitializationListener listener) {
|
||||
initializationTracker.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeInitializationListener(TorInitializationListener listener) {
|
||||
initializationTracker.removeListener(listener);
|
||||
}
|
||||
|
||||
private TorInitializationListener createReadyFlagInitializationListener() {
|
||||
return new TorInitializationListener() {
|
||||
public void initializationProgress(String message, int percent) {}
|
||||
public void initializationCompleted() {
|
||||
readyLatch.countDown();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
final TorClient client = new TorClient();
|
||||
client.addInitializationListener(createInitalizationListner());
|
||||
client.start();
|
||||
client.enableSocksListener();
|
||||
}
|
||||
|
||||
private static TorInitializationListener createInitalizationListner() {
|
||||
return new TorInitializationListener() {
|
||||
|
||||
public void initializationProgress(String message, int percent) {
|
||||
System.out.println(">>> [ "+ percent + "% ]: "+ message);
|
||||
}
|
||||
|
||||
public void initializationCompleted() {
|
||||
System.out.println("Tor is ready to go!");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void verifyUnlimitedStrengthPolicyInstalled() {
|
||||
try {
|
||||
if(Cipher.getMaxAllowedKeyLength("AES") < 256) {
|
||||
final String message = "Unlimited Strength Jurisdiction Policy Files are required but not installed.";
|
||||
logger.severe(message);
|
||||
throw new TorException(message);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.log(Level.SEVERE, "No AES provider found");
|
||||
throw new TorException(e);
|
||||
} catch (NoSuchMethodError e) {
|
||||
logger.info("Skipped check for Unlimited Strength Jurisdiction Policy Files");
|
||||
}
|
||||
}
|
||||
}
|
151
orchid/src/com/subgraph/orchid/TorConfig.java
Normal file
151
orchid/src/com/subgraph/orchid/TorConfig.java
Normal file
@ -0,0 +1,151 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.subgraph.orchid.circuits.hs.HSDescriptorCookie;
|
||||
import com.subgraph.orchid.config.TorConfigBridgeLine;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
|
||||
|
||||
public interface TorConfig {
|
||||
|
||||
@ConfigVar(type=ConfigVarType.PATH, defaultValue="~/.orchid")
|
||||
File getDataDirectory();
|
||||
void setDataDirectory(File directory);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="60 seconds")
|
||||
long getCircuitBuildTimeout();
|
||||
void setCircuitBuildTimeout(long time, TimeUnit unit);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="0")
|
||||
long getCircuitStreamTimeout();
|
||||
void setCircuitStreamTimeout(long time, TimeUnit unit);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="1 hour")
|
||||
long getCircuitIdleTimeout();
|
||||
void setCircuitIdleTimeout(long time, TimeUnit unit);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="30 seconds")
|
||||
long getNewCircuitPeriod();
|
||||
void setNewCircuitPeriod(long time, TimeUnit unit);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="10 minutes")
|
||||
long getMaxCircuitDirtiness();
|
||||
void setMaxCircuitDirtiness(long time, TimeUnit unit);
|
||||
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTEGER, defaultValue="32")
|
||||
int getMaxClientCircuitsPending();
|
||||
void setMaxClientCircuitsPending(int value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getEnforceDistinctSubnets();
|
||||
void setEnforceDistinctSubnets(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTERVAL, defaultValue="2 minutes")
|
||||
long getSocksTimeout();
|
||||
void setSocksTimeout(long value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.INTEGER, defaultValue="3")
|
||||
int getNumEntryGuards();
|
||||
void setNumEntryGuards(int value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getUseEntryGuards();
|
||||
void setUseEntryGuards(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.PORTLIST, defaultValue="21,22,706,1863,5050,5190,5222,5223,6523,6667,6697,8300")
|
||||
List<Integer> getLongLivedPorts();
|
||||
void setLongLivedPorts(List<Integer> ports);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.STRINGLIST)
|
||||
List<String> getExcludeNodes();
|
||||
void setExcludeNodes(List<String> nodes);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.STRINGLIST)
|
||||
List<String> getExcludeExitNodes();
|
||||
|
||||
void setExcludeExitNodes(List<String> nodes);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.STRINGLIST)
|
||||
List<String> getExitNodes();
|
||||
void setExitNodes(List<String> nodes);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.STRINGLIST)
|
||||
List<String> getEntryNodes();
|
||||
void setEntryNodes(List<String> nodes);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
|
||||
boolean getStrictNodes();
|
||||
void setStrictNodes(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
|
||||
boolean getFascistFirewall();
|
||||
void setFascistFirewall(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.PORTLIST, defaultValue="80,443")
|
||||
List<Integer> getFirewallPorts();
|
||||
void setFirewallPorts(List<Integer> ports);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
|
||||
boolean getSafeSocks();
|
||||
void setSafeSocks(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getSafeLogging();
|
||||
void setSafeLogging(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getWarnUnsafeSocks();
|
||||
void setWarnUnsafeSocks(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getClientRejectInternalAddress();
|
||||
void setClientRejectInternalAddress(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getHandshakeV3Enabled();
|
||||
void setHandshakeV3Enabled(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="true")
|
||||
boolean getHandshakeV2Enabled();
|
||||
void setHandshakeV2Enabled(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.HS_AUTH)
|
||||
HSDescriptorCookie getHidServAuth(String key);
|
||||
void addHidServAuth(String key, String value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.AUTOBOOL, defaultValue="auto")
|
||||
AutoBoolValue getUseNTorHandshake();
|
||||
void setUseNTorHandshake(AutoBoolValue value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.AUTOBOOL, defaultValue="auto")
|
||||
AutoBoolValue getUseMicrodescriptors();
|
||||
void setUseMicrodescriptors(AutoBoolValue value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BOOLEAN, defaultValue="false")
|
||||
boolean getUseBridges();
|
||||
void setUseBridges(boolean value);
|
||||
|
||||
@ConfigVar(type=ConfigVarType.BRIDGE_LINE)
|
||||
List<TorConfigBridgeLine> getBridges();
|
||||
void addBridge(IPv4Address address, int port);
|
||||
void addBridge(IPv4Address address, int port, HexDigest fingerprint);
|
||||
|
||||
enum ConfigVarType { INTEGER, STRING, HS_AUTH, BOOLEAN, INTERVAL, PORTLIST, STRINGLIST, PATH, AUTOBOOL, BRIDGE_LINE };
|
||||
enum AutoBoolValue { TRUE, FALSE, AUTO }
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@interface ConfigVar {
|
||||
ConfigVarType type();
|
||||
String defaultValue() default "";
|
||||
}
|
||||
}
|
22
orchid/src/com/subgraph/orchid/TorException.java
Normal file
22
orchid/src/com/subgraph/orchid/TorException.java
Normal file
@ -0,0 +1,22 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public class TorException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 2462760291055303580L;
|
||||
|
||||
public TorException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public TorException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public TorException(String message, Throwable ex) {
|
||||
super(message, ex);
|
||||
}
|
||||
|
||||
public TorException(Throwable ex) {
|
||||
super(ex);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
public interface TorInitializationListener {
|
||||
void initializationProgress(String message, int percent);
|
||||
void initializationCompleted();
|
||||
}
|
14
orchid/src/com/subgraph/orchid/TorParsingException.java
Normal file
14
orchid/src/com/subgraph/orchid/TorParsingException.java
Normal file
@ -0,0 +1,14 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
|
||||
public class TorParsingException extends TorException {
|
||||
public TorParsingException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
public TorParsingException(String string, Throwable ex) {
|
||||
super(string, ex);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = -4997757416476363399L;
|
||||
}
|
19
orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java
Normal file
19
orchid/src/com/subgraph/orchid/VoteAuthorityEntry.java
Normal file
@ -0,0 +1,19 @@
|
||||
package com.subgraph.orchid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.directory.consensus.DirectorySignature;
|
||||
|
||||
public interface VoteAuthorityEntry {
|
||||
String getNickname();
|
||||
HexDigest getIdentity();
|
||||
String getHostname();
|
||||
IPv4Address getAddress();
|
||||
int getDirectoryPort();
|
||||
int getRouterPort();
|
||||
String getContact();
|
||||
HexDigest getVoteDigest();
|
||||
List<DirectorySignature> getSignatures();
|
||||
}
|
127
orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java
Normal file
127
orchid/src/com/subgraph/orchid/circuits/CircuitBuildTask.java
Normal file
@ -0,0 +1,127 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.ConnectionCache;
|
||||
import com.subgraph.orchid.ConnectionFailedException;
|
||||
import com.subgraph.orchid.ConnectionHandshakeException;
|
||||
import com.subgraph.orchid.ConnectionTimeoutException;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Tor;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
|
||||
public class CircuitBuildTask implements Runnable {
|
||||
private final static Logger logger = Logger.getLogger(CircuitBuildTask.class.getName());
|
||||
private final CircuitCreationRequest creationRequest;
|
||||
private final ConnectionCache connectionCache;
|
||||
private final TorInitializationTracker initializationTracker;
|
||||
private final CircuitImpl circuit;
|
||||
private final CircuitExtender extender;
|
||||
|
||||
private Connection connection = null;
|
||||
|
||||
public CircuitBuildTask(CircuitCreationRequest request, ConnectionCache connectionCache, boolean ntorEnabled) {
|
||||
this(request, connectionCache, ntorEnabled, null);
|
||||
}
|
||||
|
||||
public CircuitBuildTask(CircuitCreationRequest request, ConnectionCache connectionCache, boolean ntorEnabled, TorInitializationTracker initializationTracker) {
|
||||
this.creationRequest = request;
|
||||
this.connectionCache = connectionCache;
|
||||
this.initializationTracker = initializationTracker;
|
||||
this.circuit = request.getCircuit();
|
||||
this.extender = new CircuitExtender(request.getCircuit(), ntorEnabled);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Router firstRouter = null;
|
||||
try {
|
||||
circuit.notifyCircuitBuildStart();
|
||||
creationRequest.choosePath();
|
||||
if(logger.isLoggable(Level.FINE)) {
|
||||
logger.fine("Opening a new circuit to "+ pathToString(creationRequest));
|
||||
}
|
||||
firstRouter = creationRequest.getPathElement(0);
|
||||
openEntryNodeConnection(firstRouter);
|
||||
buildCircuit(firstRouter);
|
||||
circuit.notifyCircuitBuildCompleted();
|
||||
} catch (ConnectionTimeoutException e) {
|
||||
connectionFailed("Timeout connecting to "+ firstRouter);
|
||||
} catch (ConnectionFailedException e) {
|
||||
connectionFailed("Connection failed to "+ firstRouter + " : " + e.getMessage());
|
||||
} catch (ConnectionHandshakeException e) {
|
||||
connectionFailed("Handshake error connecting to "+ firstRouter + " : " + e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
circuitBuildFailed("Circuit building thread interrupted");
|
||||
} catch(PathSelectionFailedException e) {
|
||||
circuitBuildFailed(e.getMessage());
|
||||
} catch (TorException e) {
|
||||
circuitBuildFailed(e.getMessage());
|
||||
} catch(Exception e) {
|
||||
circuitBuildFailed("Unexpected exception: "+ e);
|
||||
logger.log(Level.WARNING, "Unexpected exception while building circuit: "+ e, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String pathToString(CircuitCreationRequest ccr) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
for(Router r: ccr.getPath()) {
|
||||
if(sb.length() > 1)
|
||||
sb.append(",");
|
||||
sb.append(r.getNickname());
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void connectionFailed(String message) {
|
||||
creationRequest.connectionFailed(message);
|
||||
circuit.notifyCircuitBuildFailed();
|
||||
}
|
||||
|
||||
private void circuitBuildFailed(String message) {
|
||||
creationRequest.circuitBuildFailed(message);
|
||||
circuit.notifyCircuitBuildFailed();
|
||||
if(connection != null) {
|
||||
connection.removeCircuit(circuit);
|
||||
}
|
||||
}
|
||||
|
||||
private void openEntryNodeConnection(Router firstRouter) throws ConnectionTimeoutException, ConnectionFailedException, ConnectionHandshakeException, InterruptedException {
|
||||
connection = connectionCache.getConnectionTo(firstRouter, creationRequest.isDirectoryCircuit());
|
||||
circuit.bindToConnection(connection);
|
||||
creationRequest.connectionCompleted(connection);
|
||||
}
|
||||
|
||||
private void buildCircuit(Router firstRouter) throws TorException {
|
||||
notifyInitialization();
|
||||
final CircuitNode firstNode = extender.createFastTo(firstRouter);
|
||||
creationRequest.nodeAdded(firstNode);
|
||||
|
||||
for(int i = 1; i < creationRequest.getPathLength(); i++) {
|
||||
final CircuitNode extendedNode = extender.extendTo(creationRequest.getPathElement(i));
|
||||
creationRequest.nodeAdded(extendedNode);
|
||||
}
|
||||
creationRequest.circuitBuildCompleted(circuit);
|
||||
notifyDone();
|
||||
}
|
||||
|
||||
private void notifyInitialization() {
|
||||
if(initializationTracker != null) {
|
||||
final int event = creationRequest.isDirectoryCircuit() ?
|
||||
Tor.BOOTSTRAP_STATUS_ONEHOP_CREATE : Tor.BOOTSTRAP_STATUS_CIRCUIT_CREATE;
|
||||
initializationTracker.notifyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDone() {
|
||||
if(initializationTracker != null && !creationRequest.isDirectoryCircuit()) {
|
||||
initializationTracker.notifyEvent(Tor.BOOTSTRAP_STATUS_DONE);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitBuildHandler;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
|
||||
public class CircuitCreationRequest implements CircuitBuildHandler {
|
||||
private final CircuitImpl circuit;
|
||||
private final CircuitPathChooser pathChooser;
|
||||
private final CircuitBuildHandler buildHandler;
|
||||
private final boolean isDirectoryCircuit;
|
||||
|
||||
private List<Router> path;
|
||||
|
||||
public CircuitCreationRequest(CircuitPathChooser pathChooser, Circuit circuit, CircuitBuildHandler buildHandler, boolean isDirectoryCircuit) {
|
||||
this.pathChooser = pathChooser;
|
||||
this.circuit = (CircuitImpl) circuit;
|
||||
this.buildHandler = buildHandler;
|
||||
this.path = Collections.emptyList();
|
||||
this.isDirectoryCircuit = isDirectoryCircuit;
|
||||
}
|
||||
|
||||
void choosePath() throws InterruptedException, PathSelectionFailedException {
|
||||
if(!(circuit instanceof CircuitImpl)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
path = ((CircuitImpl)circuit).choosePath(pathChooser);
|
||||
|
||||
}
|
||||
|
||||
CircuitImpl getCircuit() {
|
||||
return circuit;
|
||||
}
|
||||
|
||||
List<Router> getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
int getPathLength() {
|
||||
return path.size();
|
||||
}
|
||||
|
||||
Router getPathElement(int idx) {
|
||||
return path.get(idx);
|
||||
}
|
||||
|
||||
CircuitBuildHandler getBuildHandler() {
|
||||
return buildHandler;
|
||||
}
|
||||
|
||||
boolean isDirectoryCircuit() {
|
||||
return isDirectoryCircuit;
|
||||
}
|
||||
|
||||
public void connectionCompleted(Connection connection) {
|
||||
if(buildHandler != null) {
|
||||
buildHandler.connectionCompleted(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionFailed(String reason) {
|
||||
if(buildHandler != null) {
|
||||
buildHandler.connectionFailed(reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void nodeAdded(CircuitNode node) {
|
||||
if(buildHandler != null) {
|
||||
buildHandler.nodeAdded(node);
|
||||
}
|
||||
}
|
||||
|
||||
public void circuitBuildCompleted(Circuit circuit) {
|
||||
if(buildHandler != null) {
|
||||
buildHandler.circuitBuildCompleted(circuit);
|
||||
}
|
||||
}
|
||||
|
||||
public void circuitBuildFailed(String reason) {
|
||||
if(buildHandler != null) {
|
||||
buildHandler.circuitBuildFailed(reason);
|
||||
}
|
||||
}
|
||||
}
|
292
orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java
Normal file
292
orchid/src/com/subgraph/orchid/circuits/CircuitCreationTask.java
Normal file
@ -0,0 +1,292 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitBuildHandler;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.ConnectionCache;
|
||||
import com.subgraph.orchid.Directory;
|
||||
import com.subgraph.orchid.ExitCircuit;
|
||||
import com.subgraph.orchid.InternalCircuit;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorConfig;
|
||||
import com.subgraph.orchid.circuits.CircuitManagerImpl.CircuitFilter;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
|
||||
public class CircuitCreationTask implements Runnable {
|
||||
private final static Logger logger = Logger.getLogger(CircuitCreationTask.class.getName());
|
||||
private final static int MAX_CIRCUIT_DIRTINESS = 300; // seconds
|
||||
private final static int MAX_PENDING_CIRCUITS = 4;
|
||||
|
||||
private final TorConfig config;
|
||||
private final Directory directory;
|
||||
private final ConnectionCache connectionCache;
|
||||
private final CircuitManagerImpl circuitManager;
|
||||
private final TorInitializationTracker initializationTracker;
|
||||
private final CircuitPathChooser pathChooser;
|
||||
private final Executor executor;
|
||||
private final CircuitBuildHandler buildHandler;
|
||||
private final CircuitBuildHandler internalBuildHandler;
|
||||
// To avoid obnoxiously printing a warning every second
|
||||
private int notEnoughDirectoryInformationWarningCounter = 0;
|
||||
|
||||
private final CircuitPredictor predictor;
|
||||
|
||||
private final AtomicLong lastNewCircuit;
|
||||
|
||||
CircuitCreationTask(TorConfig config, Directory directory, ConnectionCache connectionCache, CircuitPathChooser pathChooser, CircuitManagerImpl circuitManager, TorInitializationTracker initializationTracker) {
|
||||
this.config = config;
|
||||
this.directory = directory;
|
||||
this.connectionCache = connectionCache;
|
||||
this.circuitManager = circuitManager;
|
||||
this.initializationTracker = initializationTracker;
|
||||
this.pathChooser = pathChooser;
|
||||
this.executor = Executors.newCachedThreadPool();
|
||||
this.buildHandler = createCircuitBuildHandler();
|
||||
this.internalBuildHandler = createInternalCircuitBuildHandler();
|
||||
this.predictor = new CircuitPredictor();
|
||||
this.lastNewCircuit = new AtomicLong();
|
||||
}
|
||||
|
||||
CircuitPredictor getCircuitPredictor() {
|
||||
return predictor;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
expireOldCircuits();
|
||||
assignPendingStreamsToActiveCircuits();
|
||||
checkExpiredPendingCircuits();
|
||||
checkCircuitsForCreation();
|
||||
}
|
||||
|
||||
void predictPort(int port) {
|
||||
predictor.addExitPortRequest(port);
|
||||
}
|
||||
|
||||
private void assignPendingStreamsToActiveCircuits() {
|
||||
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
|
||||
if(pendingExitStreams.isEmpty())
|
||||
return;
|
||||
|
||||
for(ExitCircuit c: circuitManager.getRandomlyOrderedListOfExitCircuits()) {
|
||||
final Iterator<StreamExitRequest> it = pendingExitStreams.iterator();
|
||||
while(it.hasNext()) {
|
||||
if(attemptHandleStreamRequest(c, it.next()))
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptHandleStreamRequest(ExitCircuit c, StreamExitRequest request) {
|
||||
if(c.canHandleExitTo(request)) {
|
||||
if(request.reserveRequest()) {
|
||||
launchExitStreamTask(c, request);
|
||||
}
|
||||
// else request is reserved meaning another circuit is already trying to handle it
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void launchExitStreamTask(ExitCircuit circuit, StreamExitRequest exitRequest) {
|
||||
final OpenExitStreamTask task = new OpenExitStreamTask(circuit, exitRequest);
|
||||
executor.execute(task);
|
||||
}
|
||||
|
||||
private void expireOldCircuits() {
|
||||
final Set<Circuit> circuits = circuitManager.getCircuitsByFilter(new CircuitFilter() {
|
||||
|
||||
public boolean filter(Circuit circuit) {
|
||||
return !circuit.isMarkedForClose() && circuit.getSecondsDirty() > MAX_CIRCUIT_DIRTINESS;
|
||||
}
|
||||
});
|
||||
for(Circuit c: circuits) {
|
||||
logger.fine("Closing idle dirty circuit: "+ c);
|
||||
((CircuitImpl)c).markForClose();
|
||||
}
|
||||
}
|
||||
private void checkExpiredPendingCircuits() {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
private void checkCircuitsForCreation() {
|
||||
|
||||
if(!directory.haveMinimumRouterInfo()) {
|
||||
if(notEnoughDirectoryInformationWarningCounter % 20 == 0)
|
||||
logger.info("Cannot build circuits because we don't have enough directory information");
|
||||
notEnoughDirectoryInformationWarningCounter++;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(lastNewCircuit.get() != 0) {
|
||||
final long now = System.currentTimeMillis();
|
||||
if((now - lastNewCircuit.get()) < config.getNewCircuitPeriod()) {
|
||||
// return;
|
||||
}
|
||||
}
|
||||
|
||||
buildCircuitIfNeeded();
|
||||
maybeBuildInternalCircuit();
|
||||
}
|
||||
|
||||
private void buildCircuitIfNeeded() {
|
||||
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
|
||||
final List<PredictedPortTarget> predictedPorts = predictor.getPredictedPortTargets();
|
||||
final List<ExitTarget> exitTargets = new ArrayList<ExitTarget>();
|
||||
for(StreamExitRequest streamRequest: pendingExitStreams) {
|
||||
if(!streamRequest.isReserved() && countCircuitsSupportingTarget(streamRequest, false) == 0) {
|
||||
exitTargets.add(streamRequest);
|
||||
}
|
||||
}
|
||||
for(PredictedPortTarget ppt: predictedPorts) {
|
||||
if(countCircuitsSupportingTarget(ppt, true) < 2) {
|
||||
exitTargets.add(ppt);
|
||||
}
|
||||
}
|
||||
buildCircuitToHandleExitTargets(exitTargets);
|
||||
}
|
||||
|
||||
private void maybeBuildInternalCircuit() {
|
||||
final int needed = circuitManager.getNeededCleanCircuitCount(predictor.isInternalPredicted());
|
||||
|
||||
if(needed > 0) {
|
||||
launchBuildTaskForInternalCircuit();
|
||||
}
|
||||
}
|
||||
|
||||
private void launchBuildTaskForInternalCircuit() {
|
||||
logger.fine("Launching new internal circuit");
|
||||
final InternalCircuitImpl circuit = new InternalCircuitImpl(circuitManager);
|
||||
final CircuitCreationRequest request = new CircuitCreationRequest(pathChooser, circuit, internalBuildHandler, false);
|
||||
final CircuitBuildTask task = new CircuitBuildTask(request, connectionCache, circuitManager.isNtorEnabled());
|
||||
executor.execute(task);
|
||||
circuitManager.incrementPendingInternalCircuitCount();
|
||||
}
|
||||
|
||||
private int countCircuitsSupportingTarget(final ExitTarget target, final boolean needClean) {
|
||||
final CircuitFilter filter = new CircuitFilter() {
|
||||
public boolean filter(Circuit circuit) {
|
||||
if(!(circuit instanceof ExitCircuit)) {
|
||||
return false;
|
||||
}
|
||||
final ExitCircuit ec = (ExitCircuit) circuit;
|
||||
final boolean pendingOrConnected = circuit.isPending() || circuit.isConnected();
|
||||
final boolean isCleanIfNeeded = !(needClean && !circuit.isClean());
|
||||
return pendingOrConnected && isCleanIfNeeded && ec.canHandleExitTo(target);
|
||||
}
|
||||
};
|
||||
return circuitManager.getCircuitsByFilter(filter).size();
|
||||
}
|
||||
|
||||
private void buildCircuitToHandleExitTargets(List<ExitTarget> exitTargets) {
|
||||
if(exitTargets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if(!directory.haveMinimumRouterInfo())
|
||||
return;
|
||||
if(circuitManager.getPendingCircuitCount() >= MAX_PENDING_CIRCUITS)
|
||||
return;
|
||||
|
||||
if(logger.isLoggable(Level.FINE)) {
|
||||
logger.fine("Building new circuit to handle "+ exitTargets.size() +" pending streams and predicted ports");
|
||||
}
|
||||
|
||||
launchBuildTaskForTargets(exitTargets);
|
||||
}
|
||||
|
||||
private void launchBuildTaskForTargets(List<ExitTarget> exitTargets) {
|
||||
final Router exitRouter = pathChooser.chooseExitNodeForTargets(exitTargets);
|
||||
if(exitRouter == null) {
|
||||
logger.warning("Failed to select suitable exit node for targets");
|
||||
return;
|
||||
}
|
||||
|
||||
final Circuit circuit = circuitManager.createNewExitCircuit(exitRouter);
|
||||
final CircuitCreationRequest request = new CircuitCreationRequest(pathChooser, circuit, buildHandler, false);
|
||||
final CircuitBuildTask task = new CircuitBuildTask(request, connectionCache, circuitManager.isNtorEnabled(), initializationTracker);
|
||||
executor.execute(task);
|
||||
}
|
||||
|
||||
private CircuitBuildHandler createCircuitBuildHandler() {
|
||||
return new CircuitBuildHandler() {
|
||||
|
||||
public void circuitBuildCompleted(Circuit circuit) {
|
||||
logger.fine("Circuit completed to: "+ circuit);
|
||||
circuitOpenedHandler(circuit);
|
||||
lastNewCircuit.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public void circuitBuildFailed(String reason) {
|
||||
logger.fine("Circuit build failed: "+ reason);
|
||||
buildCircuitIfNeeded();
|
||||
}
|
||||
|
||||
public void connectionCompleted(Connection connection) {
|
||||
logger.finer("Circuit connection completed to "+ connection);
|
||||
}
|
||||
|
||||
public void connectionFailed(String reason) {
|
||||
logger.fine("Circuit connection failed: "+ reason);
|
||||
buildCircuitIfNeeded();
|
||||
}
|
||||
|
||||
public void nodeAdded(CircuitNode node) {
|
||||
logger.finer("Node added to circuit: "+ node);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void circuitOpenedHandler(Circuit circuit) {
|
||||
if(!(circuit instanceof ExitCircuit)) {
|
||||
return;
|
||||
}
|
||||
final ExitCircuit ec = (ExitCircuit) circuit;
|
||||
final List<StreamExitRequest> pendingExitStreams = circuitManager.getPendingExitStreams();
|
||||
for(StreamExitRequest req: pendingExitStreams) {
|
||||
if(ec.canHandleExitTo(req) && req.reserveRequest()) {
|
||||
launchExitStreamTask(ec, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CircuitBuildHandler createInternalCircuitBuildHandler() {
|
||||
return new CircuitBuildHandler() {
|
||||
|
||||
public void nodeAdded(CircuitNode node) {
|
||||
logger.finer("Node added to internal circuit: "+ node);
|
||||
}
|
||||
|
||||
public void connectionFailed(String reason) {
|
||||
logger.fine("Circuit connection failed: "+ reason);
|
||||
circuitManager.decrementPendingInternalCircuitCount();
|
||||
}
|
||||
|
||||
public void connectionCompleted(Connection connection) {
|
||||
logger.finer("Circuit connection completed to "+ connection);
|
||||
}
|
||||
|
||||
public void circuitBuildFailed(String reason) {
|
||||
logger.fine("Circuit build failed: "+ reason);
|
||||
circuitManager.decrementPendingInternalCircuitCount();
|
||||
}
|
||||
|
||||
public void circuitBuildCompleted(Circuit circuit) {
|
||||
logger.fine("Internal circuit build completed: "+ circuit);
|
||||
lastNewCircuit.set(System.currentTimeMillis());
|
||||
circuitManager.addCleanInternalCircuit((InternalCircuit) circuit);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
155
orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java
Normal file
155
orchid/src/com/subgraph/orchid/circuits/CircuitExtender.java
Normal file
@ -0,0 +1,155 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.cells.CellImpl;
|
||||
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
|
||||
import com.subgraph.orchid.crypto.TorCreateFastKeyAgreement;
|
||||
import com.subgraph.orchid.crypto.TorKeyAgreement;
|
||||
import com.subgraph.orchid.crypto.TorMessageDigest;
|
||||
import com.subgraph.orchid.crypto.TorStreamCipher;
|
||||
|
||||
public class CircuitExtender {
|
||||
private final static Logger logger = Logger.getLogger(CircuitExtender.class.getName());
|
||||
|
||||
private final static int DH_BYTES = 1024 / 8;
|
||||
private final static int PKCS1_OAEP_PADDING_OVERHEAD = 42;
|
||||
private final static int CIPHER_KEY_LEN = TorStreamCipher.KEY_LEN;
|
||||
final static int TAP_ONIONSKIN_LEN = PKCS1_OAEP_PADDING_OVERHEAD + CIPHER_KEY_LEN + DH_BYTES;
|
||||
final static int TAP_ONIONSKIN_REPLY_LEN = DH_BYTES + TorMessageDigest.TOR_DIGEST_SIZE;
|
||||
|
||||
|
||||
private final CircuitImpl circuit;
|
||||
private final boolean ntorEnabled;
|
||||
|
||||
|
||||
CircuitExtender(CircuitImpl circuit, boolean ntorEnabled) {
|
||||
this.circuit = circuit;
|
||||
this.ntorEnabled = ntorEnabled;
|
||||
}
|
||||
|
||||
|
||||
CircuitNode createFastTo(Router targetRouter) {
|
||||
logger.fine("Creating 'fast' to "+ targetRouter);
|
||||
final TorCreateFastKeyAgreement kex = new TorCreateFastKeyAgreement();
|
||||
sendCreateFastCell(kex);
|
||||
return receiveAndProcessCreateFastResponse(targetRouter, kex);
|
||||
}
|
||||
|
||||
private void sendCreateFastCell(TorCreateFastKeyAgreement kex) {
|
||||
final Cell cell = CellImpl.createCell(circuit.getCircuitId(), Cell.CREATE_FAST);
|
||||
cell.putByteArray(kex.createOnionSkin());
|
||||
circuit.sendCell(cell);
|
||||
}
|
||||
|
||||
private CircuitNode receiveAndProcessCreateFastResponse(Router targetRouter, TorKeyAgreement kex) {
|
||||
final Cell cell = circuit.receiveControlCellResponse();
|
||||
if(cell == null) {
|
||||
throw new TorException("Timeout building circuit waiting for CREATE_FAST response from "+ targetRouter);
|
||||
}
|
||||
|
||||
return processCreatedFastCell(targetRouter, cell, kex);
|
||||
}
|
||||
|
||||
private CircuitNode processCreatedFastCell(Router targetRouter, Cell cell, TorKeyAgreement kex) {
|
||||
final byte[] payload = new byte[TorMessageDigest.TOR_DIGEST_SIZE * 2];
|
||||
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
|
||||
final byte[] verifyHash = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
|
||||
cell.getByteArray(payload);
|
||||
if(!kex.deriveKeysFromHandshakeResponse(payload, keyMaterial, verifyHash)) {
|
||||
// XXX
|
||||
return null;
|
||||
}
|
||||
final CircuitNode node = CircuitNodeImpl.createFirstHop(targetRouter, keyMaterial, verifyHash);
|
||||
circuit.appendNode(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
CircuitNode extendTo(Router targetRouter) {
|
||||
if(circuit.getCircuitLength() == 0) {
|
||||
throw new TorException("Cannot EXTEND an empty circuit");
|
||||
}
|
||||
|
||||
if(useNtor(targetRouter)) {
|
||||
final NTorCircuitExtender nce = new NTorCircuitExtender(this, targetRouter);
|
||||
return nce.extendTo();
|
||||
} else {
|
||||
final TapCircuitExtender tce = new TapCircuitExtender(this, targetRouter);
|
||||
return tce.extendTo();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean useNtor(Router targetRouter) {
|
||||
return ntorEnabled && targetRouter.getNTorOnionKey() != null;
|
||||
}
|
||||
|
||||
private void logProtocolViolation(String sourceName, Router targetRouter) {
|
||||
final String version = (targetRouter == null) ? "(none)" : targetRouter.getVersion();
|
||||
final String targetName = (targetRouter == null) ? "(none)" : targetRouter.getNickname();
|
||||
logger.warning("Protocol error extending circuit from ("+ sourceName +") to ("+ targetName +") [version: "+ version +"]");
|
||||
}
|
||||
|
||||
private String nodeToName(CircuitNode node) {
|
||||
if(node == null || node.getRouter() == null) {
|
||||
return "(null)";
|
||||
}
|
||||
final Router router = node.getRouter();
|
||||
return router.getNickname();
|
||||
}
|
||||
|
||||
|
||||
public void sendRelayCell(RelayCell cell) {
|
||||
circuit.sendRelayCell(cell);
|
||||
}
|
||||
|
||||
|
||||
public RelayCell receiveRelayResponse(int expectedCommand, Router extendTarget) {
|
||||
final RelayCell cell = circuit.receiveRelayCell();
|
||||
if(cell == null) {
|
||||
throw new TorException("Timeout building circuit");
|
||||
}
|
||||
final int command = cell.getRelayCommand();
|
||||
if(command == RelayCell.RELAY_TRUNCATED) {
|
||||
final int code = cell.getByte() & 0xFF;
|
||||
final String msg = CellImpl.errorToDescription(code);
|
||||
final String source = nodeToName(cell.getCircuitNode());
|
||||
if(code == Cell.ERROR_PROTOCOL) {
|
||||
logProtocolViolation(source, extendTarget);
|
||||
}
|
||||
throw new TorException("Error from ("+ source +") while extending to ("+ extendTarget.getNickname() + "): "+ msg);
|
||||
} else if(command != expectedCommand) {
|
||||
final String expected = RelayCellImpl.commandToDescription(expectedCommand);
|
||||
final String received = RelayCellImpl.commandToDescription(command);
|
||||
throw new TorException("Received incorrect extend response, expecting "+ expected + " but received "+ received);
|
||||
} else {
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public CircuitNode createNewNode(Router r, byte[] keyMaterial, byte[] verifyDigest) {
|
||||
final CircuitNode node = CircuitNodeImpl.createNode(r, circuit.getFinalCircuitNode(), keyMaterial, verifyDigest);
|
||||
logger.fine("Adding new circuit node for "+ r.getNickname());
|
||||
circuit.appendNode(node);
|
||||
return node;
|
||||
|
||||
}
|
||||
|
||||
public RelayCell createRelayCell(int command) {
|
||||
return new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), 0, command, true);
|
||||
}
|
||||
|
||||
Router getFinalRouter() {
|
||||
final CircuitNode node = circuit.getFinalCircuitNode();
|
||||
if(node != null) {
|
||||
return node.getRouter();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
320
orchid/src/com/subgraph/orchid/circuits/CircuitIO.java
Normal file
320
orchid/src/com/subgraph/orchid/circuits/CircuitIO.java
Normal file
@ -0,0 +1,320 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.ConnectionIOException;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.cells.CellImpl;
|
||||
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderable;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderer;
|
||||
|
||||
public class CircuitIO implements DashboardRenderable {
|
||||
private static final Logger logger = Logger.getLogger(CircuitIO.class.getName());
|
||||
private final static long CIRCUIT_BUILD_TIMEOUT_MS = 30 * 1000;
|
||||
private final static long CIRCUIT_RELAY_RESPONSE_TIMEOUT = 20 * 1000;
|
||||
|
||||
private final CircuitImpl circuit;
|
||||
private final Connection connection;
|
||||
private final int circuitId;
|
||||
|
||||
private final BlockingQueue<RelayCell> relayCellResponseQueue;
|
||||
private final BlockingQueue<Cell> controlCellResponseQueue;
|
||||
private final Map<Integer, StreamImpl> streamMap;
|
||||
private final Object relaySendLock = new Object();
|
||||
|
||||
private boolean isMarkedForClose;
|
||||
private boolean isClosed;
|
||||
|
||||
CircuitIO(CircuitImpl circuit, Connection connection, int circuitId) {
|
||||
this.circuit = circuit;
|
||||
this.connection = connection;
|
||||
this.circuitId = circuitId;
|
||||
|
||||
this.relayCellResponseQueue = new LinkedBlockingQueue<RelayCell>();
|
||||
this.controlCellResponseQueue = new LinkedBlockingQueue<Cell>();
|
||||
this.streamMap = new HashMap<Integer, StreamImpl>();
|
||||
}
|
||||
|
||||
Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
int getCircuitId() {
|
||||
return circuitId;
|
||||
}
|
||||
|
||||
RelayCell dequeueRelayResponseCell() {
|
||||
try {
|
||||
final long timeout = getReceiveTimeout();
|
||||
return relayCellResponseQueue.poll(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private RelayCell decryptRelayCell(Cell cell) {
|
||||
for(CircuitNode node: circuit.getNodeList()) {
|
||||
if(node.decryptBackwardCell(cell)) {
|
||||
return RelayCellImpl.createFromCell(node, cell);
|
||||
}
|
||||
}
|
||||
destroyCircuit();
|
||||
throw new TorException("Could not decrypt relay cell");
|
||||
}
|
||||
|
||||
// Return null on timeout
|
||||
Cell receiveControlCellResponse() {
|
||||
try {
|
||||
final long timeout = getReceiveTimeout();
|
||||
return controlCellResponseQueue.poll(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private long getReceiveTimeout() {
|
||||
if(circuit.getStatus().isBuilding())
|
||||
return remainingBuildTime();
|
||||
else
|
||||
return CIRCUIT_RELAY_RESPONSE_TIMEOUT;
|
||||
}
|
||||
|
||||
private long remainingBuildTime() {
|
||||
final long elapsed = circuit.getStatus().getMillisecondsElapsedSinceCreated();
|
||||
if(elapsed == 0 || elapsed >= CIRCUIT_BUILD_TIMEOUT_MS)
|
||||
return 0;
|
||||
return CIRCUIT_BUILD_TIMEOUT_MS - elapsed;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is called by the cell reading thread in ConnectionImpl to deliver control cells
|
||||
* associated with this circuit (CREATED, CREATED_FAST, or DESTROY).
|
||||
*/
|
||||
void deliverControlCell(Cell cell) {
|
||||
if(cell.getCommand() == Cell.DESTROY) {
|
||||
processDestroyCell(cell.getByte());
|
||||
} else {
|
||||
controlCellResponseQueue.add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private void processDestroyCell(int reason) {
|
||||
logger.fine("DESTROY cell received ("+ CellImpl.errorToDescription(reason) +") on "+ circuit);
|
||||
destroyCircuit();
|
||||
}
|
||||
|
||||
/* This is called by the cell reading thread in ConnectionImpl to deliver RELAY cells. */
|
||||
void deliverRelayCell(Cell cell) {
|
||||
circuit.getStatus().updateDirtyTimestamp();
|
||||
final RelayCell relayCell = decryptRelayCell(cell);
|
||||
logRelayCell("Dispatching: ", relayCell);
|
||||
switch(relayCell.getRelayCommand()) {
|
||||
case RelayCell.RELAY_EXTENDED:
|
||||
case RelayCell.RELAY_EXTENDED2:
|
||||
case RelayCell.RELAY_RESOLVED:
|
||||
case RelayCell.RELAY_TRUNCATED:
|
||||
case RelayCell.RELAY_COMMAND_RENDEZVOUS_ESTABLISHED:
|
||||
case RelayCell.RELAY_COMMAND_INTRODUCE_ACK:
|
||||
case RelayCell.RELAY_COMMAND_RENDEZVOUS2:
|
||||
relayCellResponseQueue.add(relayCell);
|
||||
break;
|
||||
case RelayCell.RELAY_DATA:
|
||||
case RelayCell.RELAY_END:
|
||||
case RelayCell.RELAY_CONNECTED:
|
||||
processRelayDataCell(relayCell);
|
||||
break;
|
||||
|
||||
case RelayCell.RELAY_SENDME:
|
||||
if(relayCell.getStreamId() != 0)
|
||||
processRelayDataCell(relayCell);
|
||||
else
|
||||
processCircuitSendme(relayCell);
|
||||
break;
|
||||
case RelayCell.RELAY_BEGIN:
|
||||
case RelayCell.RELAY_BEGIN_DIR:
|
||||
case RelayCell.RELAY_EXTEND:
|
||||
case RelayCell.RELAY_RESOLVE:
|
||||
case RelayCell.RELAY_TRUNCATE:
|
||||
destroyCircuit();
|
||||
throw new TorException("Unexpected 'forward' direction relay cell type: "+ relayCell.getRelayCommand());
|
||||
}
|
||||
}
|
||||
|
||||
/* Runs in the context of the connection cell reading thread */
|
||||
private void processRelayDataCell(RelayCell cell) {
|
||||
if(cell.getRelayCommand() == RelayCell.RELAY_DATA) {
|
||||
cell.getCircuitNode().decrementDeliverWindow();
|
||||
if(cell.getCircuitNode().considerSendingSendme()) {
|
||||
final RelayCell sendme = createRelayCell(RelayCell.RELAY_SENDME, 0, cell.getCircuitNode());
|
||||
sendRelayCellTo(sendme, sendme.getCircuitNode());
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(streamMap) {
|
||||
final StreamImpl stream = streamMap.get(cell.getStreamId());
|
||||
// It's not unusual for the stream to not be found. For example, if a RELAY_CONNECTED arrives after
|
||||
// the client has stopped waiting for it, the stream will never be tracked and eventually the edge node
|
||||
// will send a RELAY_END for this stream.
|
||||
if(stream != null) {
|
||||
stream.addInputCell(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode) {
|
||||
return new RelayCellImpl(targetNode, circuitId, streamId, relayCommand);
|
||||
}
|
||||
|
||||
void sendRelayCellTo(RelayCell cell, CircuitNode targetNode) {
|
||||
synchronized(relaySendLock) {
|
||||
logRelayCell("Sending: ", cell);
|
||||
cell.setLength();
|
||||
targetNode.updateForwardDigest(cell);
|
||||
cell.setDigest(targetNode.getForwardDigestBytes());
|
||||
|
||||
for(CircuitNode node = targetNode; node != null; node = node.getPreviousNode())
|
||||
node.encryptForwardCell(cell);
|
||||
|
||||
if(cell.getRelayCommand() == RelayCell.RELAY_DATA)
|
||||
targetNode.waitForSendWindowAndDecrement();
|
||||
|
||||
sendCell(cell);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void logRelayCell(String message, RelayCell cell) {
|
||||
final Level level = getLogLevelForCell(cell);
|
||||
if(!logger.isLoggable(level)) {
|
||||
return;
|
||||
}
|
||||
logger.log(level, message + cell);
|
||||
}
|
||||
|
||||
private Level getLogLevelForCell(RelayCell cell) {
|
||||
switch(cell.getRelayCommand()) {
|
||||
case RelayCell.RELAY_DATA:
|
||||
case RelayCell.RELAY_SENDME:
|
||||
return Level.FINEST;
|
||||
default:
|
||||
return Level.FINER;
|
||||
}
|
||||
}
|
||||
|
||||
void sendCell(Cell cell) {
|
||||
final CircuitStatus status = circuit.getStatus();
|
||||
if(!(status.isConnected() || status.isBuilding()))
|
||||
return;
|
||||
try {
|
||||
status.updateDirtyTimestamp();
|
||||
connection.sendCell(cell);
|
||||
} catch (ConnectionIOException e) {
|
||||
destroyCircuit();
|
||||
}
|
||||
}
|
||||
|
||||
void markForClose() {
|
||||
synchronized (streamMap) {
|
||||
if(isMarkedForClose) {
|
||||
return;
|
||||
}
|
||||
isMarkedForClose = true;
|
||||
if(streamMap.isEmpty()) {
|
||||
closeCircuit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isMarkedForClose() {
|
||||
return isMarkedForClose;
|
||||
}
|
||||
|
||||
private void closeCircuit() {
|
||||
logger.fine("Closing circuit "+ circuit);
|
||||
sendDestroyCell(Cell.ERROR_NONE);
|
||||
connection.removeCircuit(circuit);
|
||||
circuit.setStateDestroyed();
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
void sendDestroyCell(int reason) {
|
||||
Cell destroy = CellImpl.createCell(circuitId, Cell.DESTROY);
|
||||
destroy.putByte(reason);
|
||||
try {
|
||||
connection.sendCell(destroy);
|
||||
} catch (ConnectionIOException e) {
|
||||
logger.warning("Connection IO error sending DESTROY cell: "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void processCircuitSendme(RelayCell cell) {
|
||||
cell.getCircuitNode().incrementSendWindow();
|
||||
}
|
||||
|
||||
void destroyCircuit() {
|
||||
synchronized(streamMap) {
|
||||
if(isClosed) {
|
||||
return;
|
||||
}
|
||||
circuit.setStateDestroyed();
|
||||
connection.removeCircuit(circuit);
|
||||
final List<StreamImpl> tmpList = new ArrayList<StreamImpl>(streamMap.values());
|
||||
for(StreamImpl s: tmpList) {
|
||||
s.close();
|
||||
}
|
||||
isClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
StreamImpl createNewStream(boolean autoclose) {
|
||||
synchronized(streamMap) {
|
||||
final int streamId = circuit.getStatus().nextStreamId();
|
||||
final StreamImpl stream = new StreamImpl(circuit, circuit.getFinalCircuitNode(), streamId, autoclose);
|
||||
streamMap.put(streamId, stream);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
void removeStream(StreamImpl stream) {
|
||||
synchronized(streamMap) {
|
||||
streamMap.remove(stream.getStreamId());
|
||||
if(streamMap.isEmpty() && isMarkedForClose) {
|
||||
closeCircuit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Stream> getActiveStreams() {
|
||||
synchronized (streamMap) {
|
||||
return new ArrayList<Stream>(streamMap.values());
|
||||
}
|
||||
}
|
||||
|
||||
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
|
||||
if((flags & DASHBOARD_STREAMS) == 0) {
|
||||
return;
|
||||
}
|
||||
for(Stream s: getActiveStreams()) {
|
||||
renderer.renderComponent(writer, flags, s);
|
||||
}
|
||||
}
|
||||
}
|
286
orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java
Normal file
286
orchid/src/com/subgraph/orchid/circuits/CircuitImpl.java
Normal file
@ -0,0 +1,286 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.DirectoryCircuit;
|
||||
import com.subgraph.orchid.ExitCircuit;
|
||||
import com.subgraph.orchid.InternalCircuit;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderable;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderer;
|
||||
|
||||
/**
|
||||
* This class represents an established circuit through the Tor network.
|
||||
*
|
||||
*/
|
||||
public abstract class CircuitImpl implements Circuit, DashboardRenderable {
|
||||
protected final static Logger logger = Logger.getLogger(CircuitImpl.class.getName());
|
||||
|
||||
static ExitCircuit createExitCircuit(CircuitManagerImpl circuitManager, Router exitRouter) {
|
||||
return new ExitCircuitImpl(circuitManager, exitRouter);
|
||||
}
|
||||
|
||||
static ExitCircuit createExitCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
return new ExitCircuitImpl(circuitManager, prechosenPath);
|
||||
}
|
||||
|
||||
static DirectoryCircuit createDirectoryCircuit(CircuitManagerImpl circuitManager) {
|
||||
return new DirectoryCircuitImpl(circuitManager, null);
|
||||
}
|
||||
|
||||
static DirectoryCircuit createDirectoryCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
return new DirectoryCircuitImpl(circuitManager, prechosenPath);
|
||||
}
|
||||
|
||||
static InternalCircuit createInternalCircuitTo(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
return new InternalCircuitImpl(circuitManager, prechosenPath);
|
||||
}
|
||||
|
||||
private final CircuitManagerImpl circuitManager;
|
||||
protected final List<Router> prechosenPath;
|
||||
|
||||
private final List<CircuitNode> nodeList;
|
||||
private final CircuitStatus status;
|
||||
|
||||
private CircuitIO io;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
protected CircuitImpl(CircuitManagerImpl circuitManager) {
|
||||
this(circuitManager, null);
|
||||
}
|
||||
|
||||
protected CircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
nodeList = new ArrayList<CircuitNode>();
|
||||
this.circuitManager = circuitManager;
|
||||
this.prechosenPath = prechosenPath;
|
||||
status = new CircuitStatus();
|
||||
}
|
||||
|
||||
List<Router> choosePath(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
|
||||
if(prechosenPath != null) {
|
||||
return new ArrayList<Router>(prechosenPath);
|
||||
} else {
|
||||
return choosePathForCircuit(pathChooser);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException;
|
||||
|
||||
void bindToConnection(Connection connection) {
|
||||
if(io != null) {
|
||||
throw new IllegalStateException("Circuit already bound to a connection");
|
||||
}
|
||||
final int id = connection.bindCircuit(this);
|
||||
io = new CircuitIO(this, connection, id);
|
||||
}
|
||||
|
||||
public void markForClose() {
|
||||
if(io != null) {
|
||||
io.markForClose();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMarkedForClose() {
|
||||
if(io == null) {
|
||||
return false;
|
||||
} else {
|
||||
return io.isMarkedForClose();
|
||||
}
|
||||
}
|
||||
|
||||
CircuitStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return status.isConnected();
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return status.isBuilding();
|
||||
}
|
||||
|
||||
public boolean isClean() {
|
||||
return !status.isDirty();
|
||||
}
|
||||
|
||||
public int getSecondsDirty() {
|
||||
return (int) (status.getMillisecondsDirty() / 1000);
|
||||
}
|
||||
|
||||
void notifyCircuitBuildStart() {
|
||||
if(!status.isUnconnected()) {
|
||||
throw new IllegalStateException("Can only connect UNCONNECTED circuits");
|
||||
}
|
||||
status.updateCreatedTimestamp();
|
||||
status.setStateBuilding();
|
||||
circuitManager.addActiveCircuit(this);
|
||||
}
|
||||
|
||||
void notifyCircuitBuildFailed() {
|
||||
status.setStateFailed();
|
||||
circuitManager.removeActiveCircuit(this);
|
||||
}
|
||||
|
||||
void notifyCircuitBuildCompleted() {
|
||||
status.setStateOpen();
|
||||
status.updateCreatedTimestamp();
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
if(!isConnected())
|
||||
throw new TorException("Circuit is not connected.");
|
||||
return io.getConnection();
|
||||
}
|
||||
|
||||
public int getCircuitId() {
|
||||
if(io == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return io.getCircuitId();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendRelayCell(RelayCell cell) {
|
||||
io.sendRelayCellTo(cell, cell.getCircuitNode());
|
||||
}
|
||||
|
||||
public void sendRelayCellToFinalNode(RelayCell cell) {
|
||||
io.sendRelayCellTo(cell, getFinalCircuitNode());
|
||||
}
|
||||
|
||||
public void appendNode(CircuitNode node) {
|
||||
nodeList.add(node);
|
||||
}
|
||||
|
||||
List<CircuitNode> getNodeList() {
|
||||
return nodeList;
|
||||
}
|
||||
|
||||
int getCircuitLength() {
|
||||
return nodeList.size();
|
||||
}
|
||||
|
||||
public CircuitNode getFinalCircuitNode() {
|
||||
if(nodeList.isEmpty())
|
||||
throw new TorException("getFinalCircuitNode() called on empty circuit");
|
||||
return nodeList.get( getCircuitLength() - 1);
|
||||
}
|
||||
|
||||
public RelayCell createRelayCell(int relayCommand, int streamId, CircuitNode targetNode) {
|
||||
return io.createRelayCell(relayCommand, streamId, targetNode);
|
||||
}
|
||||
|
||||
public RelayCell receiveRelayCell() {
|
||||
return io.dequeueRelayResponseCell();
|
||||
}
|
||||
|
||||
void sendCell(Cell cell) {
|
||||
io.sendCell(cell);
|
||||
}
|
||||
|
||||
Cell receiveControlCellResponse() {
|
||||
return io.receiveControlCellResponse();
|
||||
}
|
||||
|
||||
/*
|
||||
* This is called by the cell reading thread in ConnectionImpl to deliver control cells
|
||||
* associated with this circuit (CREATED or CREATED_FAST).
|
||||
*/
|
||||
public void deliverControlCell(Cell cell) {
|
||||
io.deliverControlCell(cell);
|
||||
}
|
||||
|
||||
/* This is called by the cell reading thread in ConnectionImpl to deliver RELAY cells. */
|
||||
public void deliverRelayCell(Cell cell) {
|
||||
io.deliverRelayCell(cell);
|
||||
}
|
||||
|
||||
protected StreamImpl createNewStream(boolean autoclose) {
|
||||
return io.createNewStream(autoclose);
|
||||
}
|
||||
protected StreamImpl createNewStream() {
|
||||
return createNewStream(false);
|
||||
}
|
||||
|
||||
void setStateDestroyed() {
|
||||
status.setStateDestroyed();
|
||||
circuitManager.removeActiveCircuit(this);
|
||||
}
|
||||
|
||||
public void destroyCircuit() {
|
||||
io.destroyCircuit();
|
||||
circuitManager.removeActiveCircuit(this);
|
||||
}
|
||||
|
||||
|
||||
public void removeStream(StreamImpl stream) {
|
||||
io.removeStream(stream);
|
||||
}
|
||||
|
||||
protected Stream processStreamOpenException(Exception e) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
if(e instanceof InterruptedException) {
|
||||
throw (InterruptedException) e;
|
||||
} else if(e instanceof TimeoutException) {
|
||||
throw(TimeoutException) e;
|
||||
} else if(e instanceof StreamConnectFailedException) {
|
||||
throw(StreamConnectFailedException) e;
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String getCircuitTypeLabel();
|
||||
|
||||
public String toString() {
|
||||
return " Circuit ("+ getCircuitTypeLabel() + ") id="+ getCircuitId() +" state=" + status.getStateAsString() +" "+ pathToString();
|
||||
}
|
||||
|
||||
|
||||
protected String pathToString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
for(CircuitNode node: nodeList) {
|
||||
if(sb.length() > 1)
|
||||
sb.append(",");
|
||||
sb.append(node.toString());
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public List<Stream> getActiveStreams() {
|
||||
if(io == null) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
return io.getActiveStreams();
|
||||
}
|
||||
}
|
||||
|
||||
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
|
||||
if(io != null) {
|
||||
writer.println(toString());
|
||||
renderer.renderComponent(writer, flags, io);
|
||||
}
|
||||
}
|
||||
}
|
395
orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java
Normal file
395
orchid/src/com/subgraph/orchid/circuits/CircuitManagerImpl.java
Normal file
@ -0,0 +1,395 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitBuildHandler;
|
||||
import com.subgraph.orchid.CircuitManager;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.Connection;
|
||||
import com.subgraph.orchid.ConnectionCache;
|
||||
import com.subgraph.orchid.ConsensusDocument;
|
||||
import com.subgraph.orchid.Directory;
|
||||
import com.subgraph.orchid.DirectoryCircuit;
|
||||
import com.subgraph.orchid.ExitCircuit;
|
||||
import com.subgraph.orchid.InternalCircuit;
|
||||
import com.subgraph.orchid.OpenFailedException;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.Tor;
|
||||
import com.subgraph.orchid.TorConfig;
|
||||
import com.subgraph.orchid.circuits.guards.EntryGuards;
|
||||
import com.subgraph.orchid.circuits.hs.HiddenServiceManager;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.crypto.TorRandom;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderable;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderer;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
|
||||
|
||||
public class CircuitManagerImpl implements CircuitManager, DashboardRenderable {
|
||||
private final static int OPEN_DIRECTORY_STREAM_RETRY_COUNT = 5;
|
||||
private final static int OPEN_DIRECTORY_STREAM_TIMEOUT = 10 * 1000;
|
||||
|
||||
interface CircuitFilter {
|
||||
boolean filter(Circuit circuit);
|
||||
}
|
||||
|
||||
private final TorConfig config;
|
||||
private final Directory directory;
|
||||
private final ConnectionCache connectionCache;
|
||||
private final Set<CircuitImpl> activeCircuits;
|
||||
private final Queue<InternalCircuit> cleanInternalCircuits;
|
||||
private int requestedInternalCircuitCount = 0;
|
||||
private int pendingInternalCircuitCount = 0;
|
||||
private final TorRandom random;
|
||||
private final PendingExitStreams pendingExitStreams;
|
||||
private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
private final CircuitCreationTask circuitCreationTask;
|
||||
private final TorInitializationTracker initializationTracker;
|
||||
private final CircuitPathChooser pathChooser;
|
||||
private final HiddenServiceManager hiddenServiceManager;
|
||||
|
||||
public CircuitManagerImpl(TorConfig config, DirectoryDownloaderImpl directoryDownloader, Directory directory, ConnectionCache connectionCache, TorInitializationTracker initializationTracker) {
|
||||
this.config = config;
|
||||
this.directory = directory;
|
||||
this.connectionCache = connectionCache;
|
||||
this.pathChooser = CircuitPathChooser.create(config, directory);
|
||||
if(config.getUseEntryGuards() || config.getUseBridges()) {
|
||||
this.pathChooser.enableEntryGuards(new EntryGuards(config, connectionCache, directoryDownloader, directory));
|
||||
}
|
||||
this.pendingExitStreams = new PendingExitStreams(config);
|
||||
this.circuitCreationTask = new CircuitCreationTask(config, directory, connectionCache, pathChooser, this, initializationTracker);
|
||||
this.activeCircuits = new HashSet<CircuitImpl>();
|
||||
this.cleanInternalCircuits = new LinkedList<InternalCircuit>();
|
||||
this.random = new TorRandom();
|
||||
|
||||
this.initializationTracker = initializationTracker;
|
||||
this.hiddenServiceManager = new HiddenServiceManager(config, directory, this);
|
||||
|
||||
directoryDownloader.setCircuitManager(this);
|
||||
}
|
||||
|
||||
public void startBuildingCircuits() {
|
||||
scheduledExecutor.scheduleAtFixedRate(circuitCreationTask, 0, 1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public synchronized void stopBuildingCircuits(boolean killCircuits) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
if(killCircuits) {
|
||||
List<CircuitImpl> circuits = new ArrayList<CircuitImpl>(activeCircuits);
|
||||
for(CircuitImpl c: circuits) {
|
||||
c.destroyCircuit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ExitCircuit createNewExitCircuit(Router exitRouter) {
|
||||
return CircuitImpl.createExitCircuit(this, exitRouter);
|
||||
}
|
||||
|
||||
void addActiveCircuit(CircuitImpl circuit) {
|
||||
synchronized (activeCircuits) {
|
||||
activeCircuits.add(circuit);
|
||||
activeCircuits.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
void removeActiveCircuit(CircuitImpl circuit) {
|
||||
synchronized (activeCircuits) {
|
||||
activeCircuits.remove(circuit);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized int getActiveCircuitCount() {
|
||||
return activeCircuits.size();
|
||||
}
|
||||
|
||||
Set<Circuit> getPendingCircuits() {
|
||||
return getCircuitsByFilter(new CircuitFilter() {
|
||||
public boolean filter(Circuit circuit) {
|
||||
return circuit.isPending();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
synchronized int getPendingCircuitCount() {
|
||||
return getPendingCircuits().size();
|
||||
}
|
||||
|
||||
Set<Circuit> getCircuitsByFilter(CircuitFilter filter) {
|
||||
final Set<Circuit> result = new HashSet<Circuit>();
|
||||
synchronized (activeCircuits) {
|
||||
for(CircuitImpl c: activeCircuits) {
|
||||
if(filter == null || filter.filter(c)) {
|
||||
result.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<ExitCircuit> getRandomlyOrderedListOfExitCircuits() {
|
||||
final Set<Circuit> notDirectory = getCircuitsByFilter(new CircuitFilter() {
|
||||
|
||||
public boolean filter(Circuit circuit) {
|
||||
final boolean exitType = circuit instanceof ExitCircuit;
|
||||
return exitType && !circuit.isMarkedForClose() && circuit.isConnected();
|
||||
}
|
||||
});
|
||||
final ArrayList<ExitCircuit> ac = new ArrayList<ExitCircuit>();
|
||||
for(Circuit c: notDirectory) {
|
||||
if(c instanceof ExitCircuit) {
|
||||
ac.add((ExitCircuit) c);
|
||||
}
|
||||
}
|
||||
final int sz = ac.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
final ExitCircuit tmp = ac.get(i);
|
||||
final int swapIdx = random.nextInt(sz);
|
||||
ac.set(i, ac.get(swapIdx));
|
||||
ac.set(swapIdx, tmp);
|
||||
}
|
||||
return ac;
|
||||
}
|
||||
|
||||
public Stream openExitStreamTo(String hostname, int port)
|
||||
throws InterruptedException, TimeoutException, OpenFailedException {
|
||||
if(hostname.endsWith(".onion")) {
|
||||
return hiddenServiceManager.getStreamTo(hostname, port);
|
||||
}
|
||||
validateHostname(hostname);
|
||||
circuitCreationTask.predictPort(port);
|
||||
return pendingExitStreams.openExitStream(hostname, port);
|
||||
}
|
||||
|
||||
private void validateHostname(String hostname) throws OpenFailedException {
|
||||
maybeRejectInternalAddress(hostname);
|
||||
if(hostname.toLowerCase().endsWith(".onion")) {
|
||||
throw new OpenFailedException("Hidden services not supported");
|
||||
} else if(hostname.toLowerCase().endsWith(".exit")) {
|
||||
throw new OpenFailedException(".exit addresses are not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeRejectInternalAddress(String hostname) throws OpenFailedException {
|
||||
if(IPv4Address.isValidIPv4AddressString(hostname)) {
|
||||
maybeRejectInternalAddress(IPv4Address.createFromString(hostname));
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeRejectInternalAddress(IPv4Address address) throws OpenFailedException {
|
||||
final InetAddress inetAddress = address.toInetAddress();
|
||||
if(inetAddress.isSiteLocalAddress() && config.getClientRejectInternalAddress()) {
|
||||
throw new OpenFailedException("Rejecting stream target with internal address: "+ address);
|
||||
}
|
||||
}
|
||||
public Stream openExitStreamTo(IPv4Address address, int port)
|
||||
throws InterruptedException, TimeoutException, OpenFailedException {
|
||||
maybeRejectInternalAddress(address);
|
||||
circuitCreationTask.predictPort(port);
|
||||
return pendingExitStreams.openExitStream(address, port);
|
||||
}
|
||||
|
||||
public List<StreamExitRequest> getPendingExitStreams() {
|
||||
return pendingExitStreams.getUnreservedPendingRequests();
|
||||
}
|
||||
|
||||
public Stream openDirectoryStream() throws OpenFailedException, InterruptedException, TimeoutException {
|
||||
return openDirectoryStream(0);
|
||||
}
|
||||
|
||||
public Stream openDirectoryStream(int purpose) throws OpenFailedException, InterruptedException {
|
||||
final int requestEventCode = purposeToEventCode(purpose, false);
|
||||
final int loadingEventCode = purposeToEventCode(purpose, true);
|
||||
|
||||
int failCount = 0;
|
||||
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
|
||||
final DirectoryCircuit circuit = openDirectoryCircuit();
|
||||
if(requestEventCode > 0) {
|
||||
initializationTracker.notifyEvent(requestEventCode);
|
||||
}
|
||||
try {
|
||||
final Stream stream = circuit.openDirectoryStream(OPEN_DIRECTORY_STREAM_TIMEOUT, true);
|
||||
if(loadingEventCode > 0) {
|
||||
initializationTracker.notifyEvent(loadingEventCode);
|
||||
}
|
||||
return stream;
|
||||
} catch (StreamConnectFailedException e) {
|
||||
circuit.markForClose();
|
||||
failCount += 1;
|
||||
} catch (TimeoutException e) {
|
||||
circuit.markForClose();
|
||||
}
|
||||
}
|
||||
throw new OpenFailedException("Retry count exceeded opening directory stream");
|
||||
}
|
||||
|
||||
public DirectoryCircuit openDirectoryCircuit() throws OpenFailedException {
|
||||
int failCount = 0;
|
||||
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
|
||||
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuit(this);
|
||||
if(tryOpenCircuit(circuit, true, true)) {
|
||||
return circuit;
|
||||
}
|
||||
failCount += 1;
|
||||
}
|
||||
throw new OpenFailedException("Could not create circuit for directory stream");
|
||||
}
|
||||
|
||||
private int purposeToEventCode(int purpose, boolean getLoadingEvent) {
|
||||
switch(purpose) {
|
||||
case DIRECTORY_PURPOSE_CONSENSUS:
|
||||
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_STATUS : Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS;
|
||||
case DIRECTORY_PURPOSE_CERTIFICATES:
|
||||
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_KEYS : Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS;
|
||||
case DIRECTORY_PURPOSE_DESCRIPTORS:
|
||||
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS : Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirectoryCircuitResult implements CircuitBuildHandler {
|
||||
|
||||
private boolean isFailed;
|
||||
|
||||
public void connectionCompleted(Connection connection) {}
|
||||
public void nodeAdded(CircuitNode node) {}
|
||||
public void circuitBuildCompleted(Circuit circuit) {}
|
||||
|
||||
public void connectionFailed(String reason) {
|
||||
isFailed = true;
|
||||
}
|
||||
|
||||
public void circuitBuildFailed(String reason) {
|
||||
isFailed = true;
|
||||
}
|
||||
|
||||
boolean isSuccessful() {
|
||||
return !isFailed;
|
||||
}
|
||||
}
|
||||
|
||||
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
|
||||
if((flags & DASHBOARD_CIRCUITS) == 0) {
|
||||
return;
|
||||
}
|
||||
renderer.renderComponent(writer, flags, connectionCache);
|
||||
renderer.renderComponent(writer, flags, circuitCreationTask.getCircuitPredictor());
|
||||
writer.println("[Circuit Manager]");
|
||||
writer.println();
|
||||
for(Circuit c: getCircuitsByFilter(null)) {
|
||||
renderer.renderComponent(writer, flags, c);
|
||||
}
|
||||
}
|
||||
|
||||
public InternalCircuit getCleanInternalCircuit() throws InterruptedException {
|
||||
synchronized(cleanInternalCircuits) {
|
||||
try {
|
||||
requestedInternalCircuitCount += 1;
|
||||
while(cleanInternalCircuits.isEmpty()) {
|
||||
cleanInternalCircuits.wait();
|
||||
}
|
||||
return cleanInternalCircuits.remove();
|
||||
} finally {
|
||||
requestedInternalCircuitCount -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getNeededCleanCircuitCount(boolean isPredicted) {
|
||||
synchronized (cleanInternalCircuits) {
|
||||
final int predictedCount = (isPredicted) ? 2 : 0;
|
||||
final int needed = Math.max(requestedInternalCircuitCount, predictedCount) - (pendingInternalCircuitCount + cleanInternalCircuits.size());
|
||||
if(needed < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return needed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void incrementPendingInternalCircuitCount() {
|
||||
synchronized (cleanInternalCircuits) {
|
||||
pendingInternalCircuitCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
void decrementPendingInternalCircuitCount() {
|
||||
synchronized (cleanInternalCircuits) {
|
||||
pendingInternalCircuitCount -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
void addCleanInternalCircuit(InternalCircuit circuit) {
|
||||
synchronized(cleanInternalCircuits) {
|
||||
pendingInternalCircuitCount -= 1;
|
||||
cleanInternalCircuits.add(circuit);
|
||||
cleanInternalCircuits.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNtorEnabled() {
|
||||
switch(config.getUseNTorHandshake()) {
|
||||
case AUTO:
|
||||
return isNtorEnabledInConsensus();
|
||||
case FALSE:
|
||||
return false;
|
||||
case TRUE:
|
||||
return true;
|
||||
default:
|
||||
throw new IllegalArgumentException("getUseNTorHandshake() returned "+ config.getUseNTorHandshake());
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNtorEnabledInConsensus() {
|
||||
ConsensusDocument consensus = directory.getCurrentConsensusDocument();
|
||||
return (consensus != null) && (consensus.getUseNTorHandshake());
|
||||
}
|
||||
|
||||
public DirectoryCircuit openDirectoryCircuitTo(List<Router> path) throws OpenFailedException {
|
||||
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuitTo(this, path);
|
||||
if(!tryOpenCircuit(circuit, true, false)) {
|
||||
throw new OpenFailedException("Could not create directory circuit for path");
|
||||
}
|
||||
return circuit;
|
||||
}
|
||||
|
||||
public ExitCircuit openExitCircuitTo(List<Router> path) throws OpenFailedException {
|
||||
final ExitCircuit circuit = CircuitImpl.createExitCircuitTo(this, path);
|
||||
if(!tryOpenCircuit(circuit, false, false)) {
|
||||
throw new OpenFailedException("Could not create exit circuit for path");
|
||||
}
|
||||
return circuit;
|
||||
}
|
||||
|
||||
public InternalCircuit openInternalCircuitTo(List<Router> path) throws OpenFailedException {
|
||||
final InternalCircuit circuit = CircuitImpl.createInternalCircuitTo(this, path);
|
||||
if(!tryOpenCircuit(circuit, false, false)) {
|
||||
throw new OpenFailedException("Could not create internal circuit for path");
|
||||
}
|
||||
return circuit;
|
||||
}
|
||||
|
||||
private boolean tryOpenCircuit(Circuit circuit, boolean isDirectory, boolean trackInitialization) {
|
||||
final DirectoryCircuitResult result = new DirectoryCircuitResult();
|
||||
final CircuitCreationRequest req = new CircuitCreationRequest(pathChooser, circuit, result, isDirectory);
|
||||
final CircuitBuildTask task = new CircuitBuildTask(req, connectionCache, isNtorEnabled(), (trackInitialization) ? (initializationTracker) : (null));
|
||||
task.run();
|
||||
return result.isSuccessful();
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.crypto.TorMessageDigest;
|
||||
import com.subgraph.orchid.crypto.TorStreamCipher;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
|
||||
public class CircuitNodeCryptoState {
|
||||
public final static int KEY_MATERIAL_SIZE = TorMessageDigest.TOR_DIGEST_SIZE * 2 + TorStreamCipher.KEY_LEN * 2;
|
||||
|
||||
public static CircuitNodeCryptoState createFromKeyMaterial(byte[] keyMaterial, byte[] verifyDigest) {
|
||||
return new CircuitNodeCryptoState(keyMaterial, verifyDigest);
|
||||
}
|
||||
|
||||
private final HexDigest checksumDigest;
|
||||
private final TorMessageDigest forwardDigest;
|
||||
private final TorMessageDigest backwardDigest;
|
||||
private final TorStreamCipher forwardCipher;
|
||||
private final TorStreamCipher backwardCipher;
|
||||
|
||||
static private byte[] extractDigestBytes(byte[] keyMaterial, int offset) {
|
||||
final byte[] digestBytes = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
|
||||
System.arraycopy(keyMaterial, offset, digestBytes, 0, TorMessageDigest.TOR_DIGEST_SIZE);
|
||||
return digestBytes;
|
||||
}
|
||||
|
||||
static private byte[] extractCipherKey(byte[] keyMaterial, int offset) {
|
||||
final byte[] keyBytes = new byte[TorStreamCipher.KEY_LEN];
|
||||
System.arraycopy(keyMaterial, offset, keyBytes, 0, TorStreamCipher.KEY_LEN);
|
||||
return keyBytes;
|
||||
}
|
||||
|
||||
private CircuitNodeCryptoState(byte[] keyMaterial, byte[] verifyDigest) {
|
||||
checksumDigest = HexDigest.createFromDigestBytes(verifyDigest);
|
||||
int offset = 0;
|
||||
|
||||
forwardDigest = new TorMessageDigest();
|
||||
forwardDigest.update(extractDigestBytes(keyMaterial, offset));
|
||||
offset += TorMessageDigest.TOR_DIGEST_SIZE;
|
||||
|
||||
backwardDigest = new TorMessageDigest();
|
||||
backwardDigest.update(extractDigestBytes(keyMaterial, offset));
|
||||
offset += TorMessageDigest.TOR_DIGEST_SIZE;
|
||||
|
||||
forwardCipher = TorStreamCipher.createFromKeyBytes(extractCipherKey(keyMaterial, offset));
|
||||
offset += TorStreamCipher.KEY_LEN;
|
||||
|
||||
backwardCipher = TorStreamCipher.createFromKeyBytes(extractCipherKey(keyMaterial, offset));
|
||||
}
|
||||
|
||||
boolean verifyPacketDigest(HexDigest packetDigest) {
|
||||
return checksumDigest.equals(packetDigest);
|
||||
}
|
||||
|
||||
void encryptForwardCell(Cell cell) {
|
||||
forwardCipher.encrypt(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
|
||||
}
|
||||
|
||||
boolean decryptBackwardCell(Cell cell) {
|
||||
backwardCipher.encrypt(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
|
||||
return isRecognizedCell(cell);
|
||||
}
|
||||
|
||||
void updateForwardDigest(Cell cell) {
|
||||
forwardDigest.update(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
|
||||
}
|
||||
|
||||
byte[] getForwardDigestBytes() {
|
||||
return forwardDigest.getDigestBytes();
|
||||
}
|
||||
|
||||
private boolean isRecognizedCell(Cell cell) {
|
||||
if(cell.getShortAt(RelayCell.RECOGNIZED_OFFSET) != 0)
|
||||
return false;
|
||||
|
||||
final byte[] digest = extractRelayDigest(cell);
|
||||
final byte[] peek = backwardDigest.peekDigest(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
|
||||
for(int i = 0; i < 4; i++)
|
||||
if(digest[i] != peek[i]) {
|
||||
replaceRelayDigest(cell, digest);
|
||||
return false;
|
||||
}
|
||||
backwardDigest.update(cell.getCellBytes(), Cell.CELL_HEADER_LEN, Cell.CELL_PAYLOAD_LEN);
|
||||
replaceRelayDigest(cell, digest);
|
||||
return true;
|
||||
}
|
||||
|
||||
private byte[] extractRelayDigest(Cell cell) {
|
||||
final byte[] digest = new byte[4];
|
||||
for(int i = 0; i < 4; i++) {
|
||||
digest[i] = (byte) cell.getByteAt(i + RelayCell.DIGEST_OFFSET);
|
||||
cell.putByteAt(i + RelayCell.DIGEST_OFFSET, 0);
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
private void replaceRelayDigest(Cell cell, byte[] digest) {
|
||||
for(int i = 0; i < 4; i++)
|
||||
cell.putByteAt(i + RelayCell.DIGEST_OFFSET, digest[i] & 0xFF);
|
||||
}
|
||||
}
|
121
orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java
Normal file
121
orchid/src/com/subgraph/orchid/circuits/CircuitNodeImpl.java
Normal file
@ -0,0 +1,121 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorException;
|
||||
|
||||
public class CircuitNodeImpl implements CircuitNode {
|
||||
|
||||
public static CircuitNode createAnonymous(CircuitNode previous, byte[] keyMaterial, byte[] verifyDigest) {
|
||||
return createNode(null, previous, keyMaterial, verifyDigest);
|
||||
}
|
||||
|
||||
public static CircuitNode createFirstHop(Router r, byte[] keyMaterial, byte[] verifyDigest) {
|
||||
return createNode(r, null, keyMaterial, verifyDigest);
|
||||
}
|
||||
|
||||
public static CircuitNode createNode(Router r, CircuitNode previous, byte[] keyMaterial, byte[] verifyDigest) {
|
||||
final CircuitNodeCryptoState cs = CircuitNodeCryptoState.createFromKeyMaterial(keyMaterial, verifyDigest);
|
||||
return new CircuitNodeImpl(r, previous, cs);
|
||||
}
|
||||
|
||||
private final static int CIRCWINDOW_START = 1000;
|
||||
private final static int CIRCWINDOW_INCREMENT = 100;
|
||||
|
||||
private final Router router;
|
||||
private final CircuitNodeCryptoState cryptoState;
|
||||
private final CircuitNode previousNode;
|
||||
|
||||
private final Object windowLock;
|
||||
private int packageWindow;
|
||||
private int deliverWindow;
|
||||
|
||||
private CircuitNodeImpl(Router router, CircuitNode previous, CircuitNodeCryptoState cryptoState) {
|
||||
previousNode = previous;
|
||||
this.router = router;
|
||||
this.cryptoState = cryptoState;
|
||||
windowLock = new Object();
|
||||
packageWindow = CIRCWINDOW_START;
|
||||
deliverWindow = CIRCWINDOW_START;
|
||||
}
|
||||
|
||||
public Router getRouter() {
|
||||
return router;
|
||||
}
|
||||
|
||||
public CircuitNode getPreviousNode() {
|
||||
return previousNode;
|
||||
}
|
||||
|
||||
public void encryptForwardCell(RelayCell cell) {
|
||||
cryptoState.encryptForwardCell(cell);
|
||||
}
|
||||
|
||||
public boolean decryptBackwardCell(Cell cell) {
|
||||
return cryptoState.decryptBackwardCell(cell);
|
||||
}
|
||||
|
||||
public void updateForwardDigest(RelayCell cell) {
|
||||
cryptoState.updateForwardDigest(cell);
|
||||
}
|
||||
|
||||
public byte[] getForwardDigestBytes() {
|
||||
return cryptoState.getForwardDigestBytes();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if(router != null) {
|
||||
return "|"+ router.getNickname() + "|";
|
||||
} else {
|
||||
return "|()|";
|
||||
}
|
||||
}
|
||||
|
||||
public void decrementDeliverWindow() {
|
||||
synchronized(windowLock) {
|
||||
deliverWindow--;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean considerSendingSendme() {
|
||||
synchronized(windowLock) {
|
||||
if(deliverWindow <= (CIRCWINDOW_START - CIRCWINDOW_INCREMENT)) {
|
||||
deliverWindow += CIRCWINDOW_INCREMENT;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForSendWindow() {
|
||||
waitForSendWindow(false);
|
||||
}
|
||||
|
||||
public void waitForSendWindowAndDecrement() {
|
||||
waitForSendWindow(true);
|
||||
}
|
||||
|
||||
private void waitForSendWindow(boolean decrement) {
|
||||
synchronized(windowLock) {
|
||||
while(packageWindow == 0) {
|
||||
try {
|
||||
windowLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new TorException("Thread interrupted while waiting for circuit send window");
|
||||
}
|
||||
}
|
||||
if(decrement)
|
||||
packageWindow--;
|
||||
}
|
||||
}
|
||||
|
||||
public void incrementSendWindow() {
|
||||
synchronized(windowLock) {
|
||||
packageWindow += CIRCWINDOW_INCREMENT;
|
||||
windowLock.notifyAll();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderable;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderer;
|
||||
|
||||
public class CircuitPredictor implements DashboardRenderable {
|
||||
|
||||
private final static Integer INTERNAL_CIRCUIT_PORT_VALUE = 0;
|
||||
private final static long TIMEOUT_MS = 60 * 60 * 1000; // One hour
|
||||
|
||||
private final Map<Integer, Long> portsSeen;
|
||||
|
||||
public CircuitPredictor() {
|
||||
portsSeen = new HashMap<Integer,Long>();
|
||||
addExitPortRequest(80);
|
||||
addInternalRequest();
|
||||
}
|
||||
|
||||
void addExitPortRequest(int port) {
|
||||
synchronized (portsSeen) {
|
||||
portsSeen.put(port, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
void addInternalRequest() {
|
||||
addExitPortRequest(INTERNAL_CIRCUIT_PORT_VALUE);
|
||||
}
|
||||
|
||||
|
||||
private boolean isEntryExpired(Entry<Integer, Long> e, long now) {
|
||||
return (now - e.getValue()) > TIMEOUT_MS;
|
||||
}
|
||||
|
||||
private void removeExpiredPorts() {
|
||||
final long now = System.currentTimeMillis();
|
||||
final Iterator<Entry<Integer, Long>> it = portsSeen.entrySet().iterator();
|
||||
while(it.hasNext()) {
|
||||
if(isEntryExpired(it.next(), now)) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isInternalPredicted() {
|
||||
synchronized (portsSeen) {
|
||||
removeExpiredPorts();
|
||||
return portsSeen.containsKey(INTERNAL_CIRCUIT_PORT_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> getPredictedPorts() {
|
||||
synchronized (portsSeen) {
|
||||
removeExpiredPorts();
|
||||
final Set<Integer> result = new HashSet<Integer>(portsSeen.keySet());
|
||||
result.remove(INTERNAL_CIRCUIT_PORT_VALUE);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
List<PredictedPortTarget> getPredictedPortTargets() {
|
||||
final List<PredictedPortTarget> targets = new ArrayList<PredictedPortTarget>();
|
||||
for(int p: getPredictedPorts()) {
|
||||
targets.add(new PredictedPortTarget(p));
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags)
|
||||
throws IOException {
|
||||
|
||||
if((flags & DASHBOARD_PREDICTED_PORTS) == 0) {
|
||||
return;
|
||||
}
|
||||
writer.println("[Predicted Ports] ");
|
||||
for(int port : portsSeen.keySet()) {
|
||||
writer.write(" "+ port);
|
||||
Long lastSeen = portsSeen.get(port);
|
||||
if(lastSeen != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
long ms = now - lastSeen;
|
||||
writer.write(" (last seen "+ TimeUnit.MINUTES.convert(ms, TimeUnit.MILLISECONDS) +" minutes ago)");
|
||||
}
|
||||
writer.println();
|
||||
}
|
||||
writer.println();
|
||||
}
|
||||
}
|
115
orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java
Normal file
115
orchid/src/com/subgraph/orchid/circuits/CircuitStatus.java
Normal file
@ -0,0 +1,115 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorRandom;
|
||||
|
||||
public class CircuitStatus {
|
||||
|
||||
enum CircuitState {
|
||||
UNCONNECTED("Unconnected"),
|
||||
BUILDING("Building"),
|
||||
FAILED("Failed"),
|
||||
OPEN("Open"),
|
||||
DESTROYED("Destroyed");
|
||||
String name;
|
||||
CircuitState(String name) { this.name = name; }
|
||||
public String toString() { return name; }
|
||||
}
|
||||
|
||||
private long timestampCreated;
|
||||
private long timestampDirty;
|
||||
private int currentStreamId;
|
||||
private Object streamIdLock = new Object();
|
||||
private volatile CircuitState state = CircuitState.UNCONNECTED;
|
||||
|
||||
CircuitStatus() {
|
||||
initializeCurrentStreamId();
|
||||
}
|
||||
|
||||
private void initializeCurrentStreamId() {
|
||||
final TorRandom random = new TorRandom();
|
||||
currentStreamId = random.nextInt(0xFFFF) + 1;
|
||||
}
|
||||
|
||||
synchronized void updateCreatedTimestamp() {
|
||||
timestampCreated = System.currentTimeMillis();
|
||||
timestampDirty = 0;
|
||||
}
|
||||
|
||||
synchronized void updateDirtyTimestamp() {
|
||||
if(timestampDirty == 0 && state != CircuitState.BUILDING) {
|
||||
timestampDirty = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized long getMillisecondsElapsedSinceCreated() {
|
||||
return millisecondsElapsedSince(timestampCreated);
|
||||
}
|
||||
|
||||
synchronized long getMillisecondsDirty() {
|
||||
return millisecondsElapsedSince(timestampDirty);
|
||||
}
|
||||
|
||||
private static long millisecondsElapsedSince(long then) {
|
||||
if(then == 0) {
|
||||
return 0;
|
||||
}
|
||||
final long now = System.currentTimeMillis();
|
||||
return now - then;
|
||||
}
|
||||
|
||||
synchronized boolean isDirty() {
|
||||
return timestampDirty != 0;
|
||||
}
|
||||
|
||||
void setStateBuilding() {
|
||||
state = CircuitState.BUILDING;
|
||||
}
|
||||
|
||||
void setStateFailed() {
|
||||
state = CircuitState.FAILED;
|
||||
}
|
||||
|
||||
void setStateOpen() {
|
||||
state = CircuitState.OPEN;
|
||||
}
|
||||
|
||||
void setStateDestroyed() {
|
||||
state = CircuitState.DESTROYED;
|
||||
}
|
||||
|
||||
boolean isBuilding() {
|
||||
return state == CircuitState.BUILDING;
|
||||
}
|
||||
|
||||
boolean isConnected() {
|
||||
return state == CircuitState.OPEN;
|
||||
}
|
||||
|
||||
boolean isUnconnected() {
|
||||
return state == CircuitState.UNCONNECTED;
|
||||
}
|
||||
|
||||
String getStateAsString() {
|
||||
if(state == CircuitState.OPEN) {
|
||||
return state.toString() + " ["+ getDirtyString() + "]";
|
||||
}
|
||||
return state.toString();
|
||||
}
|
||||
|
||||
private String getDirtyString() {
|
||||
if(!isDirty()) {
|
||||
return "Clean";
|
||||
} else {
|
||||
return "Dirty "+ (getMillisecondsDirty() / 1000) +"s";
|
||||
}
|
||||
}
|
||||
int nextStreamId() {
|
||||
synchronized(streamIdLock) {
|
||||
currentStreamId++;
|
||||
if(currentStreamId > 0xFFFF)
|
||||
currentStreamId = 1;
|
||||
return currentStreamId;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.DirectoryCircuit;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
|
||||
public class DirectoryCircuitImpl extends CircuitImpl implements DirectoryCircuit {
|
||||
|
||||
protected DirectoryCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
super(circuitManager, prechosenPath);
|
||||
}
|
||||
|
||||
public Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
final StreamImpl stream = createNewStream(autoclose);
|
||||
try {
|
||||
stream.openDirectory(timeout);
|
||||
return stream;
|
||||
} catch (Exception e) {
|
||||
removeStream(stream);
|
||||
return processStreamOpenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
|
||||
if(prechosenPath != null) {
|
||||
return prechosenPath;
|
||||
}
|
||||
return pathChooser.chooseDirectoryPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCircuitTypeLabel() {
|
||||
return "Directory";
|
||||
}
|
||||
}
|
87
orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java
Normal file
87
orchid/src/com/subgraph/orchid/circuits/ExitCircuitImpl.java
Normal file
@ -0,0 +1,87 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.ExitCircuit;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
|
||||
public class ExitCircuitImpl extends CircuitImpl implements ExitCircuit {
|
||||
|
||||
private final Router exitRouter;
|
||||
private final Set<ExitTarget> failedExitRequests;
|
||||
|
||||
ExitCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
super(circuitManager, prechosenPath);
|
||||
this.exitRouter = prechosenPath.get(prechosenPath.size() - 1);
|
||||
this.failedExitRequests = new HashSet<ExitTarget>();
|
||||
}
|
||||
|
||||
ExitCircuitImpl(CircuitManagerImpl circuitManager, Router exitRouter) {
|
||||
super(circuitManager);
|
||||
this.exitRouter = exitRouter;
|
||||
this.failedExitRequests = new HashSet<ExitTarget>();
|
||||
}
|
||||
|
||||
public Stream openExitStream(IPv4Address address, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
return openExitStream(address.toString(), port, timeout);
|
||||
}
|
||||
|
||||
public Stream openExitStream(String target, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
final StreamImpl stream = createNewStream();
|
||||
try {
|
||||
stream.openExit(target, port, timeout);
|
||||
return stream;
|
||||
} catch (Exception e) {
|
||||
removeStream(stream);
|
||||
return processStreamOpenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordFailedExitTarget(ExitTarget target) {
|
||||
synchronized(failedExitRequests) {
|
||||
failedExitRequests.add(target);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canHandleExitTo(ExitTarget target) {
|
||||
synchronized(failedExitRequests) {
|
||||
if(failedExitRequests.contains(target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(isMarkedForClose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(target.isAddressTarget()) {
|
||||
return exitRouter.exitPolicyAccepts(target.getAddress(), target.getPort());
|
||||
} else {
|
||||
return exitRouter.exitPolicyAccepts(target.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canHandleExitToPort(int port) {
|
||||
return exitRouter.exitPolicyAccepts(port);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser) throws InterruptedException, PathSelectionFailedException {
|
||||
return pathChooser.choosePathWithExit(exitRouter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCircuitTypeLabel() {
|
||||
return "Exit";
|
||||
}
|
||||
}
|
118
orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java
Normal file
118
orchid/src/com/subgraph/orchid/circuits/InternalCircuitImpl.java
Normal file
@ -0,0 +1,118 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.DirectoryCircuit;
|
||||
import com.subgraph.orchid.HiddenServiceCircuit;
|
||||
import com.subgraph.orchid.InternalCircuit;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
|
||||
import com.subgraph.orchid.circuits.path.PathSelectionFailedException;
|
||||
|
||||
public class InternalCircuitImpl extends CircuitImpl implements InternalCircuit, DirectoryCircuit, HiddenServiceCircuit {
|
||||
|
||||
private enum InternalType { UNUSED, HS_INTRODUCTION, HS_DIRECTORY, HS_CIRCUIT }
|
||||
|
||||
private InternalType type;
|
||||
private boolean ntorEnabled;
|
||||
|
||||
InternalCircuitImpl(CircuitManagerImpl circuitManager, List<Router> prechosenPath) {
|
||||
super(circuitManager, prechosenPath);
|
||||
this.type = InternalType.UNUSED;
|
||||
this.ntorEnabled = circuitManager.isNtorEnabled();
|
||||
}
|
||||
|
||||
protected InternalCircuitImpl(CircuitManagerImpl circuitManager) {
|
||||
this(circuitManager, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Router> choosePathForCircuit(CircuitPathChooser pathChooser)
|
||||
throws InterruptedException, PathSelectionFailedException {
|
||||
return pathChooser.chooseInternalPath();
|
||||
}
|
||||
|
||||
|
||||
public Circuit cannibalizeToIntroductionPoint(Router target) {
|
||||
cannibalizeTo(target);
|
||||
type = InternalType.HS_INTRODUCTION;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void cannibalizeTo(Router target) {
|
||||
if(type != InternalType.UNUSED) {
|
||||
throw new IllegalStateException("Cannot cannibalize internal circuit with type "+ type);
|
||||
|
||||
}
|
||||
final CircuitExtender extender = new CircuitExtender(this, ntorEnabled);
|
||||
extender.extendTo(target);
|
||||
}
|
||||
|
||||
public Stream openDirectoryStream(long timeout, boolean autoclose) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
if(type != InternalType.HS_DIRECTORY) {
|
||||
throw new IllegalStateException("Cannot open directory stream on internal circuit with type "+ type);
|
||||
}
|
||||
final StreamImpl stream = createNewStream();
|
||||
try {
|
||||
stream.openDirectory(timeout);
|
||||
return stream;
|
||||
} catch (Exception e) {
|
||||
removeStream(stream);
|
||||
return processStreamOpenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public DirectoryCircuit cannibalizeToDirectory(Router target) {
|
||||
cannibalizeTo(target);
|
||||
type = InternalType.HS_DIRECTORY;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public HiddenServiceCircuit connectHiddenService(CircuitNode node) {
|
||||
if(type != InternalType.UNUSED) {
|
||||
throw new IllegalStateException("Cannot connect hidden service from internal circuit type "+ type);
|
||||
}
|
||||
appendNode(node);
|
||||
type = InternalType.HS_CIRCUIT;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Stream openStream(int port, long timeout)
|
||||
throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
if(type != InternalType.HS_CIRCUIT) {
|
||||
throw new IllegalStateException("Cannot open stream to hidden service from internal circuit type "+ type);
|
||||
}
|
||||
final StreamImpl stream = createNewStream();
|
||||
try {
|
||||
stream.openExit("", port, timeout);
|
||||
return stream;
|
||||
} catch (Exception e) {
|
||||
removeStream(stream);
|
||||
return processStreamOpenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getCircuitTypeLabel() {
|
||||
switch(type) {
|
||||
case HS_CIRCUIT:
|
||||
return "Hidden Service";
|
||||
case HS_DIRECTORY:
|
||||
return "HS Directory";
|
||||
case HS_INTRODUCTION:
|
||||
return "HS Introduction";
|
||||
case UNUSED:
|
||||
return "Internal";
|
||||
default:
|
||||
return "(null)";
|
||||
}
|
||||
}
|
||||
}
|
114
orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java
Normal file
114
orchid/src/com/subgraph/orchid/circuits/NTorCircuitExtender.java
Normal file
@ -0,0 +1,114 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.crypto.TorMessageDigest;
|
||||
import com.subgraph.orchid.crypto.TorNTorKeyAgreement;
|
||||
|
||||
public class NTorCircuitExtender {
|
||||
private final static Logger logger = Logger.getLogger(NTorCircuitExtender.class.getName());
|
||||
|
||||
private final CircuitExtender extender;
|
||||
private final Router router;
|
||||
private final TorNTorKeyAgreement kex;
|
||||
|
||||
public NTorCircuitExtender(CircuitExtender extender, Router router) {
|
||||
this.extender = extender;
|
||||
this.router = router;
|
||||
this.kex = new TorNTorKeyAgreement(router.getIdentityHash(), router.getNTorOnionKey());
|
||||
}
|
||||
|
||||
CircuitNode extendTo() {
|
||||
final byte[] onion = kex.createOnionSkin();
|
||||
if(finalRouterSupportsExtend2()) {
|
||||
logger.fine("Extending circuit to "+ router.getNickname() + " with NTor inside RELAY_EXTEND2");
|
||||
return extendWithExtend2(onion);
|
||||
} else {
|
||||
logger.fine("Extending circuit to "+ router.getNickname() + " with NTor inside RELAY_EXTEND");
|
||||
return extendWithTunneledExtend(onion);
|
||||
}
|
||||
}
|
||||
|
||||
private CircuitNode extendWithExtend2(byte[] onion) {
|
||||
final RelayCell cell = createExtend2Cell(onion);
|
||||
extender.sendRelayCell(cell);
|
||||
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED2, router);
|
||||
return processExtended2(response);
|
||||
}
|
||||
|
||||
private CircuitNode extendWithTunneledExtend(byte[] onion) {
|
||||
final RelayCell cell = createExtendCell(onion, kex.getNtorCreateMagic());
|
||||
extender.sendRelayCell(cell);
|
||||
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED, router);
|
||||
return processExtended(response);
|
||||
}
|
||||
|
||||
private boolean finalRouterSupportsExtend2() {
|
||||
return extender.getFinalRouter().getNTorOnionKey() != null;
|
||||
}
|
||||
|
||||
private RelayCell createExtend2Cell(byte[] ntorOnionskin) {
|
||||
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND2);
|
||||
|
||||
cell.putByte(2);
|
||||
|
||||
cell.putByte(0);
|
||||
cell.putByte(6);
|
||||
cell.putByteArray(router.getAddress().getAddressDataBytes());
|
||||
cell.putShort(router.getOnionPort());
|
||||
|
||||
cell.putByte(2);
|
||||
cell.putByte(20);
|
||||
cell.putByteArray(router.getIdentityHash().getRawBytes());
|
||||
|
||||
cell.putShort(0x0002);
|
||||
cell.putShort(ntorOnionskin.length);
|
||||
cell.putByteArray(ntorOnionskin);
|
||||
return cell;
|
||||
}
|
||||
|
||||
private RelayCell createExtendCell(byte[] ntorOnionskin, byte[] ntorMagic) {
|
||||
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND);
|
||||
cell.putByteArray(router.getAddress().getAddressDataBytes());
|
||||
cell.putShort(router.getOnionPort());
|
||||
final int paddingLength = CircuitExtender.TAP_ONIONSKIN_LEN - (ntorOnionskin.length + ntorMagic.length);
|
||||
final byte[] padding = new byte[paddingLength];
|
||||
cell.putByteArray(ntorMagic);
|
||||
cell.putByteArray(ntorOnionskin);
|
||||
cell.putByteArray(padding);
|
||||
cell.putByteArray(router.getIdentityHash().getRawBytes());
|
||||
return cell;
|
||||
}
|
||||
|
||||
private CircuitNode processExtended(RelayCell cell) {
|
||||
byte[] payload = new byte[CircuitExtender.TAP_ONIONSKIN_REPLY_LEN];
|
||||
cell.getByteArray(payload);
|
||||
|
||||
return processPayload(payload);
|
||||
}
|
||||
|
||||
|
||||
private CircuitNode processExtended2(RelayCell cell) {
|
||||
final int payloadLength = cell.getShort();
|
||||
if(payloadLength > cell.cellBytesRemaining()) {
|
||||
throw new TorException("Incorrect payload length value in RELAY_EXTENED2 cell");
|
||||
}
|
||||
byte[] payload = new byte[payloadLength];
|
||||
cell.getByteArray(payload);
|
||||
|
||||
return processPayload(payload);
|
||||
}
|
||||
|
||||
private CircuitNode processPayload(byte[] payload) {
|
||||
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
|
||||
final byte[] verifyDigest = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
|
||||
if(!kex.deriveKeysFromHandshakeResponse(payload, keyMaterial, verifyDigest)) {
|
||||
return null;
|
||||
}
|
||||
return extender.createNewNode(router, keyMaterial, verifyDigest);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.ExitCircuit;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
|
||||
public class OpenExitStreamTask implements Runnable {
|
||||
private final static Logger logger = Logger.getLogger(OpenExitStreamTask.class.getName());
|
||||
private final ExitCircuit circuit;
|
||||
private final StreamExitRequest exitRequest;
|
||||
|
||||
OpenExitStreamTask(ExitCircuit circuit, StreamExitRequest exitRequest) {
|
||||
this.circuit = circuit;
|
||||
this.exitRequest = exitRequest;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
logger.fine("Attempting to open stream to "+ exitRequest);
|
||||
try {
|
||||
exitRequest.setCompletedSuccessfully(tryOpenExitStream());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
exitRequest.setInterrupted();
|
||||
} catch (TimeoutException e) {
|
||||
circuit.markForClose();
|
||||
exitRequest.setCompletedTimeout();
|
||||
} catch (StreamConnectFailedException e) {
|
||||
if(!e.isReasonRetryable()) {
|
||||
exitRequest.setExitFailed();
|
||||
circuit.recordFailedExitTarget(exitRequest);
|
||||
} else {
|
||||
circuit.markForClose();
|
||||
exitRequest.setStreamOpenFailure(e.getReason());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private Stream tryOpenExitStream() throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
if(exitRequest.isAddressTarget()) {
|
||||
return circuit.openExitStream(exitRequest.getAddress(), exitRequest.getPort(), exitRequest.getStreamTimeout());
|
||||
} else {
|
||||
return circuit.openExitStream(exitRequest.getHostname(), exitRequest.getPort(), exitRequest.getStreamTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.OpenFailedException;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.TorConfig;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
|
||||
public class PendingExitStreams {
|
||||
|
||||
private final Set<StreamExitRequest> pendingRequests;
|
||||
private final Object lock = new Object();
|
||||
private final TorConfig config;
|
||||
|
||||
PendingExitStreams(TorConfig config) {
|
||||
this.config = config;
|
||||
pendingRequests = new HashSet<StreamExitRequest>();
|
||||
}
|
||||
|
||||
Stream openExitStream(IPv4Address address, int port) throws InterruptedException, OpenFailedException {
|
||||
final StreamExitRequest request = new StreamExitRequest(lock, address, port);
|
||||
return openExitStreamByRequest(request);
|
||||
}
|
||||
|
||||
Stream openExitStream(String hostname, int port) throws InterruptedException, OpenFailedException {
|
||||
final StreamExitRequest request = new StreamExitRequest(lock, hostname, port);
|
||||
return openExitStreamByRequest(request);
|
||||
}
|
||||
|
||||
private Stream openExitStreamByRequest(StreamExitRequest request) throws InterruptedException, OpenFailedException {
|
||||
if(config.getCircuitStreamTimeout() != 0) {
|
||||
request.setStreamTimeout(config.getCircuitStreamTimeout());
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
pendingRequests.add(request);
|
||||
try {
|
||||
return handleRequest(request);
|
||||
} finally {
|
||||
pendingRequests.remove(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream handleRequest(StreamExitRequest request) throws InterruptedException, OpenFailedException {
|
||||
while(true) {
|
||||
while(!request.isCompleted()) {
|
||||
lock.wait();
|
||||
}
|
||||
try {
|
||||
return request.getStream();
|
||||
} catch (TimeoutException e) {
|
||||
request.resetForRetry();
|
||||
} catch (StreamConnectFailedException e) {
|
||||
request.resetForRetry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<StreamExitRequest> getUnreservedPendingRequests() {
|
||||
final List<StreamExitRequest> result = new ArrayList<StreamExitRequest>();
|
||||
synchronized (lock) {
|
||||
for(StreamExitRequest request: pendingRequests) {
|
||||
if(!request.isReserved()) {
|
||||
result.add(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
|
||||
public class PredictedPortTarget implements ExitTarget {
|
||||
|
||||
final int port;
|
||||
|
||||
public PredictedPortTarget(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public boolean isAddressTarget() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public IPv4Address getAddress() {
|
||||
return new IPv4Address(0);
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
170
orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java
Normal file
170
orchid/src/com/subgraph/orchid/circuits/StreamExitRequest.java
Normal file
@ -0,0 +1,170 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.subgraph.orchid.OpenFailedException;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.data.exitpolicy.ExitTarget;
|
||||
import com.subgraph.orchid.misc.GuardedBy;
|
||||
|
||||
public class StreamExitRequest implements ExitTarget {
|
||||
|
||||
private enum CompletionStatus {NOT_COMPLETED, SUCCESS, TIMEOUT, STREAM_OPEN_FAILURE, EXIT_FAILURE, INTERRUPTED};
|
||||
|
||||
private final boolean isAddress;
|
||||
private final IPv4Address address;
|
||||
private final String hostname;
|
||||
private final int port;
|
||||
private final Object requestCompletionLock;
|
||||
|
||||
@GuardedBy("requestCompletionLock") private CompletionStatus completionStatus;
|
||||
@GuardedBy("requestCompletionLock") private Stream stream;
|
||||
@GuardedBy("requestCompletionLock") private int streamOpenFailReason;
|
||||
|
||||
@GuardedBy("this") private boolean isReserved;
|
||||
@GuardedBy("this") private int retryCount;
|
||||
@GuardedBy("this") private long specificTimeout;
|
||||
|
||||
StreamExitRequest(Object requestCompletionLock, IPv4Address address, int port) {
|
||||
this(requestCompletionLock, true, "", address, port);
|
||||
}
|
||||
|
||||
StreamExitRequest(Object requestCompletionLock, String hostname, int port) {
|
||||
this(requestCompletionLock, false, hostname, null, port);
|
||||
}
|
||||
|
||||
private StreamExitRequest(Object requestCompletionLock, boolean isAddress, String hostname, IPv4Address address, int port) {
|
||||
this.requestCompletionLock = requestCompletionLock;
|
||||
this.isAddress = isAddress;
|
||||
this.hostname = hostname;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.completionStatus = CompletionStatus.NOT_COMPLETED;
|
||||
}
|
||||
|
||||
public boolean isAddressTarget() {
|
||||
return isAddress;
|
||||
}
|
||||
|
||||
public IPv4Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public synchronized void setStreamTimeout(long timeout) {
|
||||
specificTimeout = timeout;
|
||||
}
|
||||
|
||||
public synchronized long getStreamTimeout() {
|
||||
if(specificTimeout > 0) {
|
||||
return specificTimeout;
|
||||
} else if(retryCount < 2) {
|
||||
return 10 * 1000;
|
||||
} else {
|
||||
return 15 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
void setCompletedTimeout() {
|
||||
synchronized (requestCompletionLock) {
|
||||
newStatus(CompletionStatus.TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
void setExitFailed() {
|
||||
synchronized (requestCompletionLock) {
|
||||
newStatus(CompletionStatus.EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void setStreamOpenFailure(int reason) {
|
||||
synchronized (requestCompletionLock) {
|
||||
streamOpenFailReason = reason;
|
||||
newStatus(CompletionStatus.STREAM_OPEN_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void setCompletedSuccessfully(Stream stream) {
|
||||
synchronized (requestCompletionLock) {
|
||||
this.stream = stream;
|
||||
newStatus(CompletionStatus.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
void setInterrupted() {
|
||||
synchronized (requestCompletionLock) {
|
||||
newStatus(CompletionStatus.INTERRUPTED);
|
||||
}
|
||||
}
|
||||
|
||||
private void newStatus(CompletionStatus newStatus) {
|
||||
if(completionStatus != CompletionStatus.NOT_COMPLETED) {
|
||||
throw new IllegalStateException("Attempt to set completion state to " + newStatus +" while status is "+ completionStatus);
|
||||
}
|
||||
completionStatus = newStatus;
|
||||
requestCompletionLock.notifyAll();
|
||||
}
|
||||
|
||||
|
||||
Stream getStream() throws OpenFailedException, TimeoutException, StreamConnectFailedException, InterruptedException {
|
||||
synchronized(requestCompletionLock) {
|
||||
switch(completionStatus) {
|
||||
case NOT_COMPLETED:
|
||||
throw new IllegalStateException("Request not completed");
|
||||
case EXIT_FAILURE:
|
||||
throw new OpenFailedException("Failure at exit node");
|
||||
case TIMEOUT:
|
||||
throw new TimeoutException();
|
||||
case STREAM_OPEN_FAILURE:
|
||||
throw new StreamConnectFailedException(streamOpenFailReason);
|
||||
case INTERRUPTED:
|
||||
throw new InterruptedException();
|
||||
case SUCCESS:
|
||||
return stream;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown completion status");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void resetForRetry() {
|
||||
synchronized (requestCompletionLock) {
|
||||
streamOpenFailReason = 0;
|
||||
completionStatus = CompletionStatus.NOT_COMPLETED;
|
||||
}
|
||||
retryCount += 1;
|
||||
isReserved = false;
|
||||
}
|
||||
|
||||
boolean isCompleted() {
|
||||
synchronized (requestCompletionLock) {
|
||||
return completionStatus != CompletionStatus.NOT_COMPLETED;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean reserveRequest() {
|
||||
if(isReserved) return false;
|
||||
isReserved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized boolean isReserved() {
|
||||
return isReserved;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if(isAddress)
|
||||
return address + ":"+ port;
|
||||
else
|
||||
return hostname + ":"+ port;
|
||||
}
|
||||
}
|
219
orchid/src/com/subgraph/orchid/circuits/StreamImpl.java
Normal file
219
orchid/src/com/subgraph/orchid/circuits/StreamImpl.java
Normal file
@ -0,0 +1,219 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Circuit;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderable;
|
||||
import com.subgraph.orchid.dashboard.DashboardRenderer;
|
||||
|
||||
public class StreamImpl implements Stream, DashboardRenderable {
|
||||
private final static Logger logger = Logger.getLogger(StreamImpl.class.getName());
|
||||
|
||||
private final static int STREAMWINDOW_START = 500;
|
||||
private final static int STREAMWINDOW_INCREMENT = 50;
|
||||
private final static int STREAMWINDOW_MAX_UNFLUSHED = 10;
|
||||
|
||||
private final CircuitImpl circuit;
|
||||
|
||||
private final int streamId;
|
||||
private final boolean autoclose;
|
||||
|
||||
private final CircuitNode targetNode;
|
||||
private final TorInputStream inputStream;
|
||||
private final TorOutputStream outputStream;
|
||||
|
||||
private boolean isClosed;
|
||||
private boolean relayEndReceived;
|
||||
private int relayEndReason;
|
||||
private boolean relayConnectedReceived;
|
||||
private final Object waitConnectLock = new Object();
|
||||
private final Object windowLock = new Object();
|
||||
private int packageWindow;
|
||||
private int deliverWindow;
|
||||
|
||||
private String streamTarget = "";
|
||||
|
||||
StreamImpl(CircuitImpl circuit, CircuitNode targetNode, int streamId, boolean autoclose) {
|
||||
this.circuit = circuit;
|
||||
this.targetNode = targetNode;
|
||||
this.streamId = streamId;
|
||||
this.autoclose = autoclose;
|
||||
this.inputStream = new TorInputStream(this);
|
||||
this.outputStream = new TorOutputStream(this);
|
||||
packageWindow = STREAMWINDOW_START;
|
||||
deliverWindow = STREAMWINDOW_START;
|
||||
}
|
||||
|
||||
void addInputCell(RelayCell cell) {
|
||||
if(isClosed)
|
||||
return;
|
||||
if(cell.getRelayCommand() == RelayCell.RELAY_END) {
|
||||
synchronized(waitConnectLock) {
|
||||
relayEndReason = cell.getByte();
|
||||
relayEndReceived = true;
|
||||
inputStream.addEndCell(cell);
|
||||
waitConnectLock.notifyAll();
|
||||
}
|
||||
} else if(cell.getRelayCommand() == RelayCell.RELAY_CONNECTED) {
|
||||
synchronized(waitConnectLock) {
|
||||
relayConnectedReceived = true;
|
||||
waitConnectLock.notifyAll();
|
||||
}
|
||||
} else if(cell.getRelayCommand() == RelayCell.RELAY_SENDME) {
|
||||
synchronized(windowLock) {
|
||||
packageWindow += STREAMWINDOW_INCREMENT;
|
||||
windowLock.notifyAll();
|
||||
}
|
||||
}
|
||||
else {
|
||||
inputStream.addInputCell(cell);
|
||||
synchronized(windowLock) {
|
||||
deliverWindow--;
|
||||
if(deliverWindow < 0)
|
||||
throw new TorException("Stream has negative delivery window");
|
||||
}
|
||||
considerSendingSendme();
|
||||
}
|
||||
}
|
||||
|
||||
private void considerSendingSendme() {
|
||||
synchronized(windowLock) {
|
||||
if(deliverWindow > (STREAMWINDOW_START - STREAMWINDOW_INCREMENT))
|
||||
return;
|
||||
|
||||
if(inputStream.unflushedCellCount() >= STREAMWINDOW_MAX_UNFLUSHED)
|
||||
return;
|
||||
|
||||
final RelayCell sendme = circuit.createRelayCell(RelayCell.RELAY_SENDME, streamId, targetNode);
|
||||
circuit.sendRelayCell(sendme);
|
||||
deliverWindow += STREAMWINDOW_INCREMENT;
|
||||
}
|
||||
}
|
||||
|
||||
public int getStreamId() {
|
||||
return streamId;
|
||||
}
|
||||
|
||||
public Circuit getCircuit() {
|
||||
return circuit;
|
||||
}
|
||||
|
||||
public CircuitNode getTargetNode() {
|
||||
return targetNode;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if(isClosed)
|
||||
return;
|
||||
|
||||
logger.fine("Closing stream "+ this);
|
||||
|
||||
isClosed = true;
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
circuit.removeStream(this);
|
||||
if(autoclose) {
|
||||
circuit.markForClose();
|
||||
}
|
||||
|
||||
if(!relayEndReceived) {
|
||||
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_END);
|
||||
cell.putByte(RelayCell.REASON_DONE);
|
||||
circuit.sendRelayCellToFinalNode(cell);
|
||||
}
|
||||
}
|
||||
|
||||
public void openDirectory(long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
streamTarget = "[Directory]";
|
||||
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_BEGIN_DIR);
|
||||
circuit.sendRelayCellToFinalNode(cell);
|
||||
waitForRelayConnected(timeout);
|
||||
}
|
||||
|
||||
void openExit(String target, int port, long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
streamTarget = target + ":"+ port;
|
||||
final RelayCell cell = new RelayCellImpl(circuit.getFinalCircuitNode(), circuit.getCircuitId(), streamId, RelayCell.RELAY_BEGIN);
|
||||
cell.putString(target + ":"+ port);
|
||||
circuit.sendRelayCellToFinalNode(cell);
|
||||
waitForRelayConnected(timeout);
|
||||
}
|
||||
|
||||
private void waitForRelayConnected(long timeout) throws InterruptedException, TimeoutException, StreamConnectFailedException {
|
||||
final long start = System.currentTimeMillis();
|
||||
long elapsed = 0;
|
||||
synchronized(waitConnectLock) {
|
||||
while(!relayConnectedReceived) {
|
||||
|
||||
if(relayEndReceived) {
|
||||
throw new StreamConnectFailedException(relayEndReason);
|
||||
}
|
||||
|
||||
if(elapsed >= timeout) {
|
||||
throw new TimeoutException();
|
||||
}
|
||||
|
||||
waitConnectLock.wait(timeout - elapsed);
|
||||
|
||||
elapsed = System.currentTimeMillis() - start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
public void waitForSendWindowAndDecrement() {
|
||||
waitForSendWindow(true);
|
||||
}
|
||||
|
||||
public void waitForSendWindow() {
|
||||
waitForSendWindow(false);
|
||||
}
|
||||
|
||||
public void waitForSendWindow(boolean decrement) {
|
||||
synchronized(windowLock) {
|
||||
while(packageWindow == 0) {
|
||||
try {
|
||||
windowLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new TorException("Thread interrupted while waiting for stream package window");
|
||||
}
|
||||
}
|
||||
if(decrement)
|
||||
packageWindow--;
|
||||
}
|
||||
targetNode.waitForSendWindow();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "[Stream stream_id="+ streamId + " circuit="+ circuit +" target="+ streamTarget +"]";
|
||||
}
|
||||
|
||||
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
|
||||
writer.print(" ");
|
||||
writer.print("[Stream stream_id="+ streamId + " cid="+ circuit.getCircuitId());
|
||||
if(relayConnectedReceived) {
|
||||
writer.print(" sent="+outputStream.getBytesSent() + " recv="+ inputStream.getBytesReceived());
|
||||
} else {
|
||||
writer.print(" (waiting connect)");
|
||||
}
|
||||
writer.print(" target="+ streamTarget);
|
||||
writer.println("]");
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.crypto.TorMessageDigest;
|
||||
import com.subgraph.orchid.crypto.TorTapKeyAgreement;
|
||||
|
||||
public class TapCircuitExtender {
|
||||
private final static Logger logger = Logger.getLogger(TapCircuitExtender.class.getName());
|
||||
|
||||
private final CircuitExtender extender;
|
||||
private final TorTapKeyAgreement kex;
|
||||
private final Router router;
|
||||
|
||||
public TapCircuitExtender(CircuitExtender extender, Router router) {
|
||||
this.extender = extender;
|
||||
this.router = router;
|
||||
this.kex = new TorTapKeyAgreement(router.getOnionKey());
|
||||
}
|
||||
|
||||
public CircuitNode extendTo() {
|
||||
logger.fine("Extending to "+ router.getNickname() + " with TAP");
|
||||
final RelayCell cell = createRelayExtendCell();
|
||||
extender.sendRelayCell(cell);
|
||||
final RelayCell response = extender.receiveRelayResponse(RelayCell.RELAY_EXTENDED, router);
|
||||
if(response == null) {
|
||||
return null;
|
||||
}
|
||||
return processExtendResponse(response);
|
||||
}
|
||||
|
||||
private CircuitNode processExtendResponse(RelayCell response) {
|
||||
final byte[] handshakeResponse = new byte[TorTapKeyAgreement.DH_LEN + TorMessageDigest.TOR_DIGEST_SIZE];
|
||||
response.getByteArray(handshakeResponse);
|
||||
|
||||
final byte[] keyMaterial = new byte[CircuitNodeCryptoState.KEY_MATERIAL_SIZE];
|
||||
final byte[] verifyDigest = new byte[TorMessageDigest.TOR_DIGEST_SIZE];
|
||||
if(!kex.deriveKeysFromHandshakeResponse(handshakeResponse, keyMaterial, verifyDigest)) {
|
||||
return null;
|
||||
}
|
||||
return extender.createNewNode(router, keyMaterial, verifyDigest);
|
||||
}
|
||||
|
||||
private RelayCell createRelayExtendCell() {
|
||||
final RelayCell cell = extender.createRelayCell(RelayCell.RELAY_EXTEND);
|
||||
cell.putByteArray(router.getAddress().getAddressDataBytes());
|
||||
cell.putShort(router.getOnionPort());
|
||||
cell.putByteArray(kex.createOnionSkin());
|
||||
cell.putByteArray(router.getIdentityHash().getRawBytes());
|
||||
return cell;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.Tor;
|
||||
import com.subgraph.orchid.TorInitializationListener;
|
||||
|
||||
public class TorInitializationTracker {
|
||||
private final static Logger logger = Logger.getLogger(TorInitializationTracker.class.getName());
|
||||
private final static Map<Integer, String> messageMap = new HashMap<Integer, String>();
|
||||
|
||||
static {
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_STARTING, "Starting");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_CONN_DIR, "Connecting to directory server");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_HANDSHAKE_DIR, "Finishing handshake with directory server");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_ONEHOP_CREATE, "Establishing an encrypted directory connection");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS, "Asking for networkstatus consensus");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_STATUS, "Loading networkstatus consensus");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS, "Asking for authority key certs");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_KEYS, "Loading authority key certs");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS, "Asking for relay descriptors");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS, "Loading relay descriptors");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_CONN_OR, "Connecting to the Tor network");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_HANDSHAKE_OR, "Finished Handshake with first hop");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_CIRCUIT_CREATE, "Establishing a Tor circuit");
|
||||
messageMap.put(Tor.BOOTSTRAP_STATUS_DONE, "Done");
|
||||
}
|
||||
|
||||
private final List<TorInitializationListener> listeners = new ArrayList<TorInitializationListener>();
|
||||
|
||||
private final Object stateLock = new Object();
|
||||
private int bootstrapState = Tor.BOOTSTRAP_STATUS_STARTING;
|
||||
|
||||
|
||||
public void addListener(TorInitializationListener listener) {
|
||||
synchronized(listeners) {
|
||||
if(!listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(TorInitializationListener listener) {
|
||||
synchronized(listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public int getBootstrapState() {
|
||||
return bootstrapState;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
synchronized (stateLock) {
|
||||
bootstrapState = Tor.BOOTSTRAP_STATUS_STARTING;
|
||||
notifyListeners(Tor.BOOTSTRAP_STATUS_STARTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyEvent(int eventCode) {
|
||||
synchronized(stateLock) {
|
||||
if(eventCode <= bootstrapState || eventCode > 100) {
|
||||
return;
|
||||
}
|
||||
bootstrapState = eventCode;
|
||||
notifyListeners(eventCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListeners(int code) {
|
||||
final String message = getMessageForCode(code);
|
||||
for(TorInitializationListener listener: getListeners()) {
|
||||
try {
|
||||
listener.initializationProgress(message, code);
|
||||
if(code >= 100) {
|
||||
listener.initializationCompleted();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.log(Level.SEVERE, "Exception occurred in TorInitializationListener callback: "+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessageForCode(int code) {
|
||||
if(messageMap.containsKey(code)) {
|
||||
return messageMap.get(code);
|
||||
} else {
|
||||
return "Unknown state";
|
||||
}
|
||||
}
|
||||
|
||||
private List<TorInitializationListener> getListeners() {
|
||||
synchronized (listeners) {
|
||||
return new ArrayList<TorInitializationListener>(listeners);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
228
orchid/src/com/subgraph/orchid/circuits/TorInputStream.java
Normal file
228
orchid/src/com/subgraph/orchid/circuits/TorInputStream.java
Normal file
@ -0,0 +1,228 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
|
||||
import com.subgraph.orchid.misc.GuardedBy;
|
||||
import com.subgraph.orchid.misc.ThreadSafe;
|
||||
|
||||
@ThreadSafe
|
||||
public class TorInputStream extends InputStream {
|
||||
|
||||
private final static RelayCell CLOSE_SENTINEL = new RelayCellImpl(null, 0, 0, 0);
|
||||
private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
|
||||
|
||||
private final Stream stream;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
/** Queue of RelayCells that have been received on this stream */
|
||||
@GuardedBy("lock") private final Queue<RelayCell> incomingCells;
|
||||
|
||||
/** Number of unread data bytes in current buffer and in RELAY_DATA cells on queue */
|
||||
@GuardedBy("lock") private int availableBytes;
|
||||
|
||||
/** Total number of data bytes received in RELAY_DATA cells on this stream */
|
||||
@GuardedBy("lock") private long bytesReceived;
|
||||
|
||||
/** Bytes of data from the RELAY_DATA cell currently being consumed */
|
||||
@GuardedBy("lock") private ByteBuffer currentBuffer;
|
||||
|
||||
/** Set when a RELAY_END cell is received */
|
||||
@GuardedBy("lock") private boolean isEOF;
|
||||
|
||||
/** Set when close() is called on this stream */
|
||||
@GuardedBy("lock") private boolean isClosed;
|
||||
|
||||
TorInputStream(Stream stream) {
|
||||
this.stream = stream;
|
||||
this.incomingCells = new LinkedList<RelayCell>();
|
||||
this.currentBuffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
long getBytesReceived() {
|
||||
synchronized (lock) {
|
||||
return bytesReceived;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
synchronized (lock) {
|
||||
if(isClosed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
refillBufferIfNeeded();
|
||||
if(isEOF) {
|
||||
return -1;
|
||||
}
|
||||
availableBytes -= 1;
|
||||
return currentBuffer.get() & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
synchronized (lock) {
|
||||
if(isClosed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
|
||||
checkReadArguments(b, off, len);
|
||||
|
||||
if(len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
refillBufferIfNeeded();
|
||||
if(isEOF) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int bytesRead = 0;
|
||||
int bytesRemaining = len;
|
||||
|
||||
while(bytesRemaining > 0 && !isEOF) {
|
||||
refillBufferIfNeeded();
|
||||
bytesRead += readFromCurrentBuffer(b, off + bytesRead, len - bytesRead);
|
||||
bytesRemaining = len - bytesRead;
|
||||
if(availableBytes == 0) {
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private int readFromCurrentBuffer(byte[] b, int off, int len) {
|
||||
final int readLength = (currentBuffer.remaining() >= len) ? (len) : (currentBuffer.remaining());
|
||||
currentBuffer.get(b, off, readLength);
|
||||
availableBytes -= readLength;
|
||||
return readLength;
|
||||
}
|
||||
|
||||
private void checkReadArguments(byte[] b, int off, int len) {
|
||||
if(b == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if( (off < 0) || (off >= b.length) || (len < 0) ||
|
||||
((off + len) > b.length) || ((off + len) < 0)) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
public int available() {
|
||||
synchronized(lock) {
|
||||
return availableBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
synchronized (lock) {
|
||||
if(isClosed) {
|
||||
return;
|
||||
}
|
||||
isClosed = true;
|
||||
|
||||
incomingCells.add(CLOSE_SENTINEL);
|
||||
lock.notifyAll();
|
||||
}
|
||||
stream.close();
|
||||
}
|
||||
|
||||
void addEndCell(RelayCell cell) {
|
||||
synchronized (lock) {
|
||||
if(isClosed) {
|
||||
return;
|
||||
}
|
||||
incomingCells.add(cell);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
void addInputCell(RelayCell cell) {
|
||||
synchronized (lock) {
|
||||
if(isClosed) {
|
||||
return;
|
||||
}
|
||||
incomingCells.add(cell);
|
||||
bytesReceived += cell.cellBytesRemaining();
|
||||
availableBytes += cell.cellBytesRemaining();
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
// When this method (or fillBuffer()) returns either isEOF is set or currentBuffer has at least one byte to read
|
||||
private void refillBufferIfNeeded() throws IOException {
|
||||
if(!isEOF) {
|
||||
if(currentBuffer.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
fillBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void fillBuffer() throws IOException {
|
||||
while(true) {
|
||||
processIncomingCell(getNextCell());
|
||||
if(isEOF || currentBuffer.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void processIncomingCell(RelayCell nextCell) throws IOException {
|
||||
if(isClosed || nextCell == CLOSE_SENTINEL) {
|
||||
throw new IOException("Input stream closed");
|
||||
}
|
||||
|
||||
switch(nextCell.getRelayCommand()) {
|
||||
case RelayCell.RELAY_DATA:
|
||||
currentBuffer = nextCell.getPayloadBuffer();
|
||||
break;
|
||||
case RelayCell.RELAY_END:
|
||||
currentBuffer = EMPTY_BUFFER;
|
||||
isEOF = true;
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unexpected RelayCell command type in TorInputStream queue: "+ nextCell.getRelayCommand());
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private RelayCell getNextCell() throws IOException {
|
||||
try {
|
||||
while(incomingCells.isEmpty()) {
|
||||
lock.wait();
|
||||
}
|
||||
return incomingCells.remove();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Read interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
int unflushedCellCount() {
|
||||
synchronized (lock) {
|
||||
return incomingCells.size();
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "TorInputStream stream="+ stream.getStreamId() +" node="+ stream.getTargetNode();
|
||||
}
|
||||
}
|
85
orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java
Normal file
85
orchid/src/com/subgraph/orchid/circuits/TorOutputStream.java
Normal file
@ -0,0 +1,85 @@
|
||||
package com.subgraph.orchid.circuits;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.circuits.cells.RelayCellImpl;
|
||||
|
||||
public class TorOutputStream extends OutputStream {
|
||||
|
||||
private final StreamImpl stream;
|
||||
private RelayCell currentOutputCell;
|
||||
private volatile boolean isClosed;
|
||||
private long bytesSent;
|
||||
|
||||
TorOutputStream(StreamImpl stream) {
|
||||
this.stream = stream;
|
||||
this.bytesSent = 0;
|
||||
}
|
||||
|
||||
private void flushCurrentOutputCell() {
|
||||
if(currentOutputCell != null && currentOutputCell.cellBytesConsumed() > RelayCell.HEADER_SIZE) {
|
||||
stream.waitForSendWindowAndDecrement();
|
||||
stream.getCircuit().sendRelayCell(currentOutputCell);
|
||||
bytesSent += (currentOutputCell.cellBytesConsumed() - RelayCell.HEADER_SIZE);
|
||||
}
|
||||
|
||||
currentOutputCell = new RelayCellImpl(stream.getTargetNode(), stream.getCircuit().getCircuitId(),
|
||||
stream.getStreamId(), RelayCell.RELAY_DATA);
|
||||
}
|
||||
|
||||
long getBytesSent() {
|
||||
return bytesSent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) throws IOException {
|
||||
checkOpen();
|
||||
if(currentOutputCell == null || currentOutputCell.cellBytesRemaining() == 0)
|
||||
flushCurrentOutputCell();
|
||||
currentOutputCell.putByte(b);
|
||||
}
|
||||
|
||||
public synchronized void write(byte[] data, int offset, int length) throws IOException {
|
||||
checkOpen();
|
||||
if(currentOutputCell == null || currentOutputCell.cellBytesRemaining() == 0)
|
||||
flushCurrentOutputCell();
|
||||
|
||||
while(length > 0) {
|
||||
if(length < currentOutputCell.cellBytesRemaining()) {
|
||||
currentOutputCell.putByteArray(data, offset, length);
|
||||
return;
|
||||
}
|
||||
final int writeCount = currentOutputCell.cellBytesRemaining();
|
||||
currentOutputCell.putByteArray(data, offset, writeCount);
|
||||
flushCurrentOutputCell();
|
||||
offset += writeCount;
|
||||
length -= writeCount;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOpen() throws IOException {
|
||||
if(isClosed)
|
||||
throw new IOException("Output stream is closed");
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
if(isClosed)
|
||||
return;
|
||||
flushCurrentOutputCell();
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
if(isClosed)
|
||||
return;
|
||||
flush();
|
||||
isClosed = true;
|
||||
currentOutputCell = null;
|
||||
stream.close();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "TorOutputStream stream="+ stream.getStreamId() +" node="+ stream.getTargetNode();
|
||||
}
|
||||
}
|
215
orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java
Normal file
215
orchid/src/com/subgraph/orchid/circuits/cells/CellImpl.java
Normal file
@ -0,0 +1,215 @@
|
||||
package com.subgraph.orchid.circuits.cells;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
|
||||
public class CellImpl implements Cell {
|
||||
|
||||
public static CellImpl createCell(int circuitId, int command) {
|
||||
return new CellImpl(circuitId, command);
|
||||
}
|
||||
|
||||
public static CellImpl createVarCell(int circuitId, int command, int payloadLength) {
|
||||
return new CellImpl(circuitId, command, payloadLength);
|
||||
}
|
||||
|
||||
public static CellImpl readFromInputStream(InputStream input) throws IOException {
|
||||
final ByteBuffer header = readHeaderFromInputStream(input);
|
||||
final int circuitId = header.getShort() & 0xFFFF;
|
||||
final int command = header.get() & 0xFF;
|
||||
|
||||
if(command == VERSIONS || command > 127) {
|
||||
return readVarCell(circuitId, command, input);
|
||||
}
|
||||
|
||||
final CellImpl cell = new CellImpl(circuitId, command);
|
||||
readAll(input, cell.getCellBytes(), CELL_HEADER_LEN, CELL_PAYLOAD_LEN);
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
private static ByteBuffer readHeaderFromInputStream(InputStream input) throws IOException {
|
||||
final byte[] cellHeader = new byte[CELL_HEADER_LEN];
|
||||
readAll(input, cellHeader);
|
||||
return ByteBuffer.wrap(cellHeader);
|
||||
}
|
||||
|
||||
private static CellImpl readVarCell(int circuitId, int command, InputStream input) throws IOException {
|
||||
final byte[] lengthField = new byte[2];
|
||||
readAll(input, lengthField);
|
||||
final int length = ((lengthField[0] & 0xFF) << 8) | (lengthField[1] & 0xFF);
|
||||
CellImpl cell = new CellImpl(circuitId, command, length);
|
||||
readAll(input, cell.getCellBytes(), CELL_VAR_HEADER_LEN, length);
|
||||
return cell;
|
||||
}
|
||||
|
||||
private static void readAll(InputStream input, byte[] buffer) throws IOException {
|
||||
readAll(input, buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
private static void readAll(InputStream input, byte[] buffer, int offset, int length) throws IOException {
|
||||
int bytesRead = 0;
|
||||
while(bytesRead < length) {
|
||||
final int n = input.read(buffer, offset + bytesRead, length - bytesRead);
|
||||
if(n == -1)
|
||||
throw new EOFException();
|
||||
bytesRead += n;
|
||||
}
|
||||
}
|
||||
|
||||
private final int circuitId;
|
||||
private final int command;
|
||||
protected final ByteBuffer cellBuffer;
|
||||
|
||||
/* Variable length cell constructor (ie: VERSIONS cells only) */
|
||||
private CellImpl(int circuitId, int command, int payloadLength) {
|
||||
this.circuitId = circuitId;
|
||||
this.command = command;
|
||||
this.cellBuffer = ByteBuffer.wrap(new byte[CELL_VAR_HEADER_LEN + payloadLength]);
|
||||
cellBuffer.putShort((short)circuitId);
|
||||
cellBuffer.put((byte)command);
|
||||
cellBuffer.putShort((short) payloadLength);
|
||||
cellBuffer.mark();
|
||||
}
|
||||
|
||||
/* Fixed length cell constructor */
|
||||
protected CellImpl(int circuitId, int command) {
|
||||
this.circuitId = circuitId;
|
||||
this.command = command;
|
||||
this.cellBuffer = ByteBuffer.wrap(new byte[CELL_LEN]);
|
||||
cellBuffer.putShort((short) circuitId);
|
||||
cellBuffer.put((byte) command);
|
||||
cellBuffer.mark();
|
||||
}
|
||||
|
||||
protected CellImpl(byte[] rawCell) {
|
||||
this.cellBuffer = ByteBuffer.wrap(rawCell);
|
||||
this.circuitId = cellBuffer.getShort() & 0xFFFF;
|
||||
this.command = cellBuffer.get() & 0xFF;
|
||||
cellBuffer.mark();
|
||||
}
|
||||
|
||||
public int getCircuitId() {
|
||||
return circuitId;
|
||||
}
|
||||
|
||||
public int getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public void resetToPayload() {
|
||||
cellBuffer.reset();
|
||||
}
|
||||
|
||||
public int getByte() {
|
||||
return cellBuffer.get() & 0xFF;
|
||||
}
|
||||
|
||||
public int getByteAt(int index) {
|
||||
return cellBuffer.get(index) & 0xFF;
|
||||
}
|
||||
|
||||
public int getShort() {
|
||||
return cellBuffer.getShort() & 0xFFFF;
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return cellBuffer.getInt();
|
||||
}
|
||||
|
||||
public int getShortAt(int index) {
|
||||
return cellBuffer.getShort(index) & 0xFFFF;
|
||||
}
|
||||
|
||||
public void getByteArray(byte[] buffer) {
|
||||
cellBuffer.get(buffer);
|
||||
}
|
||||
|
||||
public int cellBytesConsumed() {
|
||||
return cellBuffer.position();
|
||||
}
|
||||
|
||||
public int cellBytesRemaining() {
|
||||
return cellBuffer.remaining();
|
||||
}
|
||||
|
||||
public void putByte(int value) {
|
||||
cellBuffer.put((byte) value);
|
||||
}
|
||||
|
||||
public void putByteAt(int index, int value) {
|
||||
cellBuffer.put(index, (byte) value);
|
||||
}
|
||||
|
||||
public void putShort(int value) {
|
||||
cellBuffer.putShort((short) value);
|
||||
}
|
||||
|
||||
public void putShortAt(int index, int value) {
|
||||
cellBuffer.putShort(index, (short) value);
|
||||
}
|
||||
|
||||
public void putInt(int value) {
|
||||
cellBuffer.putInt(value);
|
||||
}
|
||||
|
||||
public void putString(String string) {
|
||||
final byte[] bytes = new byte[string.length() + 1];
|
||||
for(int i = 0; i < string.length(); i++)
|
||||
bytes[i] = (byte) string.charAt(i);
|
||||
putByteArray(bytes);
|
||||
}
|
||||
|
||||
public void putByteArray(byte[] data) {
|
||||
cellBuffer.put(data);
|
||||
}
|
||||
|
||||
public void putByteArray(byte[] data, int offset, int length) {
|
||||
cellBuffer.put(data, offset, length);
|
||||
}
|
||||
|
||||
public byte[] getCellBytes() {
|
||||
return cellBuffer.array();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Cell: circuit_id="+ circuitId +" command="+ command +" payload_len="+ cellBuffer.position();
|
||||
}
|
||||
|
||||
public static String errorToDescription(int errorCode) {
|
||||
switch(errorCode) {
|
||||
case ERROR_NONE:
|
||||
return "No error reason given";
|
||||
case ERROR_PROTOCOL:
|
||||
return "Tor protocol violation";
|
||||
case ERROR_INTERNAL:
|
||||
return "Internal error";
|
||||
case ERROR_REQUESTED:
|
||||
return "Response to a TRUNCATE command sent from client";
|
||||
case ERROR_HIBERNATING:
|
||||
return "Not currently operating; trying to save bandwidth.";
|
||||
case ERROR_RESOURCELIMIT:
|
||||
return "Out of memory, sockets, or circuit IDs.";
|
||||
case ERROR_CONNECTFAILED:
|
||||
return "Unable to reach server.";
|
||||
case ERROR_OR_IDENTITY:
|
||||
return "Connected to server, but its OR identity was not as expected.";
|
||||
case ERROR_OR_CONN_CLOSED:
|
||||
return "The OR connection that was carrying this circuit died.";
|
||||
case ERROR_FINISHED:
|
||||
return "The circuit has expired for being dirty or old.";
|
||||
case ERROR_TIMEOUT:
|
||||
return "Circuit construction took too long.";
|
||||
case ERROR_DESTROYED:
|
||||
return "The circuit was destroyed without client TRUNCATE";
|
||||
case ERROR_NOSUCHSERVICE:
|
||||
return "Request for unknown hidden service";
|
||||
default:
|
||||
return "Error code "+ errorCode;
|
||||
}
|
||||
}
|
||||
}
|
180
orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java
Normal file
180
orchid/src/com/subgraph/orchid/circuits/cells/RelayCellImpl.java
Normal file
@ -0,0 +1,180 @@
|
||||
package com.subgraph.orchid.circuits.cells;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.subgraph.orchid.Cell;
|
||||
import com.subgraph.orchid.CircuitNode;
|
||||
import com.subgraph.orchid.RelayCell;
|
||||
import com.subgraph.orchid.TorException;
|
||||
|
||||
public class RelayCellImpl extends CellImpl implements RelayCell {
|
||||
|
||||
public static RelayCell createFromCell(CircuitNode node, Cell cell) {
|
||||
if(cell.getCommand() != Cell.RELAY)
|
||||
throw new TorException("Attempted to create RelayCell from Cell type: "+ cell.getCommand());
|
||||
return new RelayCellImpl(node, cell.getCellBytes());
|
||||
}
|
||||
|
||||
private final int streamId;
|
||||
private final int relayCommand;
|
||||
private final CircuitNode circuitNode;
|
||||
private final boolean isOutgoing;
|
||||
|
||||
/*
|
||||
* The payload of each unencrypted RELAY cell consists of:
|
||||
* Relay command [1 byte]
|
||||
* 'Recognized' [2 bytes]
|
||||
* StreamID [2 bytes]
|
||||
* Digest [4 bytes]
|
||||
* Length [2 bytes]
|
||||
* Data [CELL_LEN-14 bytes]
|
||||
*/
|
||||
|
||||
public RelayCellImpl(CircuitNode node, int circuit, int stream, int relayCommand) {
|
||||
this(node, circuit, stream, relayCommand, false);
|
||||
}
|
||||
|
||||
public RelayCellImpl(CircuitNode node, int circuit, int stream, int relayCommand, boolean isRelayEarly) {
|
||||
super(circuit, (isRelayEarly) ? (Cell.RELAY_EARLY) : (Cell.RELAY));
|
||||
this.circuitNode = node;
|
||||
this.relayCommand = relayCommand;
|
||||
this.streamId = stream;
|
||||
this.isOutgoing = true;
|
||||
putByte(relayCommand); // Command
|
||||
putShort(0); // 'Recognized'
|
||||
putShort(stream); // Stream
|
||||
putInt(0); // Digest
|
||||
putShort(0); // Length
|
||||
}
|
||||
|
||||
private RelayCellImpl(CircuitNode node, byte[] rawCell) {
|
||||
super(rawCell);
|
||||
this.circuitNode = node;
|
||||
this.relayCommand = getByte();
|
||||
getShort();
|
||||
this.streamId = getShort();
|
||||
this.isOutgoing = false;
|
||||
getInt();
|
||||
int payloadLength = getShort();
|
||||
cellBuffer.mark(); // End of header
|
||||
if(RelayCell.HEADER_SIZE + payloadLength > rawCell.length)
|
||||
throw new TorException("Header length field exceeds total size of cell");
|
||||
cellBuffer.limit(RelayCell.HEADER_SIZE + payloadLength);
|
||||
}
|
||||
|
||||
public int getStreamId() {
|
||||
return streamId;
|
||||
}
|
||||
|
||||
public int getRelayCommand() {
|
||||
return relayCommand;
|
||||
}
|
||||
|
||||
public void setLength() {
|
||||
putShortAt(LENGTH_OFFSET, (short) (cellBytesConsumed() - HEADER_SIZE));
|
||||
}
|
||||
|
||||
public void setDigest(byte[] digest) {
|
||||
for(int i = 0; i < 4; i++)
|
||||
putByteAt(DIGEST_OFFSET + i, digest[i]);
|
||||
}
|
||||
|
||||
public ByteBuffer getPayloadBuffer() {
|
||||
final ByteBuffer dup = cellBuffer.duplicate();
|
||||
dup.reset();
|
||||
return dup.slice();
|
||||
}
|
||||
|
||||
public CircuitNode getCircuitNode() {
|
||||
return circuitNode;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if(isOutgoing)
|
||||
return "["+ commandToDescription(relayCommand) +" stream="+ streamId +" payload_len="+ (cellBytesConsumed() - HEADER_SIZE) +" dest="+ circuitNode +"]";
|
||||
else
|
||||
return "["+ commandToString() + " stream="+ streamId + " payload_len="+ cellBuffer.remaining() +" source="+ circuitNode + "]";
|
||||
}
|
||||
|
||||
public String commandToString() {
|
||||
if(relayCommand == RELAY_TRUNCATED) {
|
||||
final int code = getByteAt(HEADER_SIZE);
|
||||
return commandToDescription(relayCommand) + " ("+ CellImpl.errorToDescription(code) +")";
|
||||
} else if(relayCommand == RELAY_END) {
|
||||
final int code = getByteAt(HEADER_SIZE);
|
||||
return commandToDescription(relayCommand) +" ("+ reasonToDescription(code) +")";
|
||||
}
|
||||
else
|
||||
return commandToDescription(relayCommand);
|
||||
}
|
||||
|
||||
public static String reasonToDescription(int reasonCode) {
|
||||
switch(reasonCode) {
|
||||
case REASON_MISC:
|
||||
return "Unlisted reason";
|
||||
case REASON_RESOLVEFAILED:
|
||||
return "Couldn't look up hostname";
|
||||
case REASON_CONNECTREFUSED:
|
||||
return "Remote host refused connection";
|
||||
case REASON_EXITPOLICY:
|
||||
return "OR refuses to connect to host or port";
|
||||
case REASON_DESTROY:
|
||||
return "Circuit is being destroyed";
|
||||
case REASON_DONE:
|
||||
return "Anonymized TCP connection was closed";
|
||||
case REASON_TIMEOUT:
|
||||
return "Connection timed out, or OR timed out while connecting";
|
||||
case REASON_HIBERNATING:
|
||||
return "OR is temporarily hibernating";
|
||||
case REASON_INTERNAL:
|
||||
return "Internal error at the OR";
|
||||
case REASON_RESOURCELIMIT:
|
||||
return "OR has no resources to fulfill request";
|
||||
case REASON_CONNRESET:
|
||||
return "Connection was unexpectedly reset";
|
||||
case REASON_TORPROTOCOL:
|
||||
return "Tor protocol violation";
|
||||
case REASON_NOTDIRECTORY:
|
||||
return "Client sent RELAY_BEGIN_DIR to a non-directory server.";
|
||||
default:
|
||||
return "Reason code "+ reasonCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static String commandToDescription(int command) {
|
||||
switch(command) {
|
||||
case RELAY_BEGIN:
|
||||
return "RELAY_BEGIN";
|
||||
case RELAY_DATA:
|
||||
return "RELAY_DATA";
|
||||
case RELAY_END:
|
||||
return "RELAY_END";
|
||||
case RELAY_CONNECTED:
|
||||
return "RELAY_CONNECTED";
|
||||
case RELAY_SENDME:
|
||||
return "RELAY_SENDME";
|
||||
case RELAY_EXTEND:
|
||||
return "RELAY_EXTEND";
|
||||
case RELAY_EXTENDED:
|
||||
return "RELAY_EXTENDED";
|
||||
case RELAY_TRUNCATE:
|
||||
return "RELAY_TRUNCATE";
|
||||
case RELAY_TRUNCATED:
|
||||
return "RELAY_TRUNCATED";
|
||||
case RELAY_DROP:
|
||||
return "RELAY_DROP";
|
||||
case RELAY_RESOLVE:
|
||||
return "RELAY_RESOLVE";
|
||||
case RELAY_RESOLVED:
|
||||
return "RELAY_RESOLVED";
|
||||
case RELAY_BEGIN_DIR:
|
||||
return "RELAY_BEGIN_DIR";
|
||||
case RELAY_EXTEND2:
|
||||
return "RELAY_EXTEND2";
|
||||
case RELAY_EXTENDED2:
|
||||
return "RELAY_EXTENDED2";
|
||||
default:
|
||||
return "Relay command = "+ command;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package com.subgraph.orchid.circuits.guards;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import com.subgraph.orchid.BridgeRouter;
|
||||
import com.subgraph.orchid.Descriptor;
|
||||
import com.subgraph.orchid.RouterDescriptor;
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.IPv4Address;
|
||||
import com.subgraph.orchid.geoip.CountryCodeService;
|
||||
|
||||
public class BridgeRouterImpl implements BridgeRouter {
|
||||
private final IPv4Address address;
|
||||
private final int port;
|
||||
|
||||
private HexDigest identity;
|
||||
private Descriptor descriptor;
|
||||
|
||||
private volatile String cachedCountryCode;
|
||||
|
||||
BridgeRouterImpl(IPv4Address address, int port) {
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public IPv4Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public HexDigest getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public void setIdentity(HexDigest identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
public void setDescriptor(RouterDescriptor descriptor) {
|
||||
this.descriptor = descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((address == null) ? 0 : address.hashCode());
|
||||
result = prime * result + port;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
BridgeRouterImpl other = (BridgeRouterImpl) obj;
|
||||
if (address == null) {
|
||||
if (other.address != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!address.equals(other.address)) {
|
||||
return false;
|
||||
}
|
||||
if (port != other.port) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
public String getCountryCode() {
|
||||
String cc = cachedCountryCode;
|
||||
if(cc == null) {
|
||||
cc = CountryCodeService.getInstance().getCountryCodeForAddress(getAddress());
|
||||
cachedCountryCode = cc;
|
||||
}
|
||||
return cc;
|
||||
}
|
||||
|
||||
public int getOnionPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public int getDirectoryPort() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public TorPublicKey getIdentityKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public HexDigest getIdentityHash() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public boolean isDescriptorDownloadable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public Descriptor getCurrentDescriptor() {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public HexDigest getDescriptorDigest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public HexDigest getMicrodescriptorDigest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public TorPublicKey getOnionKey() {
|
||||
if(descriptor != null) {
|
||||
return descriptor.getOnionKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getNTorOnionKey() {
|
||||
if(descriptor != null) {
|
||||
return descriptor.getNTorOnionKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasBandwidth() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getEstimatedBandwidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getMeasuredBandwidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Set<String> getFamilyMembers() {
|
||||
if(descriptor != null) {
|
||||
return descriptor.getFamilyMembers();
|
||||
} else {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
public int getAverageBandwidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getBurstBandwidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getObservedBandwidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean isHibernating() {
|
||||
if(descriptor instanceof RouterDescriptor) {
|
||||
return ((RouterDescriptor)descriptor).isHibernating();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isBadExit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isPossibleGuard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isExit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isStable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isHSDirectory() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean exitPolicyAccepts(IPv4Address address, int port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean exitPolicyAccepts(int port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "[Bridge "+ address + ":"+ port + "]";
|
||||
}
|
||||
}
|
163
orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java
Normal file
163
orchid/src/com/subgraph/orchid/circuits/guards/Bridges.java
Normal file
@ -0,0 +1,163 @@
|
||||
package com.subgraph.orchid.circuits.guards;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.BridgeRouter;
|
||||
import com.subgraph.orchid.DirectoryDownloader;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.RouterDescriptor;
|
||||
import com.subgraph.orchid.TorConfig;
|
||||
import com.subgraph.orchid.config.TorConfigBridgeLine;
|
||||
import com.subgraph.orchid.crypto.TorRandom;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
|
||||
|
||||
public class Bridges {
|
||||
private static final Logger logger = Logger.getLogger(Bridges.class.getName());
|
||||
|
||||
private class DescriptorDownloader implements Runnable {
|
||||
|
||||
private final BridgeRouterImpl target;
|
||||
|
||||
DescriptorDownloader(BridgeRouterImpl target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
downloadDescriptor();
|
||||
} finally {
|
||||
decrementOutstandingTasks();
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadDescriptor() {
|
||||
logger.fine("Downloading descriptor for bridge: "+ target);
|
||||
try {
|
||||
final RouterDescriptor descriptor = directoryDownloader.downloadBridgeDescriptor(target);
|
||||
if(descriptor != null) {
|
||||
logger.fine("Descriptor received for bridge "+ target +". Adding to list of usable bridges");
|
||||
target.setDescriptor(descriptor);
|
||||
synchronized(lock) {
|
||||
bridgeRouters.add(target);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
} catch (DirectoryRequestFailedException e) {
|
||||
logger.warning("Failed to download descriptor for bridge: "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void decrementOutstandingTasks() {
|
||||
if(outstandingDownloadTasks.decrementAndGet() == 0) {
|
||||
logger.fine("Initial descriptor fetch complete");
|
||||
synchronized(lock) {
|
||||
bridgesInitialized = true;
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final TorConfig config;
|
||||
private final DirectoryDownloader directoryDownloader;
|
||||
|
||||
private final Set<BridgeRouterImpl> bridgeRouters;
|
||||
private final TorRandom random;
|
||||
private final Object lock;
|
||||
|
||||
/** Initialization started */
|
||||
private boolean bridgesInitializing;
|
||||
/** Initialization completed */
|
||||
private boolean bridgesInitialized;
|
||||
|
||||
private AtomicInteger outstandingDownloadTasks;
|
||||
|
||||
Bridges(TorConfig config, DirectoryDownloader directoryDownloader) {
|
||||
this.config = config;
|
||||
this.directoryDownloader = directoryDownloader;
|
||||
this.bridgeRouters = new HashSet<BridgeRouterImpl>();
|
||||
this.random = new TorRandom();
|
||||
this.lock = new Object();
|
||||
this.outstandingDownloadTasks = new AtomicInteger();
|
||||
}
|
||||
|
||||
BridgeRouter chooseRandomBridge(Set<Router> excluded) throws InterruptedException {
|
||||
|
||||
synchronized(lock) {
|
||||
if(!bridgesInitialized && !bridgesInitializing) {
|
||||
initializeBridges();
|
||||
}
|
||||
while(!bridgesInitialized && !hasCandidates(excluded)) {
|
||||
lock.wait();
|
||||
}
|
||||
final List<BridgeRouter> candidates = getCandidates(excluded);
|
||||
if(candidates.isEmpty()) {
|
||||
logger.warning("Bridges enabled but no usable bridges configured");
|
||||
return null;
|
||||
}
|
||||
return candidates.get(random.nextInt(candidates.size()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasCandidates(Set<Router> excluded) {
|
||||
return !(getCandidates(excluded).isEmpty());
|
||||
}
|
||||
|
||||
private List<BridgeRouter> getCandidates(Set<Router> excluded) {
|
||||
if(bridgeRouters.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<BridgeRouter> candidates = new ArrayList<BridgeRouter>(bridgeRouters.size());
|
||||
for(BridgeRouter br: bridgeRouters) {
|
||||
if(!excluded.contains(br)) {
|
||||
candidates.add(br);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private void initializeBridges() {
|
||||
logger.fine("Initializing bridges...");
|
||||
synchronized(lock) {
|
||||
if(bridgesInitializing || bridgesInitialized) {
|
||||
return;
|
||||
}
|
||||
if(directoryDownloader == null) {
|
||||
throw new IllegalStateException("Cannot download bridge descriptors because DirectoryDownload instance not initialized");
|
||||
}
|
||||
bridgesInitializing = true;
|
||||
startAllDownloadTasks();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Runnable> createDownloadTasks() {
|
||||
final List<Runnable> tasks = new ArrayList<Runnable>();
|
||||
for(TorConfigBridgeLine line: config.getBridges()) {
|
||||
tasks.add(new DescriptorDownloader(createBridgeFromLine(line)));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private void startAllDownloadTasks() {
|
||||
final List<Runnable> tasks = createDownloadTasks();
|
||||
outstandingDownloadTasks.set(tasks.size());
|
||||
for(Runnable r: tasks) {
|
||||
final Thread thread = new Thread(r);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
private BridgeRouterImpl createBridgeFromLine(TorConfigBridgeLine line) {
|
||||
final BridgeRouterImpl bridge = new BridgeRouterImpl(line.getAddress(), line.getPort());
|
||||
if(line.getFingerprint() != null) {
|
||||
bridge.setIdentity(line.getFingerprint());
|
||||
}
|
||||
return bridge;
|
||||
}
|
||||
}
|
305
orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java
Normal file
305
orchid/src/com/subgraph/orchid/circuits/guards/EntryGuards.java
Normal file
@ -0,0 +1,305 @@
|
||||
package com.subgraph.orchid.circuits.guards;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.ConnectionCache;
|
||||
import com.subgraph.orchid.Directory;
|
||||
import com.subgraph.orchid.DirectoryDownloader;
|
||||
import com.subgraph.orchid.GuardEntry;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.TorConfig;
|
||||
import com.subgraph.orchid.circuits.path.CircuitNodeChooser;
|
||||
import com.subgraph.orchid.circuits.path.CircuitNodeChooser.WeightRule;
|
||||
import com.subgraph.orchid.circuits.path.RouterFilter;
|
||||
import com.subgraph.orchid.crypto.TorRandom;
|
||||
|
||||
public class EntryGuards {
|
||||
private final static Logger logger = Logger.getLogger(EntryGuards.class.getName());
|
||||
|
||||
private final static int MIN_USABLE_GUARDS = 2;
|
||||
private final static int NUM_ENTRY_GUARDS = 3;
|
||||
|
||||
private final TorConfig config;
|
||||
private final TorRandom random;
|
||||
private final CircuitNodeChooser nodeChooser;
|
||||
private final ConnectionCache connectionCache;
|
||||
private final Directory directory;
|
||||
private final Set<GuardEntry> pendingProbes;
|
||||
|
||||
private final Bridges bridges;
|
||||
private final Object lock;
|
||||
private final Executor executor;
|
||||
|
||||
public EntryGuards(TorConfig config, ConnectionCache connectionCache, DirectoryDownloader directoryDownloader, Directory directory) {
|
||||
this.config = config;
|
||||
this.random = new TorRandom();
|
||||
this.nodeChooser = new CircuitNodeChooser(config, directory);
|
||||
this.connectionCache = connectionCache;
|
||||
this.directory = directory;
|
||||
this.pendingProbes = new HashSet<GuardEntry>();
|
||||
this.bridges = new Bridges(config, directoryDownloader);
|
||||
this.lock = new Object();
|
||||
this.executor = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
public boolean isUsingBridges() {
|
||||
return config.getUseBridges();
|
||||
}
|
||||
|
||||
public Router chooseRandomGuard(Set<Router> excluded) throws InterruptedException {
|
||||
if(config.getUseBridges()) {
|
||||
return bridges.chooseRandomBridge(excluded);
|
||||
}
|
||||
|
||||
/*
|
||||
* path-spec 5.
|
||||
*
|
||||
* When choosing the first hop of a circuit, Tor chooses at random from among the first
|
||||
* NumEntryGuards (default 3) usable guards on the list. If there are not at least 2
|
||||
* usable guards on the list, Tor adds routers until there are, or until there are no
|
||||
* more usable routers to add.
|
||||
*/
|
||||
|
||||
final List<Router> usableGuards = getMinimumUsableGuards(excluded, MIN_USABLE_GUARDS);
|
||||
final int n = Math.min(usableGuards.size(), NUM_ENTRY_GUARDS);
|
||||
return usableGuards.get(random.nextInt(n));
|
||||
}
|
||||
|
||||
private List<Router> getMinimumUsableGuards(Set<Router> excluded, int minSize) throws InterruptedException {
|
||||
synchronized(lock) {
|
||||
testStatusOfAllGuards();
|
||||
while(true) {
|
||||
List<Router> usableGuards = getUsableGuardRouters(excluded);
|
||||
if(usableGuards.size() >= minSize) {
|
||||
return usableGuards;
|
||||
} else {
|
||||
maybeChooseNew(usableGuards.size(), minSize, getExcludedForChooseNew(excluded, usableGuards));
|
||||
}
|
||||
lock.wait(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void probeConnectionSucceeded(GuardEntry entry) {
|
||||
synchronized (lock) {
|
||||
pendingProbes.remove(entry);
|
||||
if(entry.isAdded()) {
|
||||
retestProbeSucceeded(entry);
|
||||
} else {
|
||||
initialProbeSucceeded(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void probeConnectionFailed(GuardEntry entry) {
|
||||
synchronized (lock) {
|
||||
pendingProbes.remove(entry);
|
||||
if(entry.isAdded()) {
|
||||
retestProbeFailed(entry);
|
||||
}
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/* all methods below called holding 'lock' */
|
||||
|
||||
private void retestProbeSucceeded(GuardEntry entry) {
|
||||
entry.clearDownSince();
|
||||
}
|
||||
|
||||
private void initialProbeSucceeded(GuardEntry entry) {
|
||||
logger.fine("Probe connection to "+ entry.getRouterForEntry() + " succeeded. Adding it as a new entry guard.");
|
||||
directory.addGuardEntry(entry);
|
||||
retestAllUnreachable();
|
||||
}
|
||||
|
||||
private void retestProbeFailed(GuardEntry entry) {
|
||||
entry.markAsDown();
|
||||
}
|
||||
|
||||
/*
|
||||
* path-spec 5.
|
||||
*
|
||||
* Additionally, Tor retries unreachable guards the first time it adds a new
|
||||
* guard to the list, since it is possible that the old guards were only marked
|
||||
* as unreachable because the network was unreachable or down.
|
||||
|
||||
*/
|
||||
private void retestAllUnreachable() {
|
||||
for(GuardEntry e: directory.getGuardEntries()) {
|
||||
if(e.getDownSince() != null) {
|
||||
launchEntryProbe(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void testStatusOfAllGuards() {
|
||||
for(GuardEntry entry: directory.getGuardEntries()) {
|
||||
if(isPermanentlyUnlisted(entry) || isExpired(entry)) {
|
||||
directory.removeGuardEntry(entry);
|
||||
} else if(needsUnreachableTest(entry)) {
|
||||
launchEntryProbe(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Router> getUsableGuardRouters(Set<Router> excluded) {
|
||||
List<Router> usableRouters = new ArrayList<Router>();
|
||||
for(GuardEntry entry: directory.getGuardEntries()) {
|
||||
addRouterIfUsableAndNotExcluded(entry, excluded, usableRouters);
|
||||
}
|
||||
return usableRouters;
|
||||
}
|
||||
|
||||
private void addRouterIfUsableAndNotExcluded(GuardEntry entry, Set<Router> excluded, List<Router> routers) {
|
||||
if(entry.testCurrentlyUsable() && entry.getDownSince() == null) {
|
||||
final Router r = entry.getRouterForEntry();
|
||||
if(r != null && !excluded.contains(r)) {
|
||||
routers.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Router> getExcludedForChooseNew(Set<Router> excluded, List<Router> usable) {
|
||||
final Set<Router> set = new HashSet<Router>();
|
||||
set.addAll(excluded);
|
||||
set.addAll(usable);
|
||||
addPendingInitialConnections(set);
|
||||
return set;
|
||||
}
|
||||
|
||||
private void addPendingInitialConnections(Set<Router> routerSet) {
|
||||
for(GuardEntry entry: pendingProbes) {
|
||||
if(!entry.isAdded()) {
|
||||
Router r = entry.getRouterForEntry();
|
||||
if(r != null) {
|
||||
routerSet.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeChooseNew(int usableSize, int minSize, Set<Router> excluded) {
|
||||
int sz = usableSize + countPendingInitialProbes();
|
||||
while(sz < minSize) {
|
||||
Router newGuard = chooseNewGuard(excluded);
|
||||
if(newGuard == null) {
|
||||
logger.warning("Need to add entry guards but no suitable guard routers are available");
|
||||
return;
|
||||
}
|
||||
logger.fine("Testing "+ newGuard + " as a new guard since we only have "+ usableSize + " usable guards");
|
||||
final GuardEntry entry = directory.createGuardEntryFor(newGuard);
|
||||
launchEntryProbe(entry);
|
||||
sz += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private int countPendingInitialProbes() {
|
||||
int count = 0;
|
||||
for(GuardEntry entry: pendingProbes) {
|
||||
if(!entry.isAdded()) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private Router chooseNewGuard(final Set<Router> excluded) {
|
||||
return nodeChooser.chooseRandomNode(WeightRule.WEIGHT_FOR_GUARD, new RouterFilter() {
|
||||
public boolean filter(Router router) {
|
||||
return router.isValid() && router.isPossibleGuard() && router.isRunning() && !excluded.contains(router);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void launchEntryProbe(GuardEntry entry) {
|
||||
if(!entry.testCurrentlyUsable() || pendingProbes.contains(entry)) {
|
||||
return;
|
||||
}
|
||||
pendingProbes.add(entry);
|
||||
executor.execute(new GuardProbeTask(connectionCache, this, entry));
|
||||
}
|
||||
|
||||
/*
|
||||
* path-spec 5.
|
||||
*
|
||||
* If the guard is excluded because of its status in the networkstatuses for
|
||||
* over 30 days, Tor removes it from the list entirely, preserving order.
|
||||
*/
|
||||
private boolean isPermanentlyUnlisted(GuardEntry entry) {
|
||||
final Date unlistedSince = entry.getUnlistedSince();
|
||||
if(unlistedSince == null || pendingProbes.contains(entry)) {
|
||||
return false;
|
||||
}
|
||||
final Date now = new Date();
|
||||
final long unlistedTime = now.getTime() - unlistedSince.getTime();
|
||||
return unlistedTime > THIRTY_DAYS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expire guards after 60 days since creation time.
|
||||
*/
|
||||
private boolean isExpired(GuardEntry entry) {
|
||||
final Date createdAt = entry.getCreatedTime();
|
||||
final Date now = new Date();
|
||||
final long createdAgo = now.getTime() - createdAt.getTime();
|
||||
return createdAgo > SIXTY_DAYS;
|
||||
}
|
||||
|
||||
private boolean needsUnreachableTest(GuardEntry entry) {
|
||||
final Date downSince = entry.getDownSince();
|
||||
if(downSince == null || !entry.testCurrentlyUsable()) {
|
||||
return false;
|
||||
}
|
||||
final Date now = new Date();
|
||||
final Date lastConnect = entry.getLastConnectAttempt();
|
||||
final long timeDown = now.getTime() - downSince.getTime();
|
||||
final long timeSinceLastRetest = (lastConnect == null) ? timeDown : (now.getTime() - lastConnect.getTime());
|
||||
|
||||
return timeSinceLastRetest > getRetestInterval(timeDown);
|
||||
}
|
||||
|
||||
private final static long ONE_HOUR = hoursToMs(1);
|
||||
private final static long FOUR_HOURS = hoursToMs(4);
|
||||
private final static long SIX_HOURS = hoursToMs(6);
|
||||
private final static long EIGHTEEN_HOURS = hoursToMs(18);
|
||||
private final static long THIRTYSIX_HOURS = hoursToMs(36);
|
||||
private final static long THREE_DAYS = daysToMs(3);
|
||||
private final static long SEVEN_DAYS = daysToMs(7);
|
||||
private final static long THIRTY_DAYS = daysToMs(30);
|
||||
private final static long SIXTY_DAYS = daysToMs(60);
|
||||
|
||||
private static long hoursToMs(long n) {
|
||||
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.HOURS);
|
||||
}
|
||||
private static long daysToMs(long n) {
|
||||
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.DAYS);
|
||||
}
|
||||
/*
|
||||
* path-spec 5.
|
||||
*
|
||||
* If Tor fails to connect to an otherwise usable guard, it retries
|
||||
* periodically: every hour for six hours, every 4 hours for 3 days, every
|
||||
* 18 hours for a week, and every 36 hours thereafter.
|
||||
*/
|
||||
|
||||
private long getRetestInterval(long timeDown) {
|
||||
if(timeDown < SIX_HOURS) {
|
||||
return ONE_HOUR;
|
||||
} else if(timeDown < THREE_DAYS) {
|
||||
return FOUR_HOURS;
|
||||
} else if(timeDown < SEVEN_DAYS) {
|
||||
return EIGHTEEN_HOURS;
|
||||
} else {
|
||||
return THIRTYSIX_HOURS;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.subgraph.orchid.circuits.guards;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.ConnectionCache;
|
||||
import com.subgraph.orchid.ConnectionIOException;
|
||||
import com.subgraph.orchid.GuardEntry;
|
||||
import com.subgraph.orchid.Router;
|
||||
|
||||
public class GuardProbeTask implements Runnable{
|
||||
private final static Logger logger = Logger.getLogger(GuardProbeTask.class.getName());
|
||||
private final ConnectionCache connectionCache;
|
||||
private final EntryGuards entryGuards;
|
||||
private final GuardEntry entry;
|
||||
|
||||
public GuardProbeTask(ConnectionCache connectionCache, EntryGuards entryGuards, GuardEntry entry) {
|
||||
this.connectionCache = connectionCache;
|
||||
this.entryGuards = entryGuards;
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
final Router router = entry.getRouterForEntry();
|
||||
if(router == null) {
|
||||
entryGuards.probeConnectionFailed(entry);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
connectionCache.getConnectionTo(router, false);
|
||||
entryGuards.probeConnectionSucceeded(entry);
|
||||
return;
|
||||
} catch (ConnectionIOException e) {
|
||||
logger.fine("IO exception probing entry guard "+ router + " : "+ e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(Exception e) {
|
||||
logger.log(Level.WARNING, "Unexpected exception probing entry guard: "+ e, e);
|
||||
}
|
||||
entryGuards.probeConnectionFailed(entry);
|
||||
}
|
||||
}
|
120
orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java
Normal file
120
orchid/src/com/subgraph/orchid/circuits/hs/HSAuthentication.java
Normal file
@ -0,0 +1,120 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.subgraph.orchid.TorParsingException;
|
||||
import com.subgraph.orchid.circuits.hs.HSDescriptorCookie.CookieType;
|
||||
import com.subgraph.orchid.crypto.TorMessageDigest;
|
||||
import com.subgraph.orchid.crypto.TorStreamCipher;
|
||||
|
||||
public class HSAuthentication {
|
||||
private final static int BASIC_ID_LENGTH = 4;
|
||||
private final HSDescriptorCookie cookie;
|
||||
|
||||
public HSAuthentication(HSDescriptorCookie cookie) {
|
||||
this.cookie = cookie;
|
||||
}
|
||||
|
||||
public byte[] decryptIntroductionPoints(byte[] content) throws HSAuthenticationException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(content);
|
||||
final int firstByte = buffer.get() & 0xFF;
|
||||
if(firstByte == 1) {
|
||||
return decryptIntroductionPointsWithBasicAuth(buffer);
|
||||
} else if(firstByte == 2) {
|
||||
return decryptIntroductionPointsWithStealthAuth(buffer);
|
||||
} else {
|
||||
throw new HSAuthenticationException("Introduction points section begins with unrecognized byte ("+ firstByte +")");
|
||||
}
|
||||
}
|
||||
|
||||
private static class BasicAuthEntry {
|
||||
final byte[] id;
|
||||
final byte[] skey;
|
||||
BasicAuthEntry(byte[] id, byte[] skey) {
|
||||
this.id = id;
|
||||
this.skey = skey;
|
||||
}
|
||||
}
|
||||
|
||||
private BasicAuthEntry createEntry(ByteBuffer bb) {
|
||||
final byte[] id = new byte[BASIC_ID_LENGTH];
|
||||
final byte[] skey = new byte[TorStreamCipher.KEY_LEN];
|
||||
bb.get(id);
|
||||
bb.get(skey);
|
||||
return new BasicAuthEntry(id, skey);
|
||||
}
|
||||
|
||||
private byte[] decryptIntroductionPointsWithBasicAuth(ByteBuffer buffer) throws HSAuthenticationException {
|
||||
if(cookie == null || cookie.getType() != CookieType.COOKIE_BASIC) {
|
||||
throw new TorParsingException("Introduction points encrypted with 'basic' authentication and no cookie available to decrypt");
|
||||
}
|
||||
|
||||
final List<BasicAuthEntry> entries = readBasicEntries(buffer);
|
||||
final byte[] iv = readAuthIV(buffer);
|
||||
final byte[] id = generateAuthId(iv);
|
||||
final byte[] k = findKeyInAuthEntries(entries, id);
|
||||
|
||||
return decryptRemaining(buffer, k, iv);
|
||||
}
|
||||
|
||||
private List<BasicAuthEntry> readBasicEntries(ByteBuffer b) {
|
||||
final int blockCount = b.get() & 0xFF;
|
||||
final int entryCount = blockCount * 16;
|
||||
final List<BasicAuthEntry> entries = new ArrayList<BasicAuthEntry>(entryCount);
|
||||
for(int i = 0; i < entryCount; i++) {
|
||||
entries.add( createEntry(b) );
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
private byte[] readAuthIV(ByteBuffer b) {
|
||||
final byte[] iv = new byte[16];
|
||||
b.get(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
private byte[] generateAuthId(byte[] iv) {
|
||||
final TorMessageDigest md = new TorMessageDigest();
|
||||
md.update(cookie.getValue());
|
||||
md.update(iv);
|
||||
final byte[] digest = md.getDigestBytes();
|
||||
final byte[] id = new byte[BASIC_ID_LENGTH];
|
||||
System.arraycopy(digest, 0, id, 0, BASIC_ID_LENGTH);
|
||||
return id;
|
||||
}
|
||||
|
||||
private byte[] findKeyInAuthEntries(List<BasicAuthEntry> entries, byte[] id) throws HSAuthenticationException {
|
||||
for(BasicAuthEntry e: entries) {
|
||||
if(Arrays.equals(id, e.id)) {
|
||||
return decryptAuthEntry(e);
|
||||
}
|
||||
}
|
||||
throw new HSAuthenticationException("Could not find matching cookie id for basic authentication");
|
||||
}
|
||||
|
||||
private byte[] decryptAuthEntry(BasicAuthEntry entry) throws HSAuthenticationException {
|
||||
TorStreamCipher cipher = TorStreamCipher.createFromKeyBytes(cookie.getValue());
|
||||
cipher.encrypt(entry.skey);
|
||||
return entry.skey;
|
||||
}
|
||||
|
||||
private byte[] decryptRemaining(ByteBuffer buffer, byte[] key, byte[] iv) {
|
||||
TorStreamCipher streamCipher = TorStreamCipher.createFromKeyBytesWithIV(key, iv);
|
||||
final byte[] remaining = new byte[buffer.remaining()];
|
||||
buffer.get(remaining);
|
||||
streamCipher.encrypt(remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
private byte[] decryptIntroductionPointsWithStealthAuth(ByteBuffer buffer) {
|
||||
if(cookie == null || cookie.getType() != CookieType.COOKIE_STEALTH) {
|
||||
throw new TorParsingException("Introduction points encrypted with 'stealth' authentication and no cookie available to descrypt");
|
||||
}
|
||||
final byte[] iv = readAuthIV(buffer);
|
||||
return decryptRemaining(buffer, cookie.getValue(), iv);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
public class HSAuthenticationException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
HSAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
HSAuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
109
orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java
Normal file
109
orchid/src/com/subgraph/orchid/circuits/hs/HSDescriptor.java
Normal file
@ -0,0 +1,109 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.subgraph.orchid.crypto.TorPublicKey;
|
||||
import com.subgraph.orchid.crypto.TorRandom;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
import com.subgraph.orchid.data.Timestamp;
|
||||
|
||||
public class HSDescriptor {
|
||||
private final static long MS_24_HOURS = (24 * 60 * 60 * 1000);
|
||||
private final HiddenService hiddenService;
|
||||
private HexDigest descriptorId;
|
||||
private Timestamp publicationTime;
|
||||
private HexDigest secretIdPart;
|
||||
private TorPublicKey permanentKey;
|
||||
private int[] protocolVersions;
|
||||
private List<IntroductionPoint> introductionPoints;
|
||||
|
||||
public HSDescriptor(HiddenService hiddenService) {
|
||||
this.hiddenService = hiddenService;
|
||||
introductionPoints = new ArrayList<IntroductionPoint>();
|
||||
}
|
||||
|
||||
HiddenService getHiddenService() {
|
||||
return hiddenService;
|
||||
}
|
||||
|
||||
void setPublicationTime(Timestamp ts) {
|
||||
this.publicationTime = ts;
|
||||
}
|
||||
|
||||
void setSecretIdPart(HexDigest secretIdPart) {
|
||||
this.secretIdPart = secretIdPart;
|
||||
}
|
||||
|
||||
void setDescriptorId(HexDigest descriptorId) {
|
||||
this.descriptorId = descriptorId;
|
||||
}
|
||||
|
||||
void setPermanentKey(TorPublicKey permanentKey) {
|
||||
this.permanentKey = permanentKey;
|
||||
}
|
||||
|
||||
void setProtocolVersions(int[] protocolVersions) {
|
||||
this.protocolVersions = protocolVersions;
|
||||
}
|
||||
|
||||
void addIntroductionPoint(IntroductionPoint ip) {
|
||||
introductionPoints.add(ip);
|
||||
}
|
||||
|
||||
HexDigest getDescriptorId() {
|
||||
return descriptorId;
|
||||
}
|
||||
|
||||
int getVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
TorPublicKey getPermanentKey() {
|
||||
return permanentKey;
|
||||
}
|
||||
|
||||
HexDigest getSecretIdPart() {
|
||||
return secretIdPart;
|
||||
}
|
||||
|
||||
Timestamp getPublicationTime() {
|
||||
return publicationTime;
|
||||
}
|
||||
|
||||
int[] getProtocolVersions() {
|
||||
return protocolVersions;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
final long now = System.currentTimeMillis();
|
||||
final long then = publicationTime.getTime();
|
||||
return (now - then) > MS_24_HOURS;
|
||||
}
|
||||
|
||||
List<IntroductionPoint> getIntroductionPoints() {
|
||||
return new ArrayList<IntroductionPoint>(introductionPoints);
|
||||
}
|
||||
|
||||
List<IntroductionPoint> getShuffledIntroductionPoints() {
|
||||
return shuffle(getIntroductionPoints());
|
||||
}
|
||||
|
||||
private List<IntroductionPoint> shuffle(List<IntroductionPoint> list) {
|
||||
final TorRandom r = new TorRandom();
|
||||
final int sz = list.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
swap(list, i, r.nextInt(sz));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void swap(List<IntroductionPoint> list, int a, int b) {
|
||||
if(a == b) {
|
||||
return;
|
||||
}
|
||||
final IntroductionPoint tmp = list.get(a);
|
||||
list.set(a, list.get(b));
|
||||
list.set(b, tmp);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
public class HSDescriptorCookie {
|
||||
|
||||
public enum CookieType { COOKIE_BASIC, COOKIE_STEALTH };
|
||||
|
||||
private final CookieType type;
|
||||
private final byte[] value;
|
||||
|
||||
public HSDescriptorCookie(CookieType type, byte[] value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getAuthTypeByte() {
|
||||
switch(type) {
|
||||
case COOKIE_BASIC:
|
||||
return 1;
|
||||
case COOKIE_STEALTH:
|
||||
return 2;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public CookieType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.data.HexDigest;
|
||||
|
||||
public class HSDescriptorDirectory {
|
||||
|
||||
private final HexDigest descriptorId;
|
||||
private final Router directory;
|
||||
|
||||
HSDescriptorDirectory(HexDigest descriptorId, Router directory) {
|
||||
this.descriptorId = descriptorId;
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
Router getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
HexDigest getDescriptorId() {
|
||||
return descriptorId;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return descriptorId + " : " + directory;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.subgraph.orchid.DirectoryCircuit;
|
||||
import com.subgraph.orchid.InternalCircuit;
|
||||
import com.subgraph.orchid.OpenFailedException;
|
||||
import com.subgraph.orchid.Router;
|
||||
import com.subgraph.orchid.Stream;
|
||||
import com.subgraph.orchid.StreamConnectFailedException;
|
||||
import com.subgraph.orchid.TorException;
|
||||
import com.subgraph.orchid.circuits.CircuitManagerImpl;
|
||||
import com.subgraph.orchid.directory.DocumentFieldParserImpl;
|
||||
import com.subgraph.orchid.directory.downloader.DirectoryRequestFailedException;
|
||||
import com.subgraph.orchid.directory.downloader.HttpConnection;
|
||||
import com.subgraph.orchid.directory.parsing.DocumentParsingResultHandler;
|
||||
|
||||
public class HSDescriptorDownloader {
|
||||
private final static Logger logger = Logger.getLogger(HSDescriptorDirectory.class.getName());
|
||||
|
||||
private final HiddenService hiddenService;
|
||||
private final CircuitManagerImpl circuitManager;
|
||||
private final List<HSDescriptorDirectory> directories;
|
||||
|
||||
public HSDescriptorDownloader(HiddenService hiddenService, CircuitManagerImpl circuitManager, List<HSDescriptorDirectory> directories) {
|
||||
this.hiddenService = hiddenService;
|
||||
this.circuitManager = circuitManager;
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
|
||||
public HSDescriptor downloadDescriptor() {
|
||||
for(HSDescriptorDirectory d: directories) {
|
||||
HSDescriptor descriptor = downloadDescriptorFrom(d);
|
||||
if(descriptor != null) {
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
// All directories failed
|
||||
return null;
|
||||
}
|
||||
|
||||
private HSDescriptor downloadDescriptorFrom(HSDescriptorDirectory dd) {
|
||||
logger.fine("Downloading descriptor from "+ dd.getDirectory());
|
||||
|
||||
Stream stream = null;
|
||||
try {
|
||||
stream = openHSDirectoryStream(dd.getDirectory());
|
||||
HttpConnection http = new HttpConnection(stream);
|
||||
http.sendGetRequest("/tor/rendezvous2/"+ dd.getDescriptorId().toBase32());
|
||||
http.readResponse();
|
||||
if(http.getStatusCode() == 200) {
|
||||
return readDocument(dd, http.getMessageBody());
|
||||
} else {
|
||||
logger.fine("HS descriptor download for "+ hiddenService.getOnionAddressForLogging() + " failed with status "+ http.getStatusCode());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
} catch (TimeoutException e) {
|
||||
logger.fine("Timeout downloading HS descriptor from "+ dd.getDirectory());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
logger.info("IOException downloading HS descriptor from "+ dd.getDirectory() +" : "+ e);
|
||||
return null;
|
||||
} catch (OpenFailedException e) {
|
||||
logger.info("Failed to open stream to HS directory "+ dd.getDirectory() +" : "+ e.getMessage());
|
||||
return null;
|
||||
} catch (DirectoryRequestFailedException e) {
|
||||
logger.info("Directory request to HS directory "+ dd.getDirectory() + " failed "+ e.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
if(stream != null) {
|
||||
stream.close();
|
||||
stream.getCircuit().markForClose();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private Stream openHSDirectoryStream(Router directory) throws TimeoutException, InterruptedException, OpenFailedException {
|
||||
|
||||
final InternalCircuit circuit = circuitManager.getCleanInternalCircuit();
|
||||
|
||||
try {
|
||||
final DirectoryCircuit dc = circuit.cannibalizeToDirectory(directory);
|
||||
return dc.openDirectoryStream(10000, true);
|
||||
} catch (StreamConnectFailedException e) {
|
||||
circuit.markForClose();
|
||||
throw new OpenFailedException("Failed to open directory stream");
|
||||
} catch (TorException e) {
|
||||
circuit.markForClose();
|
||||
throw new OpenFailedException("Failed to extend circuit to HS directory: "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private HSDescriptor readDocument(HSDescriptorDirectory dd, ByteBuffer body) {
|
||||
DocumentFieldParserImpl fieldParser = new DocumentFieldParserImpl(body);
|
||||
HSDescriptorParser parser = new HSDescriptorParser(hiddenService, fieldParser, hiddenService.getAuthenticationCookie());
|
||||
DescriptorParseResult result = new DescriptorParseResult(dd);
|
||||
parser.parse(result);
|
||||
return result.getDescriptor();
|
||||
}
|
||||
|
||||
private static class DescriptorParseResult implements DocumentParsingResultHandler<HSDescriptor> {
|
||||
HSDescriptorDirectory dd;
|
||||
HSDescriptor descriptor;
|
||||
|
||||
public DescriptorParseResult(HSDescriptorDirectory dd) {
|
||||
this.dd = dd;
|
||||
}
|
||||
|
||||
HSDescriptor getDescriptor() {
|
||||
return descriptor;
|
||||
}
|
||||
public void documentParsed(HSDescriptor document) {
|
||||
this.descriptor = document;
|
||||
}
|
||||
|
||||
public void documentInvalid(HSDescriptor document, String message) {
|
||||
logger.info("Invalid HS descriptor document received from "+ dd.getDirectory() + " for descriptor "+ dd.getDescriptorId());
|
||||
}
|
||||
|
||||
public void parsingError(String message) {
|
||||
logger.info("Failed to parse HS descriptor document received from "+ dd.getDirectory() + " for descriptor "+ dd.getDescriptorId() + " : " + message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.subgraph.orchid.circuits.hs;
|
||||
|
||||
public enum HSDescriptorKeyword {
|
||||
RENDEZVOUS_SERVICE_DESCRIPTOR("rendezvous-service-descriptor", 1),
|
||||
VERSION("version", 1),
|
||||
PERMANENT_KEY("permanent-key", 0),
|
||||
SECRET_ID_PART("secret-id-part", 1),
|
||||
PUBLICATION_TIME("publication-time", 2),
|
||||
PROTOCOL_VERSIONS("protocol-versions", 2),
|
||||
INTRODUCTION_POINTS("introduction-points", 0),
|
||||
SIGNATURE("signature", 0),
|
||||
UNKNOWN_KEYWORD("KEYWORD NOT FOUND", 0);
|
||||
|
||||
private final String keyword;
|
||||
private final int argumentCount;
|
||||
|
||||
HSDescriptorKeyword(String keyword, int argumentCount) {
|
||||
this.keyword = keyword;
|
||||
this.argumentCount = argumentCount;
|
||||
}
|
||||
|
||||
String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
int getArgumentCount() {
|
||||
return argumentCount;
|
||||
}
|
||||
|
||||
static HSDescriptorKeyword findKeyword(String keyword) {
|
||||
for(HSDescriptorKeyword k: values()) {
|
||||
if(k.getKeyword().equals(keyword)) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return UNKNOWN_KEYWORD;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user