3
0
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:
Devrandom 2014-03-12 22:01:47 -07:00 committed by Mike Hearn
parent c5e82e6bc5
commit 99448b730a
260 changed files with 32486 additions and 3 deletions

View File

@ -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>

View File

@ -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()];

View File

@ -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
View File

@ -0,0 +1,5 @@
bin/
orchid-*.jar
orchid-*.zip
build-revision
lib/xmlrpc-*

25
orchid/LICENSE Normal file
View 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
View File

116
orchid/build.xml Normal file
View 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

Binary file not shown.

3
orchid/data/README Normal file
View File

@ -0,0 +1,3 @@
GeoIP.dat GeoLite Country database downloaded September, 2013
http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz

View 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.

View 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.

File diff suppressed because it is too large Load Diff

2132
orchid/doc/spec/dir-spec.txt Normal file

File diff suppressed because it is too large Load Diff

View 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]

View 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.

View 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

View 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.

View 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

View File

@ -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());
}
}

View File

@ -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
View 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>

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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;
}

View 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);
}

View 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);
}

View 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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View 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();
}

View 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);
}

View 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);
}

View 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;
}

View 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;
}

View 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);
}

View 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();
}

View File

@ -0,0 +1,9 @@
package com.subgraph.orchid;
import java.nio.ByteBuffer;
public interface Document {
ByteBuffer getRawDocumentBytes();
String getRawDocumentData();
boolean isValidDocument();
}

View 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);
}

View 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();
}

View 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;
}

View 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);
}

View 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();
}

View 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);
}
}

View 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);
}

View 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();
}
}
}

View 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);
}

View 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();
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface RouterMicrodescriptor extends Descriptor {
}

View 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();
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface SocksPortListener {
void addListeningPort(int port);
void stop();
}

View 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();
}

View File

@ -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;
}
}
}

View 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);
}
}

View 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");
}
}
}

View 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 "";
}
}

View 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);
}
}

View File

@ -0,0 +1,6 @@
package com.subgraph.orchid;
public interface TorInitializationListener {
void initializationProgress(String message, int percent);
void initializationCompleted();
}

View 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;
}

View 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();
}

View 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);
}
}
}

View File

@ -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);
}
}
}

View 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);
}
};
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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();
}
}

View File

@ -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);
}
}

View 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();
}
}
}

View File

@ -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();
}
}

View 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;
}
}
}

View File

@ -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";
}
}

View 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";
}
}

View 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)";
}
}
}

View 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);
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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("]");
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@ -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 + "]";
}
}

View 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;
}
}

View 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;
}
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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