[Pkg-privacy-commits] [txtorcon] 01/08: New upstream version 0.19.3

Iain R. Learmonth irl at moszumanska.debian.org
Sat Sep 16 03:54:54 UTC 2017


This is an automated email from the git hooks/post-receive script.

irl pushed a commit to branch master
in repository txtorcon.

commit 2bcb63b56180e2c87a841a512dcd767405dda9e4
Author: Iain R. Learmonth <irl at debian.org>
Date:   Mon Sep 11 11:56:24 2017 +0100

    New upstream version 0.19.3
---
 INSTALL                                            |   10 +-
 Makefile                                           |   56 +-
 PKG-INFO                                           |  313 ++----
 README.rst                                         |  309 ++----
 dev-requirements.txt                               |    3 +-
 docs/README.rst                                    |  232 ----
 docs/_themes/alabaster/static/alabaster.css_t      |   23 +
 docs/conf.py                                       |   25 +-
 docs/examples.rst                                  |  210 ++--
 docs/guide.rst                                     |  754 +++++++++++++
 docs/hacking.rst                                   |   77 ++
 docs/howtos.rst                                    |  138 ---
 docs/index.rst                                     |  191 +---
 docs/installing.rst                                |  213 ++++
 docs/introduction.rst                              |  122 +-
 docs/release-checklist.rst                         |   22 +-
 docs/releases.rst                                  |  112 +-
 docs/tutorial.rst                                  |  262 -----
 docs/txtorcon-config.rst                           |    4 +-
 docs/txtorcon-controller.rst                       |   24 +
 docs/txtorcon-endpoints.rst                        |   12 +-
 docs/txtorcon-launching.rst                        |   27 -
 docs/txtorcon-protocol.rst                         |   21 +-
 docs/txtorcon-socks.rst                            |   72 ++
 docs/txtorcon-state.rst                            |   14 +-
 docs/txtorcon.rst                                  |   17 +-
 docs/walkthrough.rst                               |  334 ------
 examples/add_hiddenservice_to_system_tor.py        |   63 --
 examples/add_hiddenservice_to_system_tor.py.orig   |    0
 examples/attach_streams_by_country.py              |  223 ----
 examples/attach_streams_by_country.py.orig         |    0
 examples/circuit_failure_rates.py                  |  219 ----
 examples/circuit_failure_rates.py.orig             |    0
 examples/circuit_for_next_stream.py                |  136 ---
 examples/circuit_for_next_stream.py.orig           |    0
 examples/connect.py                                |   24 +
 examples/connect.py.orig                           |   24 +
 examples/disallow_streams_by_port.py               |   61 +-
 examples/dns_lookups.py                            |   20 +
 examples/dns_lookups.py.orig                       |   20 +
 examples/dump_config.py                            |   63 --
 examples/dump_config.py.orig                       |    0
 examples/ephemeral_endpoint.py                     |   63 --
 examples/gui-boom.py                               |   39 -
 examples/gui-cairo.py                              |   94 --
 examples/gui-map.py                                |   66 --
 examples/gui.py                                    |   83 --
 examples/gui2.py                                   |   52 -
 examples/hello_darkweb.py                          |   24 -
 examples/hello_darkweb.py.orig                     |    0
 examples/hidden-service-systemd.service            |   35 -
 examples/hidden_echo.py                            |   36 +
 examples/hidden_echo.py.orig                       |   36 +
 examples/launch_tor.py                             |   80 +-
 examples/launch_tor.py.orig                        |   60 +
 examples/launch_tor2web.py                         |   12 +-
 examples/launch_tor_endpoint.py                    |  140 +--
 examples/launch_tor_endpoint.py.orig               |   91 ++
 examples/launch_tor_endpoint2.py                   |   12 +-
 ...r_endpoint2.py => launch_tor_endpoint2.py.orig} |   12 +-
 examples/launch_tor_unix_sockets.py                |   61 +
 examples/launch_tor_with_hiddenservice.py          |   95 --
 examples/launch_tor_with_hiddenservice.py.orig     |    0
 examples/launch_tor_with_simplehttpd.py            |   51 +-
 examples/minimal_endpoint.py                       |    4 +-
 examples/monitor.py                                |   34 +-
 examples/readme.py                                 |   45 +
 examples/readme3.py                                |   49 +
 examples/schedule_bandwidth.py                     |   75 --
 examples/stem_relay_descriptor.py                  |   48 +-
 examples/stream_circuit_logger.py                  |   56 +-
 examples/systemd.service                           |   43 -
 examples/tor_info.py                               |   24 +-
 examples/torflow_path_selection.py                 |   93 --
 examples/tunnel_tls_through_tor_client.py          |   30 -
 examples/web_client.py                             |   42 +
 examples/web_client.py.orig                        |   42 +
 examples/web_client_custom_circuit.py              |   84 ++
 examples/web_client_custom_circuit.py.orig         |   84 ++
 examples/web_client_treq.py                        |   40 +
 examples/web_client_treq.py.orig                   |   40 +
 examples/webui_server.py                           |    8 +-
 requirements.txt                                   |    5 +-
 setup.cfg                                          |    1 -
 setup.py                                           |    8 +-
 test/profile_startup.py                            |    8 +-
 test/py3_torstate.py                               |   91 ++
 test/test_addrmap.py                               |    4 +-
 test/test_attacher.py                              |   41 +
 test/test_circuit.py                               |  190 +++-
 test/test_controller.py                            | 1161 ++++++++++++++++++++
 test/test_endpoints.py                             |  579 +++++++---
 test/test_fsm.py                                   |    7 +-
 test/test_microdesc.py                             |   96 ++
 test/test_router.py                                |  115 +-
 test/test_socks.py                                 |  780 +++++++++++++
 test/test_stream.py                                |   16 +-
 test/test_torconfig.py                             |  968 ++++------------
 test/test_torcontrolprotocol.py                    |  481 ++++----
 test/test_torinfo.py                               |   39 +-
 test/test_torstate.py                              |  473 +++++---
 test/test_util.py                                  |  175 ++-
 test/test_util_imports.py                          |   12 +-
 test/test_web.py                                   |  108 ++
 test/verify-release.py                             |   58 +
 txtorcon.egg-info/PKG-INFO                         |  313 ++----
 txtorcon.egg-info/SOURCES.txt                      |   65 +-
 txtorcon.egg-info/requires.txt                     |    8 +-
 txtorcon.egg-info/top_level.txt                    |    1 +
 txtorcon/__init__.py                               |   15 +-
 txtorcon/_metadata.py                              |    4 +-
 txtorcon/_microdesc_parser.py                      |  112 ++
 txtorcon/attacher.py                               |   82 ++
 txtorcon/circuit.py                                |  350 +++++-
 txtorcon/controller.py                             |  968 ++++++++++++++++
 txtorcon/endpoints.py                              |  280 +++--
 txtorcon/interface.py                              |  104 +-
 txtorcon/router.py                                 |   69 +-
 txtorcon/socks.py                                  |  744 +++++++++++++
 txtorcon/stream.py                                 |    6 +-
 txtorcon/torconfig.py                              |  735 ++++---------
 txtorcon/torcontrolprotocol.py                     |  152 +--
 txtorcon/torstate.py                               |  272 ++---
 txtorcon/util.py                                   |  204 +++-
 txtorcon/web.py                                    |  146 +++
 125 files changed, 10515 insertions(+), 5964 deletions(-)

diff --git a/INSTALL b/INSTALL
index 20ed05f..543dfca 100644
--- a/INSTALL
+++ b/INSTALL
@@ -4,16 +4,22 @@ See README for more information.
 To just install this as quickly as possible, using a Debian or Ubuntu
 system, run the following as root:
 
-   apt-get install python-setuptools python-twisted python-ipaddress python-geoip python-psutil graphviz
+   apt-get install python-setuptools python-twisted python-ipaddress graphviz
 
    python setup.py install
 
+It's recommended to use a virtualenv (see below), but on OSX (and
+assuming homebrew is installed):
+
+   brew install geoip
+   pip install -r requirements.txt
+   pip install -r dev-requirements.txt
+
 Or, instead of installing locally, simply:
 
    export PYTHONPATH=.
 
 
-
 If you want to take slightly more time, but only install temporarily,
 use virtualenv:
 
diff --git a/Makefile b/Makefile
index ee3b63c..299bc07 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,19 @@
-.PHONY: test html counts coverage sdist clean install doc integration
+.PHONY: test html counts coverage sdist clean install doc integration diagrams
 default: test
-VERSION = 0.17.0
+VERSION = 0.19.3
 
 test:
-	trial --reporter=text test
+	PYTHONPATH=. trial --reporter=text test
 
 tox:
 	tox -i http://localhost:3141/root/pypi
 
+diagrams:
+	automat-visualize --image-directory ./diagrams --image-type png txtorcon
+
+diagrams:
+	automat-visualize --image-directory ./diagrams --image-type png txtorcon
+
 # see also http://docs.docker.io/en/latest/use/baseimages/
 dockerbase-wheezy:
 	@echo 'Building a minimal "wheezy" system.'
@@ -19,15 +25,26 @@ dockerbase-wheezy-image: dockerbase-wheezy
 	tar -C dockerbase-wheezy -c . | docker import - dockerbase-wheezy
 	docker run dockerbase-wheezy cat /etc/issue
 
-txtorcon-tester: Dockerfile dockerbase-wheezy-image
+# see also http://docs.docker.io/en/latest/use/baseimages/
+dockerbase-jessie:
+	@echo 'Building a minimal "jessie" system.'
+	@echo "This may take a while...and will consume about 240MB when done."
+	debootstrap jessie dockerbase-jessie
+
+dockerbase-jessie-image: dockerbase-jessie
+	@echo 'Importing dockerbase-jessie into docker'
+	tar -C dockerbase-jessie -c . | docker import - dockerbase-jessie
+	docker run dockerbase-jessie cat /etc/issue
+
+txtorcon-tester: Dockerfile dockerbase-jessie-image
 	@echo "Creating a Docker.io container"
-	docker build -rm -q -t txtorcon-tester ./
+	docker build --rm -q -t txtorcon-tester ./
 
 integration: ## txtorcon-tester
 	python integration/run.py
 
 install:
-	sudo apt-get install python-setuptools python-twisted python-ipaddr python-geoip python-psutil graphviz
+	sudo apt-get install python-setuptools python-twisted python-ipaddress graphviz
 	python setup.py install
 
 doc: docs/*.rst
@@ -35,8 +52,8 @@ doc: docs/*.rst
 	-cp dist/txtorcon-${VERSION}.tar.gz docs/_build/html
 
 coverage:
-	coverage run --source=txtorcon `which trial` test
-	coverage report --show-missing
+	PYTHONPATH=. coverage run --source=txtorcon `which trial` test
+	cuv graph
 
 htmlcoverage:
 	coverage run --source=txtorcon `which trial` test
@@ -72,9 +89,9 @@ clean:
 	-rm MANIFEST
 	-rm `find . -name \*.py[co]`
 	-cd docs && make clean
-	-rm -rf dockerbase-wheezy
+	-rm -rf dockerbase-jessie
 	-docker rmi txtorcon-tester
-	-docker rmi dockerbase-wheezy
+	-docker rmi dockerbase-jessie
 
 counts:
 	ohcount -s txtorcon/*.py
@@ -82,17 +99,18 @@ counts:
 test-release: dist
 	./scripts/test-release.sh $(shell pwd) ${VERSION}
 
-dist: dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}.tar.gz
+dist: dist/txtorcon-${VERSION}-py2.py3-none-any.whl dist/txtorcon-${VERSION}.tar.gz
 
-dist-sigs: dist/txtorcon-${VERSION}-py2-none-any.whl.asc dist/txtorcon-${VERSION}.tar.gz.asc
+dist-sigs: dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc dist/txtorcon-${VERSION}.tar.gz.asc
 
-sdist: setup.py 
+sdist: setup.py
 	python setup.py sdist
 
-dist/txtorcon-${VERSION}-py2-none-any.whl:
-	python setup.py bdist_wheel
-dist/txtorcon-${VERSION}-py2-none-any.whl.asc: dist/txtorcon-${VERSION}-py2-none-any.whl
-	gpg --verify dist/txtorcon-${VERSION}-py2-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah at meejah.ca dist/txtorcon-${VERSION}-py2-none-any.whl
+dist/txtorcon-${VERSION}-py2.py3-none-any.whl:
+	python setup.py bdist_wheel --universal
+
+dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc: dist/txtorcon-${VERSION}-py2.py3-none-any.whl
+	gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah at meejah.ca dist/txtorcon-${VERSION}-py2.py3-none-any.whl
 
 dist/txtorcon-${VERSION}.tar.gz: sdist
 dist/txtorcon-${VERSION}.tar.gz.asc: dist/txtorcon-${VERSION}.tar.gz
@@ -100,7 +118,7 @@ dist/txtorcon-${VERSION}.tar.gz.asc: dist/txtorcon-${VERSION}.tar.gz
 
 release:
 	twine upload -r pypi -c "txtorcon v${VERSION} tarball" dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}.tar.gz.asc
-	twine upload -r pypi -c "txtorcon v${VERSION} wheel" dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}-py2-none-any.whl.asc
+	twine upload -r pypi -c "txtorcon v${VERSION} wheel" dist/txtorcon-${VERSION}-py2.py3-none-any.whl dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc
 
 
 venv:
@@ -112,5 +130,5 @@ venv:
 	@echo "pip install -r dev-requirements.txt"
 	@echo "python examples/monitor.py"
 
-html: docs/README.rst
+html: docs/*.rst
 	cd docs && make html
diff --git a/PKG-INFO b/PKG-INFO
index f537578..2a92510 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,18 +1,28 @@
 Metadata-Version: 1.1
 Name: txtorcon
-Version: 0.17.0
+Version: 0.19.3
 Summary: 
     Twisted-based Tor controller client, with state-tracking and
     configuration abstractions.
+    https://txtorcon.readthedocs.org
+    https://github.com/meejah/txtorcon
 
 Home-page: https://github.com/meejah/txtorcon
 Author: meejah
 Author-email: meejah at meejah.ca
 License: MIT
-Description: README
-        ======
+Description: 
+        
+        
+        
+        
+        .. _NOTE: see docs/index.rst for the starting-point
+        .. _ALSO: https://txtorcon.readthedocs.org for rendered docs
+        
+        
+        
+        
         
-        Documentation at https://txtorcon.readthedocs.org
         
         .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
             :target: https://www.travis-ci.org/meejah/txtorcon
@@ -26,6 +36,14 @@ Description: README
             :target: http://codecov.io/github/meejah/txtorcon?branch=master
             :alt: codecov
         
+        .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
+            :target: https://txtorcon.readthedocs.io/en/stable
+            :alt: ReadTheDocs
+        
+        .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest
+            :target: https://txtorcon.readthedocs.io/en/latest
+            :alt: ReadTheDocs
+        
         .. image:: http://api.flattr.com/button/flattr-badge-large.png
             :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
             :alt: flattr
@@ -34,213 +52,118 @@ Description: README
             :target: https://landscape.io/github/meejah/txtorcon/master
             :alt: Code Health
         
-        quick start
-        -----------
-        
-        For the impatient, there are two quick ways to install this::
-        
-           $ pip install txtorcon
-        
-        ... or, if you checked out or downloaded the source::
-        
-           $ python setup.py install
-        
-        ... or, better yet, use a virtualenv and the dev requirements::
-        
-           $ virtualenv venv
-           $ ./venv/bin/pip install -e .[dev]
         
-        For OSX, we can install txtorcon with the help of easy_install::
+        txtorcon
+        ========
         
-           $ easy_install txtorcon
+        - **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion
+        - **code**: https://github.com/meejah/txtorcon
+        - ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git``
+        - MIT-licensed;
+        - Python 2.7, PyPy 5.0.0+, Python 3.4+;
+        - depends on
+          `Twisted <https://twistedmatrix.com>`_,
+          `Automat <https://github.com/glyph/automat>`_,
+          (and the `ipaddress <https://pypi.python.org/pypi/ipaddress>`_ backport for non Python 3)
         
-        To avoid installing, you can just add the base of the source to your
-        PYTHONPATH::
+        .. caution::
         
-           $ export PYTHONPATH=`pwd`:$PYTHONPATH
+          Several large, new features have landed on master. If you're working
+          directly from master, note that some of these APIs may change before
+          the next release.
         
-        Then, you will want to explore the examples. Try "python
-        examples/stream\_circuit\_logger.py" for instance.
         
-        On Debian testing (jessie), or with wheezy-backports (big thanks to
-        Lunar^ for all his packaging work) you can install easily::
-        
-            $ apt-get install python-txtorcon
-        
-        You may also like `this asciinema demo <http://asciinema.org/a/5654>`_
-        for an overview.
-        
-        Tor configuration
+        Ten Thousand Feet
         -----------------
         
-        You'll want to have the following options on in your ``torrc``::
-        
-           CookieAuthentication 1
-           CookieAuthFileGroupReadable 1
-        
-        If you want to use unix sockets to speak to tor::
-        
-           ControlSocketsGroupWritable 1
-           ControlSocket /var/run/tor/control
-        
-        The defaults used by py:meth:`txtorcon.build_local_tor_connection` will
-        find a Tor on ``9051`` or ``/var/run/tor/control``
-        
-        
-        overview
-        --------
-        
-        txtorcon is a Twisted-based asynchronous Tor control protocol
-        implementation. Twisted is an event-driven networking engine written
-        in Python and Tor is an onion-routing network designed to improve
-        people's privacy and anonymity on the Internet.
-        
-        The main abstraction of this library is txtorcon.TorControlProtocol
-        which presents an asynchronous API to speak the Tor client protocol in
-        Python. txtorcon also provides abstractions to track and get updates
-        about Tor's state (txtorcon.TorState) and current configuration
-        (including writing it to Tor or disk) in txtorcon.TorConfig, along
-        with helpers to asynchronously launch slave instances of Tor including
-        Twisted endpoint support.
-        
-        txtorcon runs all tests cleanly on:
-        
-        -  Debian "squeeze", "wheezy" and "jessie"
-        -  OS X 10.4 (naif)
-        -  OS X 10.8 (lukas lueg)
-        -  OS X 10.9 (kurt neufeld)
-        -  Fedora 18 (lukas lueg)
-        -  FreeBSD 10 (enrique fynn) (**needed to install "lsof"**)
-        -  RHEL6
-        -  Reports from other OSes appreciated.
-        
-        If instead you want a synchronous (threaded) Python controller
-        library, check out Stem at https://stem.torproject.org/
-        
-        
-        quick implementation overview
-        -----------------------------
-        
-        txtorcon provides a class to track Tor's current state -- such as
-        details about routers, circuits and streams -- called
-        txtorcon.TorState and an abstraction to the configuration values via
-        txtorcon.TorConfig which provides attribute-style accessors to Tor's
-        state (including making changes). txtorcon.TorState provides
-        txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which
-        implement a listener interface so client code may receive updates (in
-        real time) including Tor events.
-        
-        txtorcon uses **trial for unit-tests** and has 100% test-coverage --
-        which is not to say I've covered all the cases, but nearly all of the
-        code is at least exercised somehow by the unit tests.
-        
-        Tor itself is not required to be running for any of the tests. ohcount
-        claims around 2000 lines of code for the core bit; around 4000
-        including tests. About 37% comments in the not-test code.
-        
-        There are a few simple integration tests, based on Docker. More are
-        always welcome!
-        
-        
-        dependencies / requirements
+        txtorcon is an implementation of the `control-spec
+        <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+        for `Tor <https://www.torproject.org/>`_ using the `Twisted
+        <https://twistedmatrix.com/trac/>`_ networking library for `Python
+        <http://python.org/>`_.
+        
+        This is useful for writing utilities to control or make use of Tor in
+        event-based Python programs. If your Twisted program supports
+        endpoints (like ``twistd`` does) your server or client can make use of
+        Tor immediately, with no code changes. Start your own Tor or connect
+        to one and get live stream, circuit, relay updates; read and change
+        config; monitor events; build circuits; create onion services;
+        etcetera (`ReadTheDocs <https://txtorcon.readthedocs.org>`_).
+        
+        
+        Some Possibly Motivational Example Code
+        ---------------------------------------
+        
+        `download <examples/readme.py>`_
+        (also `python3 style <examples/readme3.py>`_)
+        
+        .. code:: python
+        
+            from twisted.internet.task import react
+            from twisted.internet.defer import inlineCallbacks
+            from twisted.internet.endpoints import UNIXClientEndpoint
+            import treq
+            import txtorcon
+        
+            @react
+            @inlineCallbacks
+            def main(reactor):
+                tor = yield txtorcon.connect(
+                    reactor,
+                    UNIXClientEndpoint(reactor, "/var/run/tor/control")
+                )
+        
+                print("Connected to Tor version {}".format(tor.version))
+        
+                url = 'https://www.torproject.org:443'
+                print("Downloading {}".format(url))
+                resp = yield treq.get(url, agent=tor.web_agent())
+        
+                print("   {} bytes".format(resp.length))
+                data = yield resp.text()
+                print("Got {} bytes:\n{}\n[...]{}".format(
+                    len(data),
+                    data[:120],
+                    data[-120:],
+                ))
+        
+                print("Creating a circuit")
+                state = yield tor.create_state()
+                circ = yield state.build_circuit()
+                yield circ.when_built()
+                print("  path: {}".format(" -> ".join([r.ip for r in circ.path])))
+        
+                print("Downloading meejah's public key via above circuit...")
+                resp = yield treq.get(
+                    'https://meejah.ca/meejah.asc',
+                    agent=circ.web_agent(reactor, tor.config.socks_endpoint(reactor)),
+                )
+                data = yield resp.text()
+                print(data)
+        
+        
+        
+        Try It Now On Debian/Ubuntu
         ---------------------------
         
-        - `twisted <http://twistedmatrix.com>`_: txtorcon should work with any
-           Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so
-           does txtorcon (if you find something broken on Py3 please file a bug).
-        
-        -  `GeoIP <https://www.maxmind.com/app/python>`_: **optional** provides location
-           information for ip addresses; you will want to download GeoLite City
-           from `MaxMind <https://www.maxmind.com/app/geolitecity>`_ or pay them
-           for more accuracy. Or use tor-geoip, which makes this sort-of
-           optional, in that we'll query Tor for the IP if the GeoIP database
-           doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
-        
-        -  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
-           documentation. In that case you'll also need something called
-           ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
-           the Interface-derived docs properly.
-        
-        -  development: `coverage <http://nedbatchelder.com/code/coverage/>`_ to
-           run the code-coverage metrics, and Tox
-        
-        -  optional: GraphViz is used in the tests (and to generate state-machine
-           diagrams, if you like) but those tests are skipped if "dot" isn't
-           in your path
-        
-        .. BEGIN_INSTALL
-        
-        In any case, on a `Debian <http://www.debian.org/>`_ wheezy, squeeze or
-        Ubuntu system, this should work::
-        
-            apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor
-            apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development
-        
-        .. END_INSTALL
-        
-        Using pip this would be::
-        
-            pip install Twisted ipaddr pygeoip
-            pip install GeoIP Sphinx repoze.sphinx.autointerface coverage  # for development
-        
-        or::
-        
-            pip install -r requirements.txt
-            pip install -r dev-requirements.txt
-        
-        or for the bare minimum::
-        
-            pip install Twisted  # will install zope.interface too
-        
-        
-        documentation
-        -------------
-        
-        It is likely that you will need to read at least some of
-        `control-spec.txt <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
-        from the torspec git repository so you know what's being abstracted by
-        this library.
-        
-        Run "make doc" to build the Sphinx documentation locally, or rely on
-        ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged
-        release and the latest master.
-        
-        There is also a directory of examples/ scripts, which have inline
-        documentation explaining their use.
-        
-        
-        contact information
-        -------------------
-        
-        For novelty value, the Web site (with built documentation and so forth)
-        can be viewed via Tor at http://timaq4ygg2iegci7.onion although the
-        code itself is hosted via git::
-        
-            torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git
-        
-        or::
+        For example, serve some files via an onion service (*aka* hidden
+        service):
         
-            git clone git://github.com/meejah/txtorcon.git
+        .. code-block:: shell-session
         
-        You may contact me via ``meejah at meejah dot ca`` with GPG key
-        `0xC2602803128069A7
-        <http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xC2602803128069A7>`_
-        or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A
-        2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``.
+            $ sudo apt-get install python-txtorcon
+            $ twistd -n web --port "onion:80" --path ~/public_html
         
-        It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC
-        <http://www.oftc.net/oftc/>`_ but be patient for replies (I do look at
-        scrollback, so putting "meejah: " in front will alert my client).
         
-        More conventionally, you may get the code at GitHub and documentation
-        via ReadTheDocs:
+        Read More
+        ---------
         
-        -  https://github.com/meejah/txtorcon
-        -  https://txtorcon.readthedocs.org
+        All the documentation starts `in docs/index.rst
+        <docs/index.rst>`_. Also hosted at `txtorcon.rtfd.org
+        <https://txtorcon.readthedocs.io/en/latest/>`_.
         
-        Please do **use the GitHub issue-tracker** to report bugs. Patches,
-        pull-requests, comments and criticisms are all welcomed and
-        appreciated.
+        You'll want to start with `the introductions <docs/introduction.rst>`_ (`hosted at RTD
+        <https://txtorcon.readthedocs.org/en/latest/introduction.html>`_).
         
 Keywords: python,twisted,tor,tor controller
 Platform: UNKNOWN
diff --git a/README.rst b/README.rst
index 24760cf..d3b39d2 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,15 @@
-README
-======
 
-Documentation at https://txtorcon.readthedocs.org
+
+
+
+
+.. _NOTE: see docs/index.rst for the starting-point
+.. _ALSO: https://txtorcon.readthedocs.org for rendered docs
+
+
+
+
+
 
 .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
     :target: https://www.travis-ci.org/meejah/txtorcon
@@ -15,6 +23,14 @@ Documentation at https://txtorcon.readthedocs.org
     :target: http://codecov.io/github/meejah/txtorcon?branch=master
     :alt: codecov
 
+.. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
+    :target: https://txtorcon.readthedocs.io/en/stable
+    :alt: ReadTheDocs
+
+.. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest
+    :target: https://txtorcon.readthedocs.io/en/latest
+    :alt: ReadTheDocs
+
 .. image:: http://api.flattr.com/button/flattr-badge-large.png
     :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
     :alt: flattr
@@ -23,210 +39,115 @@ Documentation at https://txtorcon.readthedocs.org
     :target: https://landscape.io/github/meejah/txtorcon/master
     :alt: Code Health
 
-quick start
------------
-
-For the impatient, there are two quick ways to install this::
-
-   $ pip install txtorcon
-
-... or, if you checked out or downloaded the source::
-
-   $ python setup.py install
-
-... or, better yet, use a virtualenv and the dev requirements::
 
-   $ virtualenv venv
-   $ ./venv/bin/pip install -e .[dev]
+txtorcon
+========
 
-For OSX, we can install txtorcon with the help of easy_install::
+- **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion
+- **code**: https://github.com/meejah/txtorcon
+- ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git``
+- MIT-licensed;
+- Python 2.7, PyPy 5.0.0+, Python 3.4+;
+- depends on
+  `Twisted <https://twistedmatrix.com>`_,
+  `Automat <https://github.com/glyph/automat>`_,
+  (and the `ipaddress <https://pypi.python.org/pypi/ipaddress>`_ backport for non Python 3)
 
-   $ easy_install txtorcon
+.. caution::
 
-To avoid installing, you can just add the base of the source to your
-PYTHONPATH::
+  Several large, new features have landed on master. If you're working
+  directly from master, note that some of these APIs may change before
+  the next release.
 
-   $ export PYTHONPATH=`pwd`:$PYTHONPATH
 
-Then, you will want to explore the examples. Try "python
-examples/stream\_circuit\_logger.py" for instance.
-
-On Debian testing (jessie), or with wheezy-backports (big thanks to
-Lunar^ for all his packaging work) you can install easily::
-
-    $ apt-get install python-txtorcon
-
-You may also like `this asciinema demo <http://asciinema.org/a/5654>`_
-for an overview.
-
-Tor configuration
+Ten Thousand Feet
 -----------------
 
-You'll want to have the following options on in your ``torrc``::
-
-   CookieAuthentication 1
-   CookieAuthFileGroupReadable 1
-
-If you want to use unix sockets to speak to tor::
-
-   ControlSocketsGroupWritable 1
-   ControlSocket /var/run/tor/control
-
-The defaults used by py:meth:`txtorcon.build_local_tor_connection` will
-find a Tor on ``9051`` or ``/var/run/tor/control``
-
-
-overview
---------
-
-txtorcon is a Twisted-based asynchronous Tor control protocol
-implementation. Twisted is an event-driven networking engine written
-in Python and Tor is an onion-routing network designed to improve
-people's privacy and anonymity on the Internet.
-
-The main abstraction of this library is txtorcon.TorControlProtocol
-which presents an asynchronous API to speak the Tor client protocol in
-Python. txtorcon also provides abstractions to track and get updates
-about Tor's state (txtorcon.TorState) and current configuration
-(including writing it to Tor or disk) in txtorcon.TorConfig, along
-with helpers to asynchronously launch slave instances of Tor including
-Twisted endpoint support.
-
-txtorcon runs all tests cleanly on:
-
--  Debian "squeeze", "wheezy" and "jessie"
--  OS X 10.4 (naif)
--  OS X 10.8 (lukas lueg)
--  OS X 10.9 (kurt neufeld)
--  Fedora 18 (lukas lueg)
--  FreeBSD 10 (enrique fynn) (**needed to install "lsof"**)
--  RHEL6
--  Reports from other OSes appreciated.
-
-If instead you want a synchronous (threaded) Python controller
-library, check out Stem at https://stem.torproject.org/
-
-
-quick implementation overview
------------------------------
-
-txtorcon provides a class to track Tor's current state -- such as
-details about routers, circuits and streams -- called
-txtorcon.TorState and an abstraction to the configuration values via
-txtorcon.TorConfig which provides attribute-style accessors to Tor's
-state (including making changes). txtorcon.TorState provides
-txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which
-implement a listener interface so client code may receive updates (in
-real time) including Tor events.
-
-txtorcon uses **trial for unit-tests** and has 100% test-coverage --
-which is not to say I've covered all the cases, but nearly all of the
-code is at least exercised somehow by the unit tests.
-
-Tor itself is not required to be running for any of the tests. ohcount
-claims around 2000 lines of code for the core bit; around 4000
-including tests. About 37% comments in the not-test code.
-
-There are a few simple integration tests, based on Docker. More are
-always welcome!
-
-
-dependencies / requirements
+txtorcon is an implementation of the `control-spec
+<https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+for `Tor <https://www.torproject.org/>`_ using the `Twisted
+<https://twistedmatrix.com/trac/>`_ networking library for `Python
+<http://python.org/>`_.
+
+This is useful for writing utilities to control or make use of Tor in
+event-based Python programs. If your Twisted program supports
+endpoints (like ``twistd`` does) your server or client can make use of
+Tor immediately, with no code changes. Start your own Tor or connect
+to one and get live stream, circuit, relay updates; read and change
+config; monitor events; build circuits; create onion services;
+etcetera (`ReadTheDocs <https://txtorcon.readthedocs.org>`_).
+
+
+Some Possibly Motivational Example Code
+---------------------------------------
+
+`download <examples/readme.py>`_
+(also `python3 style <examples/readme3.py>`_)
+
+.. code:: python
+
+    from twisted.internet.task import react
+    from twisted.internet.defer import inlineCallbacks
+    from twisted.internet.endpoints import UNIXClientEndpoint
+    import treq
+    import txtorcon
+
+    @react
+    @inlineCallbacks
+    def main(reactor):
+        tor = yield txtorcon.connect(
+            reactor,
+            UNIXClientEndpoint(reactor, "/var/run/tor/control")
+        )
+
+        print("Connected to Tor version {}".format(tor.version))
+
+        url = 'https://www.torproject.org:443'
+        print("Downloading {}".format(url))
+        resp = yield treq.get(url, agent=tor.web_agent())
+
+        print("   {} bytes".format(resp.length))
+        data = yield resp.text()
+        print("Got {} bytes:\n{}\n[...]{}".format(
+            len(data),
+            data[:120],
+            data[-120:],
+        ))
+
+        print("Creating a circuit")
+        state = yield tor.create_state()
+        circ = yield state.build_circuit()
+        yield circ.when_built()
+        print("  path: {}".format(" -> ".join([r.ip for r in circ.path])))
+
+        print("Downloading meejah's public key via above circuit...")
+        resp = yield treq.get(
+            'https://meejah.ca/meejah.asc',
+            agent=circ.web_agent(reactor, tor.config.socks_endpoint(reactor)),
+        )
+        data = yield resp.text()
+        print(data)
+
+
+
+Try It Now On Debian/Ubuntu
 ---------------------------
 
-- `twisted <http://twistedmatrix.com>`_: txtorcon should work with any
-   Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so
-   does txtorcon (if you find something broken on Py3 please file a bug).
-
--  `GeoIP <https://www.maxmind.com/app/python>`_: **optional** provides location
-   information for ip addresses; you will want to download GeoLite City
-   from `MaxMind <https://www.maxmind.com/app/geolitecity>`_ or pay them
-   for more accuracy. Or use tor-geoip, which makes this sort-of
-   optional, in that we'll query Tor for the IP if the GeoIP database
-   doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
-
--  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
-   documentation. In that case you'll also need something called
-   ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
-   the Interface-derived docs properly.
-
--  development: `coverage <http://nedbatchelder.com/code/coverage/>`_ to
-   run the code-coverage metrics, and Tox
-
--  optional: GraphViz is used in the tests (and to generate state-machine
-   diagrams, if you like) but those tests are skipped if "dot" isn't
-   in your path
-
-.. BEGIN_INSTALL
-
-In any case, on a `Debian <http://www.debian.org/>`_ wheezy, squeeze or
-Ubuntu system, this should work::
-
-    apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor
-    apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development
-
-.. END_INSTALL
-
-Using pip this would be::
-
-    pip install Twisted ipaddr pygeoip
-    pip install GeoIP Sphinx repoze.sphinx.autointerface coverage  # for development
-
-or::
-
-    pip install -r requirements.txt
-    pip install -r dev-requirements.txt
-
-or for the bare minimum::
-
-    pip install Twisted  # will install zope.interface too
-
-
-documentation
--------------
-
-It is likely that you will need to read at least some of
-`control-spec.txt <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
-from the torspec git repository so you know what's being abstracted by
-this library.
-
-Run "make doc" to build the Sphinx documentation locally, or rely on
-ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged
-release and the latest master.
-
-There is also a directory of examples/ scripts, which have inline
-documentation explaining their use.
-
-
-contact information
--------------------
-
-For novelty value, the Web site (with built documentation and so forth)
-can be viewed via Tor at http://timaq4ygg2iegci7.onion although the
-code itself is hosted via git::
-
-    torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git
-
-or::
+For example, serve some files via an onion service (*aka* hidden
+service):
 
-    git clone git://github.com/meejah/txtorcon.git
+.. code-block:: shell-session
 
-You may contact me via ``meejah at meejah dot ca`` with GPG key
-`0xC2602803128069A7
-<http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xC2602803128069A7>`_
-or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A
-2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``.
+    $ sudo apt-get install python-txtorcon
+    $ twistd -n web --port "onion:80" --path ~/public_html
 
-It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC
-<http://www.oftc.net/oftc/>`_ but be patient for replies (I do look at
-scrollback, so putting "meejah: " in front will alert my client).
 
-More conventionally, you may get the code at GitHub and documentation
-via ReadTheDocs:
+Read More
+---------
 
--  https://github.com/meejah/txtorcon
--  https://txtorcon.readthedocs.org
+All the documentation starts `in docs/index.rst
+<docs/index.rst>`_. Also hosted at `txtorcon.rtfd.org
+<https://txtorcon.readthedocs.io/en/latest/>`_.
 
-Please do **use the GitHub issue-tracker** to report bugs. Patches,
-pull-requests, comments and criticisms are all welcomed and
-appreciated.
+You'll want to start with `the introductions <docs/introduction.rst>`_ (`hosted at RTD
+<https://txtorcon.readthedocs.org/en/latest/introduction.html>`_).
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 219160a..44188ee 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,5 +1,6 @@
 tox
 coverage
+cuvner
 setuptools>=0.8.0
 Sphinx
 repoze.sphinx.autointerface>=0.4
@@ -11,4 +12,4 @@ pyflakes
 pep8
 mock
 ipaddress>=1.0.16
-GeoIP
+geoip
diff --git a/docs/README.rst b/docs/README.rst
deleted file mode 100644
index 24760cf..0000000
--- a/docs/README.rst
+++ /dev/null
@@ -1,232 +0,0 @@
-README
-======
-
-Documentation at https://txtorcon.readthedocs.org
-
-.. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
-    :target: https://www.travis-ci.org/meejah/txtorcon
-    :alt: travis
-
-.. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png
-    :target: https://coveralls.io/r/meejah/txtorcon
-    :alt: coveralls
-
-.. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master
-    :target: http://codecov.io/github/meejah/txtorcon?branch=master
-    :alt: codecov
-
-.. image:: http://api.flattr.com/button/flattr-badge-large.png
-    :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
-    :alt: flattr
-
-.. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
-    :target: https://landscape.io/github/meejah/txtorcon/master
-    :alt: Code Health
-
-quick start
------------
-
-For the impatient, there are two quick ways to install this::
-
-   $ pip install txtorcon
-
-... or, if you checked out or downloaded the source::
-
-   $ python setup.py install
-
-... or, better yet, use a virtualenv and the dev requirements::
-
-   $ virtualenv venv
-   $ ./venv/bin/pip install -e .[dev]
-
-For OSX, we can install txtorcon with the help of easy_install::
-
-   $ easy_install txtorcon
-
-To avoid installing, you can just add the base of the source to your
-PYTHONPATH::
-
-   $ export PYTHONPATH=`pwd`:$PYTHONPATH
-
-Then, you will want to explore the examples. Try "python
-examples/stream\_circuit\_logger.py" for instance.
-
-On Debian testing (jessie), or with wheezy-backports (big thanks to
-Lunar^ for all his packaging work) you can install easily::
-
-    $ apt-get install python-txtorcon
-
-You may also like `this asciinema demo <http://asciinema.org/a/5654>`_
-for an overview.
-
-Tor configuration
------------------
-
-You'll want to have the following options on in your ``torrc``::
-
-   CookieAuthentication 1
-   CookieAuthFileGroupReadable 1
-
-If you want to use unix sockets to speak to tor::
-
-   ControlSocketsGroupWritable 1
-   ControlSocket /var/run/tor/control
-
-The defaults used by py:meth:`txtorcon.build_local_tor_connection` will
-find a Tor on ``9051`` or ``/var/run/tor/control``
-
-
-overview
---------
-
-txtorcon is a Twisted-based asynchronous Tor control protocol
-implementation. Twisted is an event-driven networking engine written
-in Python and Tor is an onion-routing network designed to improve
-people's privacy and anonymity on the Internet.
-
-The main abstraction of this library is txtorcon.TorControlProtocol
-which presents an asynchronous API to speak the Tor client protocol in
-Python. txtorcon also provides abstractions to track and get updates
-about Tor's state (txtorcon.TorState) and current configuration
-(including writing it to Tor or disk) in txtorcon.TorConfig, along
-with helpers to asynchronously launch slave instances of Tor including
-Twisted endpoint support.
-
-txtorcon runs all tests cleanly on:
-
--  Debian "squeeze", "wheezy" and "jessie"
--  OS X 10.4 (naif)
--  OS X 10.8 (lukas lueg)
--  OS X 10.9 (kurt neufeld)
--  Fedora 18 (lukas lueg)
--  FreeBSD 10 (enrique fynn) (**needed to install "lsof"**)
--  RHEL6
--  Reports from other OSes appreciated.
-
-If instead you want a synchronous (threaded) Python controller
-library, check out Stem at https://stem.torproject.org/
-
-
-quick implementation overview
------------------------------
-
-txtorcon provides a class to track Tor's current state -- such as
-details about routers, circuits and streams -- called
-txtorcon.TorState and an abstraction to the configuration values via
-txtorcon.TorConfig which provides attribute-style accessors to Tor's
-state (including making changes). txtorcon.TorState provides
-txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which
-implement a listener interface so client code may receive updates (in
-real time) including Tor events.
-
-txtorcon uses **trial for unit-tests** and has 100% test-coverage --
-which is not to say I've covered all the cases, but nearly all of the
-code is at least exercised somehow by the unit tests.
-
-Tor itself is not required to be running for any of the tests. ohcount
-claims around 2000 lines of code for the core bit; around 4000
-including tests. About 37% comments in the not-test code.
-
-There are a few simple integration tests, based on Docker. More are
-always welcome!
-
-
-dependencies / requirements
----------------------------
-
-- `twisted <http://twistedmatrix.com>`_: txtorcon should work with any
-   Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so
-   does txtorcon (if you find something broken on Py3 please file a bug).
-
--  `GeoIP <https://www.maxmind.com/app/python>`_: **optional** provides location
-   information for ip addresses; you will want to download GeoLite City
-   from `MaxMind <https://www.maxmind.com/app/geolitecity>`_ or pay them
-   for more accuracy. Or use tor-geoip, which makes this sort-of
-   optional, in that we'll query Tor for the IP if the GeoIP database
-   doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
-
--  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
-   documentation. In that case you'll also need something called
-   ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
-   the Interface-derived docs properly.
-
--  development: `coverage <http://nedbatchelder.com/code/coverage/>`_ to
-   run the code-coverage metrics, and Tox
-
--  optional: GraphViz is used in the tests (and to generate state-machine
-   diagrams, if you like) but those tests are skipped if "dot" isn't
-   in your path
-
-.. BEGIN_INSTALL
-
-In any case, on a `Debian <http://www.debian.org/>`_ wheezy, squeeze or
-Ubuntu system, this should work::
-
-    apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor
-    apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development
-
-.. END_INSTALL
-
-Using pip this would be::
-
-    pip install Twisted ipaddr pygeoip
-    pip install GeoIP Sphinx repoze.sphinx.autointerface coverage  # for development
-
-or::
-
-    pip install -r requirements.txt
-    pip install -r dev-requirements.txt
-
-or for the bare minimum::
-
-    pip install Twisted  # will install zope.interface too
-
-
-documentation
--------------
-
-It is likely that you will need to read at least some of
-`control-spec.txt <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
-from the torspec git repository so you know what's being abstracted by
-this library.
-
-Run "make doc" to build the Sphinx documentation locally, or rely on
-ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged
-release and the latest master.
-
-There is also a directory of examples/ scripts, which have inline
-documentation explaining their use.
-
-
-contact information
--------------------
-
-For novelty value, the Web site (with built documentation and so forth)
-can be viewed via Tor at http://timaq4ygg2iegci7.onion although the
-code itself is hosted via git::
-
-    torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git
-
-or::
-
-    git clone git://github.com/meejah/txtorcon.git
-
-You may contact me via ``meejah at meejah dot ca`` with GPG key
-`0xC2602803128069A7
-<http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xC2602803128069A7>`_
-or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A
-2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``.
-
-It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC
-<http://www.oftc.net/oftc/>`_ but be patient for replies (I do look at
-scrollback, so putting "meejah: " in front will alert my client).
-
-More conventionally, you may get the code at GitHub and documentation
-via ReadTheDocs:
-
--  https://github.com/meejah/txtorcon
--  https://txtorcon.readthedocs.org
-
-Please do **use the GitHub issue-tracker** to report bugs. Patches,
-pull-requests, comments and criticisms are all welcomed and
-appreciated.
diff --git a/docs/_themes/alabaster/static/alabaster.css_t b/docs/_themes/alabaster/static/alabaster.css_t
index d136a34..53b121d 100644
--- a/docs/_themes/alabaster/static/alabaster.css_t
+++ b/docs/_themes/alabaster/static/alabaster.css_t
@@ -31,6 +31,29 @@ div.document {
     margin: 30px auto 0 auto;
 }
 
+.first-time {
+    font-size: 110%;
+    font-family: 'source sans pro', sans;
+    background-color: #eeeeee;
+    padding: .5em;
+    margin-top: -.25em;
+    margin-bottom: .5em;
+    border: 0.33em solid #ddeedd;
+    border-radius: 0.25em;
+    box-shadow: 0px 1px 4px 1px #ccc;
+}
+
+.caution {
+    font-size: 110%;
+    font-family: 'source sans pro', sans;
+    background-color: #eedddd;
+    padding: .5em;
+    margin-top: -.25em;
+    margin-bottom: -.25em;
+    border: 0.33em solid #ffeeee;
+    border-radius: 0.25em;
+}
+
 div.documentwrapper {
     float: left;
     width: 100%;
diff --git a/docs/conf.py b/docs/conf.py
index 7f7817f..b1b3ab5 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,6 +27,10 @@ sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '_themes'))
 # If your documentation needs a minimal Sphinx version, state it here.
 #needs_sphinx = '1.0'
 
+#keep_warnings = True
+pygments_style = 'monokai'
+#pygments_style = 'solarized_dark256'
+
 ## trying to set t his somewhere...
 autodoc_member_order = 'bysource'
 autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members']
@@ -34,14 +38,15 @@ autoclass_content = 'both'
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc',
-              'sphinx.ext.todo',
-              'sphinx.ext.autosummary',
-              'sphinx.ext.todo',
-              'sphinx.ext.coverage',
-              'repoze.sphinx.autointerface',
-              'apilinks_sphinxext'
-              ]
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.todo',
+    'sphinx.ext.autosummary',
+    'sphinx.ext.todo',
+    'sphinx.ext.coverage',
+    'repoze.sphinx.autointerface',
+    'apilinks_sphinxext',
+]
 
 todo_include_todos = True
 
@@ -100,7 +105,7 @@ exclude_patterns = ['_build']
 #show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+#pygments_style = 'sphinx'
 
 # A list of ignored prefixes for module index sorting.
 #modindex_common_prefix = []
@@ -152,7 +157,7 @@ html_theme_options = {
     'logo_name': 'true',
     'description': 'Control Tor from Twisted',
     'logo_text_align': 'center',
-    'flattr_uri': 'http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub',
+##    'flattr_uri': 'http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub',
     'note_bg': '#ccddcc',
     'note_border': '#839496',
 }
diff --git a/docs/examples.rst b/docs/examples.rst
index 0286bed..e62ceb0 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -1,181 +1,168 @@
+.. _examples:
+
 Examples
 ========
 
-In the :file:`examples/` sub-directory are a few different
-mostly-simple ways of using txtorcon. They all show how to set up a
-connection and then wait for and use various information from Tor.
+The examples are grouped by functionality and serve as mini-HOWTOs --
+if you have a use-case that is missing, it may be useful to add an
+example, so please file a bug.
 
-.. _hello_darkweb.py:
+All files are in the :file:`examples/` sub-directory and are ready to
+run, usually with defaults designed to work with Tor Browser Bundle
+(``localhost:9151``).
 
-:file:`hello_darkweb.py`
-------------------------
+The examples use `default_control_port()` to determine how to connect
+which you can override with an environment variable:
+`TX_CONTROL_PORT`. So e.g. `export TX_CONTROL_PORT=9050` to run the
+examples again a system-wide Tor daemon.
 
-:download:`Download the example <../examples/hello_darkweb.py>`.
 
-This is a minimal (but still working) hidden-service set up using the
-endpoint parsers (these are Twisted ``IPlugin`` implementations; see
-`the documentation
-<https://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.serverFromString.html>`_
-for more).  It even shows Tor's progress messages on the console.
+.. contents::
+   :depth: 2
+   :local:
+   :backlinks: none
 
-.. literalinclude:: ../examples/hello_darkweb.py
 
+Web: clients
+------------
 
-.. _disallow_streams_by_port.py:
 
-:file:`disallow_streams_by_port.py`
------------------------------------
+.. _web_client.py:
 
-:download:`Download the example <../examples/disallow_streams_by_port.py>`.
-An example using :class:`~txtorcon.torstate.IStreamAttacher` which is
-very simple and does just what it sounds like: never attaches Streams
-exiting to a port in the "disallowed" list (it also explicitly closes
-them). Note that **Tor already has this feature**; this is just to
-illustrate how to use IStreamAttacher and that you may close streams.
+``web_client.py``
+~~~~~~~~~~~~~~~~~
 
-.. literalinclude:: ../examples/disallow_streams_by_port.py
+:download:`Download the example <../examples/web_client.py>`.
 
+Uses `twisted.web.client
+<http://twistedmatrix.com/documents/current/web/howto/client.html>`_
+to download a Web page using a ``twisted.web.client.Agent``, via any
+circuit Tor chooses.
 
-.. _launch_tor.py:
+.. literalinclude:: ../examples/web_client.py
 
-:file:`launch_tor.py`
----------------------
 
-:download:`Download the example <../examples/launch_tor.py>`.  Set up
-a tor configuration and launch a slave Tor. This takes care of the
-setting Tor's notion ownership so that when the control connection
-goes away, so does the running Tor.
 
-.. literalinclude:: ../examples/launch_tor.py
+.. _web_client_treq.py:
 
+``web_client_treq.py``
+~~~~~~~~~~~~~~~~~~~~~~
 
-.. _launch_tor_endpoint.py:
+:download:`Download the example <../examples/web_client_treq.py>`.
 
-:file:`launch_tor_endpoint.py`
-------------------------------
+Uses `treq <https://treq.readthedocs.io/en/latest/>`_ to download a
+Web page via Tor.
 
-:download:`Download the example
-<../examples/launch_tor_endpoint.py>`. Using the
-:class:`txtorcon.TCP4HiddenServiceEndpoint` class to start up a Tor
-with a hidden service pointed to an
-:api:`twisted.internet.interfaces.IStreamServerEndpoint
-<IStreamServerEndpoint>`; fairly similar to
-:ref:`launch_tor_with_hiddenservice.py` but more things are automated.
+.. literalinclude:: ../examples/web_client_treq.py
 
-.. literalinclude:: ../examples/launch_tor_endpoint.py
 
 
-.. _launch_tor_with_hiddenservice.py:
+.. _web_client_custom_circuit.py:
 
-:file:`launch_tor_with_hiddenservice.py`
-----------------------------------------
+``web_client_custom_circuit.py``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-:download:`Download the example
-<../examples/launch_tor_with_hiddenservice.py>`. A more complicated
-version of the :ref:`launch_tor.py` example where we also set up a
-Twisted Web server in the process and have the slave Tor set up a
-hidden service configuration pointing to it.
+:download:`Download the example <../examples/web_client_custom_circuit.py>`.
 
-.. literalinclude:: ../examples/launch_tor_with_hiddenservice.py
+Builds a custom circuit, and then uses `twisted.web.client
+<http://twistedmatrix.com/documents/current/web/howto/client.html>`_
+to download a Web page using the circuit created.
 
+.. literalinclude:: ../examples/web_client_custom_circuit.py
 
-.. _stream_circuit_logger.py:
 
-:file:`stream_circuit_logger.py`
---------------------------------
+Starting Tor
+------------
 
-:download:`Download the example <../examples/stream_circuit_logger.py>`.
-For listening to changes in the Circuit and State objects, this
-example is the easiest to understand as it just prints out (some of)
-the events that happen. Run this, then visit some Web sites via Tor to
-see what's going on.
-
-.. literalinclude:: ../examples/stream_circuit_logger.py
+.. _launch_tor.py:
 
+:file:`launch_tor.py`
+~~~~~~~~~~~~~~~~~~~~~
 
-.. _attach_streams_by_country.py:
+:download:`Download the example <../examples/launch_tor.py>`.  Launch
+a new Tor instance. This takes care of setting Tor's notion ownership
+so that when the control connection goes away the running Tor exits.
 
-:file:`circuit_for_next_stream.py`
-------------------------------------
+.. literalinclude:: ../examples/launch_tor.py
 
-:download:`Download the example
-<../examples/circuit_for_next_stream.py>`.  This creates a custom
-stream specified via router names on the command-line and then
-attaches the next new stream the controller sees to this circuit and
-exits. A decent custom-circuit example, and a little simpler than the
-following example (attach_streams_by_country).
 
-.. literalinclude:: ../examples/circuit_for_next_stream.py
+.. _launch_tor_endpoint.py:
 
+:file:`launch_tor_endpoint.py`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. _attach_streams_by_country.py:
+:download:`Download the example
+<../examples/launch_tor_endpoint.py>`. Using the
+:class:`txtorcon.TCP4HiddenServiceEndpoint` class to start up a Tor
+with a hidden service pointed to an
+:api:`twisted.internet.interfaces.IStreamServerEndpoint
+<IStreamServerEndpoint>`.
 
-:file:`attach_streams_by_country.py`
-------------------------------------
+.. literalinclude:: ../examples/launch_tor_endpoint.py
 
-:download:`Download the example <../examples/attach_streams_by_country.py>`.
-This is one of the more complicated examples. It uses a custom Stream
-attacher (via :class:`~txtorcon.torstate.IStreamAttacher`) to only attach
-Streams to a Circuit with an exit node in the same country as the
-server to which the Stream is going (as determined by GeoIP). Caveat:
-the DNS lookups go via a Tor-assigned stream, so for sites which use
-DNS trickery to get you to a "close" server, this won't be as
-interesting. For bonus points, if there is no Circuit exiting in the
-correct country, one is created before the Stream is attached.
 
-.. literalinclude:: ../examples/attach_streams_by_country.py
+Circuits and Streams
+--------------------
 
+.. _disallow_streams_by_port.py:
 
-.. _schedule_bandwidth.py:
+:file:`disallow_streams_by_port.py`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-:file:`schedule_bandwidth.py`
------------------------------
+:download:`Download the example <../examples/disallow_streams_by_port.py>`.
+An example using :class:`~txtorcon.torstate.IStreamAttacher` which is
+very simple and does just what it sounds like: never attaches Streams
+exiting to a port in the "disallowed" list (it also explicitly closes
+them). Note that **Tor already has this feature**; this is just to
+illustrate how to use IStreamAttacher and that you may close streams.
 
-:download:`Download the example <../examples/schedule_bandwidth.py>`.
-This is pretty similar to a feature Tor already has and is basically
-useless as-is since what it does is toggle the amount of relay
-bandwidth you're willing to carry from 0 to 20KiB/s every 20
-minutes. A slightly-more-entertaining way to illustate config
-changes. (This is useless because your relay takes at least an hour to
-appear in the consensus).
+XXX keep this one?
 
-.. literalinclude:: ../examples/schedule_bandwidth.py
+.. literalinclude:: ../examples/disallow_streams_by_port.py
 
 
 
-.. _dump_config.py:
+.. _stream_circuit_logger.py:
 
-:file:`dump_config.py`
------------------------------
+:file:`stream_circuit_logger.py`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-:download:`Download the example <../examples/dump_config.py>`.
-Very simple read-only use of :class:`txtorcon.TorConfig`
+:download:`Download the example <../examples/stream_circuit_logger.py>`.
+For listening to changes in the Circuit and State objects, this
+example is the easiest to understand as it just prints out (some of)
+the events that happen. Run this, then visit some Web sites via Tor to
+see what's going on.
 
-.. literalinclude:: ../examples/dump_config.py
+.. literalinclude:: ../examples/stream_circuit_logger.py
 
 
+Events
+------
 
 
 .. _monitor.py:
 
 :file:`monitor.py`
------------------------------
+~~~~~~~~~~~~~~~~~~
 
 :download:`Download the example <../examples/monitor.py>`.
 
 Use a plain :class:`txtorcon.TorControlProtocol` instance to listen
-for SETEVNET updates. In this case marginally useful, as it listens
-for logging things INFO, NOTICE, WARN, ERR.
+for some simple events -- in this case marginally useful, as it
+listens for logging at level ``INFO``, ``NOTICE``, ``WARN`` and ``ERR``.
 
 .. literalinclude:: ../examples/monitor.py
 
 
 
+Miscellaneous
+-------------
+
 
 .. _stem_relay_descriptor.py:
 
 :file:`stem_relay_descriptor.py`
---------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 :download:`Download the example <../examples/stem_relay_descriptor.py>`.
 
@@ -189,21 +176,10 @@ the details.
 
 
 
-.. _circuit_failure_rates.py:
-
-:file:`circuit_failure_rates.py`
---------------------------------
-
-:download:`Download the example <../examples/circuit_failure_rates.py>`.
-
-.. literalinclude:: ../examples/circuit_failure_rates.py
-
-
-
 .. _txtorcon.tac:
 
 :file:`txtorcon.tac`
---------------------
+~~~~~~~~~~~~~~~~~~~~
 
 :download:`Download the example <../examples/txtorcon.tac>`
 
diff --git a/docs/guide.rst b/docs/guide.rst
new file mode 100644
index 0000000..b93642b
--- /dev/null
+++ b/docs/guide.rst
@@ -0,0 +1,754 @@
+.. _programming_guide:
+
+Programming Guide
+=================
+
+.. contents::
+    :depth: 2
+    :local:
+    :backlinks: none
+
+.. _api_stability:
+
+API Stability
+-------------
+
+In general, any method or class prefixed with an underscore (like
+``_method`` or ``_ClassName``) is private, and the API may change at
+any time. You SHOULD NOT use these. Any method in an interface class
+(which all begin with ``I``, like ``IAnInterface``) are stable, public
+APIs and will maintain backwards-compatibility between releases.
+
+There is **one exception to this** at the moment: the hidden- / onion-
+services APIs are NOT yet considered stable, and may still change
+somewhat.
+
+Any APIs that will go away will first be deprecated for at least one
+major release before being removed.
+
+There are also some attributes which *don't* have underscores but
+really should; these will get "deprecated" via an ``@property``
+decorator so your code will still work.
+
+
+.. _guide_overview:
+
+High Level Overview
+-------------------
+
+Interacting with Tor via txtorcon should involve *only* calling
+methods of the :class:`txtorcon.Tor` class.
+
+You get an instance of :class:`txtorcon.Tor` in one of two ways:
+
+ - call :meth:`txtorcon.connect` or;
+ - call :meth:`txtorcon.launch`
+
+Once you've got a ``Tor`` instance you can use it to gain access to
+(or create) instances of the other interesting classes; see "A Tor
+Instance" below for various use-cases.
+
+Note that for historical reasons (namely: ``Tor`` is a relatively new
+class) there are many other functions and classes exported from
+txtorcon but you *shouldn't* need to instantiate these directly. If
+something is missing from this top-level class, please get in touch
+(file a bug, chat on IRC, etc) because it's probably a missing
+feature.
+
+
+.. _guide_tor_instance:
+
+A Tor Instance
+--------------
+
+You will need a connection to a Tor instance for txtorcon to
+control. This can be either an already-running Tor that you're
+authorized to connect to, or a Tor instance that has been freshly
+launched by txtorcon.
+
+We abstract "a Tor instance" behind the :class:`txtorcon.Tor` class,
+which provides a very high-level API for all the other things you
+might want to do:
+
+ - make client-type connections over tor (see ":ref:`guide_client_use`");
+ - change its configuration (see ":ref:`guide_configuration`");
+ - monitor its state (see ":ref:`guide_state`");
+ - offer hidden-/onion- services via Tor (see ":ref:`guide_onions`");
+ - create and use custom circuits (see ":ref:`guide_custom_circuits`");
+ - issue low-level commands (see ":ref:`protocol`")
+
+The actual control-protocol connection to tor is abstracted behind
+:class:`txtorcon.TorControlProtocol`. This can usually be ignored by
+most users, but can be useful to issue protocol commands directly,
+listen to raw events, etc.
+
+In general, txtorcon tries to never look at Tor's version and instead
+queries required information directly via the control-protocol (there
+is only one exception to this). So the names of configuration values
+and events may change (or, more typically, expand) depending on what
+version of Tor you're connected to.
+
+
+Connecting to a Running Tor
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tor can listen for control connections on TCP ports or UNIX
+sockets. See ":ref:`configure_tor`" for information on how to
+configure Tor to work with txtorcon. By default, "COOKIE"
+authentication is used; only if that is not available do we try
+password authentication.
+
+To connect, use :meth:`txtorcon.connect` which returns a Deferred that
+will fire with a :class:`txtorcon.Tor` instance. If you need access to
+the :class:`txtorcon.TorControlProtocol` instance, it's available via
+the ``.protocol`` property (there is always exactly one of these per
+:class:`txtorcon.Tor` instance). Similarly, the current configuration
+is available via ``.config``. You can change the configuration by
+updating attributes on this class but it won't take effect until you
+call :meth:`txtorcon.TorConfig.save`.
+
+
+Launching a New Tor
+~~~~~~~~~~~~~~~~~~~
+
+It's also possible to launch your own Tor instance. txtorcon keeps a
+"global" tor available for use by e.g. the ``.global_tor`` endpoint
+factory functions (like
+:func:`txtorcon.TCPHiddenServiceEndpoint.global_tor`). You can access
+it via :func:`txtorcon.get_global_tor`. There is exactly zero or one
+of these *per Python process* that uses ``txtorcon``.
+
+ XXX FIXME the above isn't quite true, as that function existed
+ previously and returned a TorConfig so we need to come up with
+ another name :(
+
+To explicitly launch your own Tor instance, use
+:meth:`txtorcon.launch`. You can pass a couple of minimal options
+(``data_directory`` being recommended). If you need to set other Tor
+options, use ``.config`` to retrieve the :class:`txtorcon.TorConfig`
+instance associated with this tor and change configuration afterwards.
+
+Setting ``data_directory`` gives your Tor instance a place to cache
+its state information which includes the current "consensus"
+document. If you don't set it, txtorcon creates a temporary directly
+(which is deleted when this Tor instance exits). Startup time is
+drammatically improved if Tor already has a recent consensus, so when
+integrating with Tor by launching your own client it's highly
+recommended to specify a ``data_directory`` somewhere sensible
+(e.g. ``~/.config/your_program_name/`` is a popular choice on
+Linux). See `the Tor manual
+<https://www.torproject.org/docs/tor-manual.html.en>`_ under the
+``DataDirectory`` option for more information.
+
+Tor itself will create a missing ``data_directory`` with the correct
+permissions and Tor will also ``chdir`` into its ``DataDirectory``
+when running. For these reasons, txtorcon doesn't try to create the
+``data_directory`` nor do any ``chdir``-ing, and neither should you.
+
+
+.. _guide_style:
+
+A Note On Style
+---------------
+
+Most of txtorcon tends towards "attribute-style access".  The guiding
+principle is that "mere data" that is immediately available will be an
+attribute, whereas things that "take work" or are async (and thus
+return ``Deferred`` s) will be functions. For example,
+:meth:`txtorcon.Router.get_location` is a method because it
+potentially has to ask Tor for the country, whereas
+:attr:`txtorcon.Router.hex_id` is a plain attribute because it's
+always available.
+
+
+.. _guide_configuration:
+
+Tracking and Changing Tor's Configuration
+-----------------------------------------
+
+Instances of the :class:`txtorcon.TorConfig` class represent the
+current, live state of a running Tor. There is a bit of
+attribute-magic to make it possible to simply get and set things
+easily:
+
+.. sourcecode:: python
+
+    tor = launch(..)
+    print("SOCKS ports: {}".format(tor.config.SOCKSPort))
+    tor.config.ControlPort.append(4321)
+    tor.config.save()
+
+**Only when** ``.save()`` is called are any ``SETCONF`` commands
+issued -- and then, all configuration values are sent in a single
+command. All ``TorConfig`` instances subscribe to configuration
+updates from Tor, so **"live state" includes actions by any other
+controllers that may be connected**.
+
+For some configuration items, the order they're sent to Tor
+matters. Sometimes, if you change one config item, you have to set a
+series of related items. TorConfig handles these cases for you -- you
+just manipulate the configuration, and wait for ``.save()`` 's
+``Deferred`` to fire and the running Tor's configuration is updated.
+
+Note there is a tiny window during which the state may appear slightly
+inconsistent if you have multiple ``TorConfig`` instances: after Tor
+has acknowledged a ``SETCONF`` command, but before a separate
+``TorConfig`` instance has gotten all the ``CONF_CHANGED`` events
+(because they're hung up in the networking stack for some
+reason). This shouldn't concern most users. (I'm not even 100% sure
+this is possible; it may be that Tor doesn't send the OK until after
+all the CONF_CHANGED events). In normal use, there should only be a
+single ``TorConfig`` instance for every ``Tor`` instance so this
+shouldn't affect you unless you've created your own ``TorConfig``.
+
+Since :class:`txtorcon.TorConfig` conforms to the Iterator protocol,
+you can easily find all the config-options that Tor supports:
+
+.. sourcecode:: python
+
+    tor = launch(..)
+    for config_key in tor.config:
+        print("{} has value: {}".format(config_key, getattr(tor.config.config_key)))
+
+.. fixme::  why doesn't dir() work; fix it, or mention it here
+
+
+These come from interrogating Tor using ``GETINFO config/names`` and
+so represent the configuration options of the current connected Tor
+process. If the value "isn't set" (i.e. is the default), the value
+from Tor will be ``txtorcon.DEFAULT_VALUE``.
+
+When you set values into ``TorConfig``, they are parsed according to
+control-spec for the different types given to the values, via
+information from ``GETINFO config/names``. So, for example, setting
+``.SOCKSPort`` to a ``"quux"`` won't work. Of course, it would also
+fail the whole ``SETCONF`` command if txtorcon happens to allow some
+values that Tor doesn't. Unfortunately, **for any item that's a
+list**, Tor doesn't tell us anything about each element so they're all
+strings. This means we can't pre-validate them and so some things may
+not fail until you call ``.save()``.
+
+
+.. _guide_state:
+
+Monitor and Change Tor's State
+------------------------------
+
+Instances of :class:`txtorcon.TorState` prepresent a live, interactive
+version of all the relays/routers (:class:`txtorcon.Router`
+instances), all circuits (:class:`txtorcon.Circuit` instances) and
+streams (:class:`txtorcon.Stream` instances) active in the underlying
+Tor instance.
+
+As the ``TorState`` instance has subscribed to various events from
+Tor, the "live" state represents an "as up-to-date as possible"
+view. This includes all other controlers, Tor Browser, etcetera that
+might be interacting with your Tor client.
+
+A ``Tor`` instance doesn't have a ``TorState`` instance by default (it
+can take a few hundred milliseconds to set up) and so one is created
+via the asynchronous method :meth:`txtorcon.Tor.get_state`.
+
+.. note::
+
+    If you need to be **absolutely sure** there's nothing stuck in
+    networking buffers and that your instance is "definitely
+    up-to-date" you can issue a do-nothing command to Tor via
+    :meth:`txtorcon.TorControlProtocol.queue_command` (e.g. ``yield
+    queue_command("GETINFO version")``). Most users shouldn't have to
+    worry about this edge-case. In any case, there could be a new
+    update that Tor decides to issue at any moment.
+
+You can modify the state of Tor in a few simple ways. For example, you
+can call :meth:`txtorcon.Stream.close` or
+:meth:`txtorcon.Circuit.close` to cause a stream or circuit to be
+closed. You can wait for a circuit to become usable with
+:meth:`txtorcon.Circuit.when_built`.
+
+For a lot of the read-only state, you can simply access interesting
+attributes. The relays through which a circuit traverses are in
+``Circuit.path`` (a list of :class:`txtorcon.Router` instances),
+``Circuit.streams`` contains a list of :class:`txtorcon.Stream`
+instances, ``.state`` and ``.purpose`` are strings. ``.time_created``
+returns a `datetime
+<https://docs.python.org/2/library/datetime.html>`_ instance. There
+are also some convenience functions like :meth:`txtorcon.Circuit.age`.
+
+For sending streams over a particular circuit,
+:meth:`txtorcon.Circuit.stream_via` returns an
+`IStreamClientEndpoint`_ implementation that will cause a subsequent
+``.connect()`` on it to go via the given circuit in Tor. A similar
+method (:meth:`txtorcon.Circuit.web_agent`) exists for Web requests.
+
+Listening for certain events to happen can be done by implementing the
+interfaces :class:`txtorcon.interface.IStreamListener` and
+:class:`txtorcon.interface.ICircuitListener`. You can request
+notifications on a Tor-wide basis with
+:meth:`txtorcon.TorState.add_circuit_listener` or
+:meth:`txtorcon.TorState.add_stream_listener`. If you are just
+interested in a single circuit, you can call
+:meth:`txtorcon.Circuit.listen` directly on a ``Circuit`` instance.
+
+The Tor relays are abstracted with :class:`txtorcon.Router`
+instances. Again, these have read-only attributes for interesting
+information, e.g.: ``id_hex``, ``ip``, ``flags`` (a list of strings),
+``bandwidth``, ``policy``, etc. Note that all information in these
+objects is from "microdescriptors". If you're doing a long-running
+iteration over relays, it may be important to remember that the
+collection of routers can change every hour (when a new "consensus"
+from the Directory Authorities is published) which may change the
+underlying collection (e.g. :attr:`txtorcon.TorState.routers_by_hash`)
+over which you're iterating.
+
+Here's a simple sketch that traverses all circuits printing their
+router IDs, and closing each stream and circuit afterwards:
+
+(XXX FIXME test this for realz; can we put it in a "listing"-type
+file?)
+
+.. code-block:: python
+
+    @inlineCallbacks
+    def main(reactor):
+        tor = yield connect(reactor, UNIXClientEndpoint('/var/run/tor/control'))
+        state = yield tor.get_state()
+        for circuit in state.circuits.values():
+            path = '->'.join(map(lambda r: r.id_hex, circuit.streams))
+            print("Circuit {} through {}".format(circuit.id, path))
+            for stream in circuit.streams:
+                print("  Stream {} to {}".format(stream.id, stream.target_host))
+                yield stream.close()
+            yield circuit.close()
+
+
+.. _guide_client_use:
+
+Making Connections Over Tor
+---------------------------
+
+SOCKS5
+~~~~~~
+
+Tor exposes a SOCKS5 interface to make client-type connections over
+the network. There are also a couple of `custom extensions
+<https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt>`_
+Tor provides to do DNS resolution over a Tor circuit (txtorcon
+supports these, too).
+
+All client-side interactions are via instances that implement
+`IStreamClientEndpoint`_. There are several factory functions used to
+create suitable instances.
+
+The recommended API is to acquire a :class:`txtorcon.Tor` instance
+(see ":ref:`guide_tor_instance`") and then call
+:meth:`txtorcon.Tor.create_client_endpoint`. To do DNS lookups (or
+reverse lookups) via a Tor circuit, use
+:meth:`txtorcon.Tor.dns_resolve` and
+:meth:`txtorcon.Tor.dns_resolve_ptr`.
+
+A common use-case is to download a Web resource; you can do so via
+Twisted's built-in ``twisted.web.client`` package, or using the
+friendlier `treq`_ library. In both cases, you need a
+`twisted.web.client.Agent
+<https://twistedmatrix.com/documents/current/api/twisted.web.client.Agent.html>`_
+instance which you can acquire with :meth:`txtorcon.Tor.web_agent` or
+:meth:`txtorcon.Circuit.web_agent`. The latter is used to make the
+request over a specific circuit. Usually, txtorcon will simply use one
+of the available SOCKS ports configured in the Tor it is connected to
+-- if you care which one, you can specify it as the optional
+``_socks_endpoint=`` argument (this starts with an underscore on
+purpose as it's not recommended for "public" use and its semantics
+might change in the future).
+
+.. note::
+
+   Tor supports SOCKS over Unix sockets. So does txtorcon. To take
+   advantage of this, simply pass a valid ``SocksPort`` value for unix
+   sockets (e.g. ``unix:/tmp/foo/socks``) as the ``_socks_endpoint``
+   argument to either ``web_agent()`` call. If this doesn't already
+   exist in the underlying Tor, it will be added. Tor has particular
+   requirements for the directory in which the socket file is
+   (``0700``). We don't have a way (yet?) to auto-discover if the Tor
+   we're connected to can support Unix sockets so the default is to
+   use TCP.
+
+You can also use Twisted's `clientFromString`_ API as txtorcon
+registers a ``tor:`` plugin. This also implies that any Twisted-using
+program that supports configuring endpoint strings gets Tor support
+"for free". For example, passing a string like
+``tor:timaq4ygg2iegci7.onion:80`` to `clientFromString`_ will return
+an endpoint that will connect to txtorcon's hidden-service
+website. Note that these endpoints will use the "global to txtorcon"
+Tor instance (available from :meth:`txtorcon.get_global_tor`). Thus,
+if you want to control *which* tor instance your circuit goes over,
+this is not a suitable API.
+
+There are also lower-level APIs to create
+:class:`txtorcon.TorClientEndpoint` instances directly if you have a
+:class:`txtorcon.TorConfig` instance. These very APIs are used by the
+``Tor`` object mentioned above. If you have a use-case that *requires*
+using this API, I'd be curious to learn why the :class:`txtorcon.Tor`
+methods are un-suitable (as those are the suggested API).
+
+You should expect these APIs to raise SOCKS5 errors, which can all be
+handled by catching the :class:`txtorcon.socks.SocksError` class. If
+you need to work with each specific error (corresponding to the
+`RFC-specified SOCKS5 replies`_), see the ":ref:`socks`" for a list of
+them.
+
+.. _guide_onions:
+
+.. _server_use:
+
+Onion (Hidden) Services
+-----------------------
+
+.. caution::
+
+  The Onion service APIs are not stable and will still change; the
+  following is written to what they *will probably* become but **DO
+  NOT** document the current state of the code.
+
+An "Onion Service" (also called a "Hidden Service") refers to a
+feature of Tor allowing servers (e.g. a Web site) to get additional
+security properties such as: hiding their network location; providing
+end-to-end encryption; self-certifying domain-names; or offering
+authentication. For details of how this works, please read `Tor's
+documentation on Hidden Services
+<https://www.torproject.org/docs/hidden-services.html.en>`_.
+
+For more background, the `RiseUp Onion service best-practices guide
+<https://riseup.net/en/security/network-security/tor/onionservices-best-practices>`_
+is a good read as well.
+
+From an API perspective, here are the parts we care about:
+
+ - each service has a secret, private key (with a corresponding public
+   part):
+    - these keys can be on disk (in the "hidden service directory");
+    - or, they can be "ephemeral" (only in memory);
+ - the "host name" is a hash of the public-key (e.g. ``timaq4ygg2iegci7.onion``);
+ - a "Descriptor" (which tells clients how to connect) must be published;
+ - a service has a list of port-mappings (public -> local)
+    - e.g. ``"80 127.0.0.1:5432"`` says you can contact the service
+      publically on port 80, which Tor will redirect to a daemon
+      running locally on port ``5432``;
+    - note that "Descriptors" don't show this information
+ - services can be "authenticated", which means they have a list of
+   client names for which Tor creates associated keys (``.auth_token``).
+ - Tor has two flavours of service authentication: ``basic`` and
+   ``stealth`` -- there's no API-level difference, but the
+   ``.hostname`` is unique for each client in the ``stealth`` case.
+ - See :ref:`create_onion` for details on how to choose which (if any)
+   authentication method you'd like
+
+To summarize the above in a table format, here are the possible types
+of Onion Service interfaces classes you may interact with (ephemeral
+services don't yet support any authentication).
+
++----------------------------------+--------------------------------------+------------------------+
+|                                  | Keys on disk                         | Keys in memory         |
++==================================+======================================+========================+
+|      **no authentication**       | IFilesystemOnionService              | IOnionService          |
++----------------------------------+--------------------------------------+------------------------+
+| **basic/stealth authentication** | IOnionClients                        |                        |
++----------------------------------+--------------------------------------+------------------------+
+
+Note that it's **up to you to save the private keys** of ephemeral
+services if you want to re-launch them later; the "ephemeral" refers
+to the fact that Tor doesn't persist the private keys -- when Tor
+shuts down, they're gone and there will never be a service at the same
+URI again.
+
+
+Onion Services Endpoints API
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+No matter which kind of service you need, you interact via Twisted's
+`IStreamServerEndpoint`_ interface. There are various txtorcon methods
+(see ":ref:`create_onion`") which return some instance implementing that
+interface. These instances will also implement
+:class:`txtorcon.IProgressProvider` -- which is a hook to register
+listeners which get updates about Tor's launching progress (if we
+started a new Tor) and Descriptor uploading.
+
+Fundamentally, "authenticated" services are different from
+non-authenticated services because they have a list of
+clients. Therefore, there are two different endpoint types:
+
+ - :class:`txtorcon.TCPHiddenServiceEndpoint`
+ - :class:`txtorcon.TCPAuthenticatedHiddenServiceEndpoint`
+
+In either case, the ``listen`` method will return an instance
+implementing `IListeningPort`_. In addition to `IListeningPort`_,
+these instances will implement one of:
+
+ - :class:`txtorcon.IOnionService` or;
+ - :class:`txtorcon.IOnionClients`
+
+The first one corresponds to a non-authenticated service, while the
+latter is authenticated. The latter manages a collection of instances
+by (arbitrary) client names, where each of these instances implements
+:class:`txtorcon.IOnionClient` (and therefore also
+:class:`txtorcon.IOnionService`). Note that the ``.auth_token`` member
+is secret, private data which you need to give to **one** client; this
+information goes in the client's Tor configuration as ``HidServAuth
+onion-address auth-cookie [service-name]``. See `the Tor manual
+<https://www.torproject.org/docs/tor-manual-dev.html.en>`_ for more
+information.
+
+Also note that Tor's API for adding "ephemeral" services doesn't yet
+support any type of authentication (however, it may in the future).
+
+
+.. _create_onion:
+
+Creating Onion Endpoints
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The easiest to use API are methods of :class:`txtorcon.Tor`, which
+allow you to create `IStreamServerEndpoint` instances for the various
+Onion Service types.
+
+Both the main endpoint types have several factory-methods to return
+instances -- so you first must decide whether to use an
+"authenticated" service or not.
+
+ - if you want anyone with e.g. the URL http://timaq4ygg2iegci7.onion
+   to be able to put it in `Tor Browser Bundle
+   <https://www.torproject.org/download/download.html.en>`_ and see a
+   Web site, you **do not want** authentication;
+ - if you want only people with the URL *and* a secret authentication
+   token to see the Web site, you want **basic** authentication (these
+   support many more clients than stealth auth);
+ - if you don't even want anyone to be able to decrypt the descriptor
+   without a unique URL *and* a secret authentication token, you want
+   **stealth** authentication (a lot less scalable; for only "a few"
+   clients).
+
+
+Non-Authenticated Services
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For non-authenticated services, you want to create a
+:class:`txtorcon.TCPHiddenServiceEndpoint` instance.
+
+You can do this via the :meth:`txtorcon.create_onion_service` factory
+function or with :meth:`txtorcon.Tor.create_onion_service`. It's also
+possible to use Twisted's ``serverFromString`` API with the ``onion:``
+prefix. (Thus, any program supporting endpoint strings for
+configuration can use Tor Onion Services with *no code changes*).
+
+If you don't want to manage launching or connecting to Tor yourself,
+you can use one of the three @classmethods on the class, which all
+return a new endpoint instance:
+
+ - :meth:`txtorcon.TCPHiddenSeviceEndpoint.global_tor`: uses a Tor
+   instance launched at most once in this Python process (the
+   underlying :class:`txtorcon.Tor` instance for this is available via
+   :meth:`txtorcon.get_global_tor()` if you need to make manual
+   configuration adjustments);
+
+ - :meth:`txtorcon.TCPHiddenSeviceEndpoint.system_tor`: connects to
+   the control-protocol endpoint you provide (a good choice on Debian
+   would be ``UNIXClientEndpoint('/var/run/tor/control')``);
+
+ - :meth:`txtorcon.TCPHiddenSeviceEndpoint.private_tor`: causes a
+   fresh, private instance of Tor to be launched for this service
+   alone. This uses a tempdir (honoring ``$TMP``) which is deleted
+   upon reactor shutdown or loss of the control connection.
+
+Note that nothing actually "happens" until you call ``.listen()`` on
+the ``IStreamServerEndpoint`` at which point Tor will possibly be
+launched, the Onion Service created, and the descriptor published.
+
+
+Authenticated Services
+~~~~~~~~~~~~~~~~~~~~~~
+
+To use authenticated services, you want to create a
+:class:`txtorcon.TCPAuthenticatedHiddenServiceEndpoint` instance. This
+provides the very same factory methods as for non-authenticatd
+instances, but adds arguments for a list of clients (strings) and an
+authentication method (``"basic"`` or ``"stealth"``).
+
+For completeness, the methods to create authenticated endpoints are:
+
+ - :meth:`txtorcon.Tor.create_authenticated_onion_service()`;
+ - :meth:`txtorcon.create_authenticated_onion_service`;
+ - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.global_tor`
+ - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.system_tor`
+ - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.private_tor`
+
+
+Onion Service Configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you just want to "look at" the configuration of existing onion
+services, they are avaialble via :class:`txtorcon.TorConfig` and the
+``.HiddenServices`` attribute.
+
+This presents a "flattened" version of any authenticated services, so
+that each element in the list of ``.HiddenServices`` is itself at
+least a :class:`txtorcon.IOnionService` (it may also implement other
+interfaces, but every one will implement ``IOnionService``).
+
+You can still set any settable attributes on these objects, and Tor's
+configuration for them will be updated when you call
+:meth:`txtorcon.TorConfig.save` with an **important exception**:
+"ephemeral" services cannot be updated after they're created.
+
+Note that it's possible for other controllers to create ephemeral
+services that your controller can't enumerate.
+
+
+.. _guide_custom_circuits:
+
+Custom Circuits
+---------------
+
+Tor provides a way to let controllers like txtorcon decide which
+streams go on which circuits. Since your Tor client will then be
+acting differently from a "normal" Tor client, it **may become easier
+to de-anonymize you**.
+
+High Level
+~~~~~~~~~~
+
+With that in mind, you may still decide to attach streams to
+circuits. Most often, this means you simply want to make a client
+connection over a particluar circuit. The recommended API uses
+:meth:`txtorcon.Circuit.stream_via` for arbitrary protocols or
+:meth:`txtorcon.Circuit.web_agent` as a convenience for Web
+connections. The latter can be used via `Twisted's Web client
+<https://twistedmatrix.com/documents/current/web/howto/client.html>`_
+or via `treq <https://treq.readthedocs.io/en/latest/>`_ (a
+"requests"-like library for Twisted).
+
+See the following examples:
+
+ - :ref:`web_client.py`
+ - :ref:`web_client_treq.py`
+ - :ref:`web_client_custom_circuit.py`
+
+Note that these APIs mimic :meth:`txtorcon.Tor.stream_via` and
+:meth:`txtorcon.Tor.web_agent` except they use a particular Circuit.
+
+
+Low Level
+~~~~~~~~~
+
+Under the hood of these calls, txtorcon provides a low-level interface
+directly over top of Tor's circuit-attachment API.
+
+This works by:
+
+ - setting ``__LeaveStreamsUnattached 1`` in the Tor's configuration
+ - listening for ``STREAM`` events
+ - telling Tor (via ``ATTACHSTREAM``) what circuit to put each new
+   stream on
+ - (we can also choose to tell Tor "attach this one however you
+   normally would")
+
+This is an asynchronous API (i.e. Tor isn't "asking us" for each
+stream) so arbitrary work can be done on a per-stream basis before
+telling Tor which circuit to use. There are two limitations though:
+
+ - Tor doesn't play nicely with multiple controllers playing the role
+   of attaching circuits. Generally, there's not a good way to know if
+   there's another controller trying to attach streams, but basically the
+   first one to answer "wins".
+ - Tor doesn't currently allow controllers to attach circuits destined
+   for onion-services (even if the circuit is actually suitable and
+   goes to the correct Introduction Point).
+
+In order to do custom stream -> circuit mapping, you call
+:meth:`txtorcon.TorState.set_attacher` with an object implementing
+:class:`txtorcon.interface.IStreamAttacher`. Then every time a new
+stream is detected, txtorcon will call
+:meth:`txtorcon.interface.IStreamAttacher.attach_stream` with the
+:class:`txtorcon.Stream` instance and a list of all available
+circuits. You make an appropriate return.
+
+There can be either no attacher at all or a single attacher
+object. You can "un-set" an attacher by calling ``set_attacher(None)``
+(in which case ``__LeaveStreamsUnattached`` will be set back to 0).
+If you really do need multiple attachers, you can use the utility
+class :class:`txtorcon.attacher.PriorityAttacher` which acts as the
+"top level" one (so you add your multiple attachers to it).
+
+Be aware that txtorcon internally uses this API itself if you've
+*ever* called the "high level" API
+(:meth:`txtorcon.Circuit.stream_via` or
+:meth:`txtorcon.Circuit.web_agent`) and so it is an **error** to set a
+new attacher if there is already an existing attacher.
+
+
+.. _guide_building_circuits:
+
+Building Your Own Circuits
+--------------------------
+
+To re-iterate the warning above, making your own circuits differently
+from how Tor normally does **runs a high risk of de-anonymizing
+you**. That said, you can build custom circuits using txtorcon.
+
+
+Building a Single Circuit
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If your use-case needs just a single circuit, it is probably easiest
+to call :meth:`txtorcon.TorState.build_circuit`. This methods takes a
+list of :class:`txtorcon.Router` instances, which you can get from the
+:class:`txtorcon.TorState` instance by using one of the attributes:
+
+ - ``.all_routers``
+ - ``.routers``
+ - ``.routers_by_name`` or
+ - ``.routers_by_hash``
+
+The last three are all hash-tables. For relays that have the ``Guard``
+flag, you can access the hash-tables ``.guards`` (for **all** of them)
+or ``.entry_guards`` (for just the entry guards configured on this Tor
+client).
+
+If you don't actually care which relays are used, but simply want a
+fresh circuit, you can call :meth:`txtorcon.TorState.build_circuit`
+without any arguments at all which asks Tor to build a new circuit in
+the way it normally would (i.e. respecting your guard nodes etc).
+
+
+.. _circuit_builder:
+
+Building Many Circuits
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. caution::
+
+   This API doesn't exist yet; this is documenting what **may** become
+   a new API in a future version of txtorcon. Please get in touch if
+   you want this now.
+
+If you would like to build many circuits, you'll want an instance that
+implements :class:`txtorcon.ICircuitBuilder` (which is usually simply
+an instance of :class:`txtorcon.CircuitBuilder`). Instances of this
+class can be created by calling one of the factory functions like
+:func:`txtorcon.circuit_builder_fixed_exit`.
+
+XXX what about a "config object" idea, e.g. could have keys:
+
+ - ``guard_selection``: one of ``entry_only`` (use one of the current
+   entry guards) or ``random_guard`` (use any relay with the Guard
+   flag, selected by XXX).
+ - ``middle_selection``: one of ``uniform`` (selected randomly from
+   all relays), ``weighted`` (selected randomly, but weighted by
+   consensus weight -- basically same way as Tor would select).
+
+
+.. _istreamclientendpoint: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html
+.. _istreamserverendpoint: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamServerEndpoint.html
+.. _clientfromstring: http://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html#clientFromString
+.. _serverfromstring: http://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html#serverFromString
+.. _ilisteningport: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IListeningPort.html
+.. _treq: https://github.com/twisted/treq
+.. _`rfc-specified socks5 replies`: https://tools.ietf.org/html/rfc1928#section-6
diff --git a/docs/hacking.rst b/docs/hacking.rst
new file mode 100644
index 0000000..f47e87d
--- /dev/null
+++ b/docs/hacking.rst
@@ -0,0 +1,77 @@
+.. _hacking:
+.. _getting help:
+
+Contributions
+=============
+
+You can help contribute to txtorcon by reporting bugs, sending success
+stories, by adding a feature or fixing a bug. Even asking that "silly"
+question helps me with documentation writing.
+
+
+.. _contact info:
+
+Contact Information
+-------------------
+
+Discussing txtorcon is welcome in the following places:
+
+ - IRC: ``#tor-dev`` on `OFTC <http://www.oftc.net/oftc/>`_ (please
+   prefix lines with ``meejah:`` to get my attention, and be patient
+   for replies).
+ - email: preferably on the `tor-dev
+   <https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-dev>`_
+   list, or see `meejah.ca <https://meejah.ca/contact>`_ for other ways
+   to contact me.
+ - bugs: use `txtorcon's issues
+   <https://github.com/meejah/txtorcon/issues>`_ tracker on GitHub.
+ - `@txtorcon <https://twitter.com/txtorcon>`_ on Twitter (announcements only)
+
+
+Public Key
+----------
+
+You can download my key from a keyserver (`0xC2602803128069A7
+<http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xC2602803128069A7>`_)
+or see :download:`meejah.asc <../meejah.asc>` in the repository. The fingerprint
+is ``9D5A 2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``.
+
+Also available at `https://meejah.ca/meejah.asc <https://meejah.ca/meejah.asc>`_.
+For convenience: ``curl https://meejah.ca/meejah.asc | gpg --import``
+
+
+Pull Requests
+-------------
+
+Yes, please!
+
+If you have a new feature or a bug fix, the very best way is to submit
+a pull-request on GitHub. Since we have 100% coverage, all new lines
+of code should at least be covered by unit-tests. You can also include
+a note about the change in ``docs/releases.rst`` if you like (or I can
+make one up after the merge).
+
+I prefer if you rebase/squash commits into logical chunks. Discussion
+of any implementation details can simply occur on the pull-request
+itself. Force-pushing to the same branch/PR is fine by me if you want
+to re-order commits etcetera (but, it's also fine if you just want to
+push new "fix issues" commits instead).
+
+Some example pull-requests:
+
+  * good discussion + more commits: `PR #150 <https://github.com/meejah/txtorcon/pull/150>`_;
+  * a simple one that was "ready-to-go": `PR #51 <https://github.com/meejah/txtorcon/pull/51>`_.
+
+If you want an easy thing to start with, here are `all issues tagged
+"easy" <https://github.com/meejah/txtorcon/labels/easy_ticket>`_
+
+
+Making a Release
+----------------
+
+Mostly a note-to-self, but here is my release checklist.
+
+.. toctree::
+   :maxdepth: 3
+
+   release-checklist
diff --git a/docs/howtos.rst b/docs/howtos.rst
deleted file mode 100644
index fb723be..0000000
--- a/docs/howtos.rst
+++ /dev/null
@@ -1,138 +0,0 @@
-HOWTOs
-======
-
-Try txtorcon in a Virtualenv
-----------------------------
-
-Setting up txtorcon in a virtualenv is a really easy way to play
-around with it without "messing up" your system site-packages. If
-you're unfamiliar with ``virtualenv``, you can read more `at
-readthedocs <http://virtualenv.readthedocs.org/en/latest/>`_.
-
-.. code-block:: shell-session
-
-   $ virtualenv try_txtorcon
-   $ . ./try_txtorcon/bin/activate
-   $ pip install txtorcon # will install Twisted etc as well
-   $ python try_txtorcon/share/txtorcon/examples/circuit_failure_rates.py
-   # ...
-
-You can also use the above virtualenv to play with ``twistd`` and
-endpoints; see below.
-
-Install txtorcon On Debian
---------------------------
-
-Thanks to work by `Lunar
-<http://qa.debian.org/developer.php?login=lunar@debian.org>`_,
-txtorcon is usually rapidly packaged into Debian. This means that it
-gets into `stretch
-<https://packages.debian.org/stretch/python-txtorcon>`_ fairly quickly,
-and then arrives in `jessie-backports
-<https://packages.debian.org/jessie-backports/python-txtorcon>`_ a
-couple weeks after that. You can see the current status on the `Debian
-QA Page for txtorcon <http://packages.qa.debian.org/t/txtorcon.html>`_
-
-If you're using ``stretch`` (testing), simply:
-
-.. code-block:: shell-session
-
-   $ apt-get install python-txtorcon
-
-If you're using wheezy, it should "just work".  For jessie users,
-you'll probably want to enabled the ``jessie-backports`` repository to
-Apt. There are `instructions on the Debian wiki
-<https://wiki.debian.org/Backports#Adding_the_repository>`_ If you're
-in a hurry, you could try this:
-
-.. code-block:: shell-session
-
-   # echo "deb http://ftp.debian.org/debian jessie-backports main contrib non-free" >> /etc/apt/sources.list
-   # apt-get update
-   # apt-get install -t jessie-backports python-txtorcon
-
-.. _howto-endpoint:
-
-
-Endpoints Enable Tor With Any Twisted Service
----------------------------------------------
-
-.. raw:: html
-
-   <div style="margin-left: 3em;"><script type="text/javascript" src="https://asciinema.org/a/10145.js" id="asciicast-10145" async></script></div>
-
-(or view `directly on asciienma.org <https://asciinema.org/a/10145>`_).
-
-As of v0.10.0, there is full support for :api:`twisted.plugin.IPlugin
-<IPlugin>`-based endpoint parsers. This adds an ``onion:`` prefix to
-the system. (If you're unfamiliar with Twisted's endpoint system,
-`read their high-level documentation
-<http://twistedmatrix.com/documents/current/core/howto/endpoints.html>`_
-first).
-
-So, with txtorcon installed, **any** Twisted program that uses
-:api:`twisted.internet.endpoints.serverFromString <serverFromString>`
-and lets you pass endpoint strings can cause a new or existing
-hidden-service to become available (usually by launching a new Tor
-instance).
-
-Twisted's own `twistd
-<http://twistedmatrix.com/documents/current/core/howto/basics.html#twistd>`_
-provides a Web server out of the box that supports this, so if you
-have a collection of documents in ``~/public_html`` you could make
-these available via a hidden-service like so (once txtorcon is
-installed):
-
-.. code-block:: shell-session
-
-   $ twistd web --port "onion:80" --path ~/public_html
-
-You can look in the ``twistd.log`` file created to determine what the
-hidden-serivce keys are. **You must save them** if you want to
-re-launch this same onion URI later. If you've done that, you can
-(re-)launch a hidden-service with existing keys by adding an argument
-to the string:
-
-.. code-block:: shell-session
-
-   $ ls /srv/seekrit/my_service
-   hostname private_key
-   $ twistd web --port "onion:80:hiddenServiceDir=/srv/seekrit/my_service" --path ~/public_html
-
-To find out your service's hostname and where the private key is
-located, look in the ``twistd.log`` file, which will look something
-like this (trunacted for space):
-
-.. code-block:: shell-session
-
-   ...
-   2014-06-13 23:48:39-0600 [-] Spawning tor process from: /tmp/tortmpkh4bsM
-   2014-06-13 23:48:40-0600 [TorControlProtocol,client] 10% Finishing handshake with directory server
-   ...
-   2014-06-13 23:48:53-0600 [TorControlProtocol,client] 90% Establishing a Tor circuit
-   2014-06-13 23:48:54-0600 [TorControlProtocol,client] 100% Done
-   2014-06-13 23:48:54-0600 [TorControlProtocol,client] Site starting on 48275
-   2014-06-13 23:48:54-0600 [TorControlProtocol,client] Starting factory <twisted.web.server.Site instance at 0x7f1b6753e710>
-   2014-06-13 23:48:54-0600 [TorControlProtocol,client] Started hidden service "rv5gkzutsh2k5bzg.onion" on port 80
-   2014-06-13 23:48:54-0600 [TorControlProtocol,client] Keys are in "/tmp/tortmpoeZJYC".
-
-See :class:`txtorcon.TCPHiddenServiceEndpointParser` for all the
-available options. To test the Web server, you can simply launch with
-a local-only server string, like so:
-
-.. code-block:: shell-session
-
-   $ twistd web --port "tcp:localhost:8080" --path ~/public_html
-   $ curl http://localhost:8080/index.html
-
-If you need more control over the options passed to Tor, you can use
-the existing Python APIs to accomplish any Tor configuration and
-launching you like (or connect to already-running Tor instances).
-
-Although Twisted Matrix themselves don't recommend doing "Web
-development" with Twisted, the Twisted Web server is a robust provider
-of HTTP and HTTPS services. It also supports WSGI so can easily front
-a Python-based Web application (e.g. Django or Flask).
-
-``twistd`` provides several other services as well; see `twistd(1)
-<http://linux.die.net/man/1/twistd>`_ for more information.
diff --git a/docs/index.rst b/docs/index.rst
index daede1d..d0465bd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -4,176 +4,93 @@
 txtorcon
 ========
 
-txtorcon is a `Twisted <https://twistedmatrix.com/>`_-based `Python
-<http://python.org/>`_ asynchronous controller library for `Tor
-<https://www.torproject.org/>`_, following `control-spec
-<https://gitweb.torproject.org/torspec.git/tree/control-spec.txt>`_.
-This would be of interest to anyone wishing to write event-based
-software in Python that talks to (and/or launches) a Tor program.
+- **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion
+- **code**: https://github.com/meejah/txtorcon
+- ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git``
+- .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
+      :target: https://www.travis-ci.org/meejah/txtorcon
 
-You get real-time access to all state in Tor (circuits, streams,
-logging, hidden-services) and utilities to launch or connect to running
-Tor instances (including Tor Browser Bundle).
+  .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.svg
+      :target: https://coveralls.io/r/meejah/txtorcon
 
-There is a `Walkthrough <walkthrough.html>`_ and `HOWTOs <howtos.html>`_.
+  .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master
+      :target: http://codecov.io/github/meejah/txtorcon?branch=master
 
-The main code is around 2300 lines according to ohcount, or about 5600
-lines including tests.
+  .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
+      :target: https://txtorcon.readthedocs.io/en/stable
+      :alt: ReadTheDocs
 
-With txtorcon installed, you can use ``"onion:"`` port/endpoint
-strings with **any endpoint-aware Twisted program**. For example, to use
-Twisted Web to serve your ``~/public_html`` as a hidden service
-(``-n`` *means don't daemonize and log to stdout*):
+  .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest
+      :target: https://txtorcon.readthedocs.io/en/latest
+      :alt: ReadTheDocs
 
-.. code-block:: shell-session
+  .. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
+      :target: https://landscape.io/github/meejah/txtorcon/master
+      :alt: Code Health
 
-    $ twistd -n web --port "onion:80" --path ~/public_html
-    2014-05-30 21:40:23-0600 [-] Log opened.
-    #...truncated
-    2014-05-30 21:41:16-0600 [TorControlProtocol,client] Tor launching: 90% Establishing a Tor circuit
-    2014-05-30 21:41:17-0600 [TorControlProtocol,client] Tor launching: 100% Done
-    2014-05-30 21:41:17-0600 [TorControlProtocol,client] Site starting on 46197
-    2014-05-30 21:41:17-0600 [TorControlProtocol,client] Starting factory <twisted.web.server.Site instance at 0x7f57667d0cb0>
-    2014-05-30 21:41:17-0600 [TorControlProtocol,client] Set up hidden service "2vrrgqtpiaildmsm.onion" on port 80
+.. container:: first_time
 
-There's a `complete demonstration <https://asciinema.org/a/10145>`_ at asciinema.org.
+    If this is your first time exploring txtorcon, please **look at the**
+    :ref:`introduction` **first**. These docs are for version |version|.
 
-Some (other) features and motivating examples:
+.. comment::
 
- - :class:`txtorcon.TorControlProtocol` implements the control-spec protocol (only)
-    - see :ref:`monitor.py` which listens for events (SETEVENT ones)
+    +---------------+---------+---------+
+    |   Twisted     | 15.5.0+ | 16.3.0+ |
+    +===============+=========+=========+
+    |   Python 2.7+ |    ✓    |    ✓    |
+    +---------------+---------+---------+
+    |   Python 3.5+ |    ✓    |    ✓    |
+    +---------------+---------+---------+
+    |   PyPy 5.0.0+ |    ✓    |    ✓    |
+    +---------------+---------+---------+
 
- - :class:`txtorcon.TorState` tracks state for you: all Routers, Streams and Circuits, with listeners
-    - see :ref:`stream_circuit_logger.py` which logs all stream and circuit activity
+Supported and tested platforms: Python 2.7+, Python 3.5+, PyPy 5.0.0+
+using Twisted 15.5.0+, 16.3.0+, or 17.1.0+ (see `travis
+<https://travis-ci.org/meejah/txtorcon>`_).
 
- - :class:`txtorcon.TorConfig` tracks and allows updating of config with attribute-style acccess (including hidden services):
-    - :samp:`print config.ORPort`
-    - :samp:`config.HiddenServices.append(HiddenService(config, '/hidden/service/dir', ['80 127.0.0.1:1234']))`
-    - :samp:`config.SocksPort = 9052`
-    - see :ref:`dump_config.py`
-    - see also :ref:`launch_tor_with_hiddenservice.py`
 
- - helpers to launch new slave Tor instances
-    - use :class:`txtorcon.TCPHiddenServiceEndpoint` and :api:`twisted.internet.endpoints.serverFromString <serverFromString>` if you can
-    - uses TAKEOWNERSHIP and __OwningControllerProcess (killing connection causes Tor to exit)
-    - see :ref:`launch_tor.py`
-    - see :ref:`launch_tor_with_hiddenservice.py`
-
- - :class:`txtorcon.TCPHiddenServiceEndpoint` to simplify hidden service listening into Twisteds endpoint paradigm.
-    - see :ref:`launch_tor_endpoint.py`
-
-
-A slight change to the Echo Server example on the front page of
-`Twisted's Web site <https://twistedmatrix.com/trac>`_ can make it
-appear as a hidden service:
-
-.. code-block:: python
-
-    from __future__ import print_function
-    from twisted.internet import protocol, reactor, endpoints
-
-    class Echo(protocol.Protocol):
-        def dataReceived(self, data):
-            self.transport.write(data)
-
-    class EchoFactory(protocol.Factory):
-        def buildProtocol(self, addr):
-            return Echo()
-
-    endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory()).addCallback(lambda x: print(x.getHost()))
-    reactor.run()
-
-This is just a one-line change. Note there isn't even an "import
-txtorcon" (although it does need to be installed so that Twisted finds
-the ``IPlugin`` that does the parsing).
-
-
-This documentation was generated |today|.
-
-.. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
-    :target: https://www.travis-ci.org/meejah/txtorcon
-
-.. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png
-    :target: https://coveralls.io/r/meejah/txtorcon
-
-.. image:: https://pypip.in/d/txtorcon/badge.png
-    :target: https://pypi.python.org/pypi/txtorcon
-
-
-Getting txtorcon:
------------------
-
-The canonical URI is http://timaq4ygg2iegci7.onion
-Code available at https://github.com/meejah/txtorcon
-
-- meejah at meejah.ca (public key: :download:`meejah.asc <../meejah.asc>`)
-- ``git clone git://github.com/meejah/txtorcon.git``
-- ``pip install txtorcon``
-- Watch an `asciinema demo <http://asciinema.org/a/5654>`_ for an overview.
-
-
-If you're using Debian, txtorcon is now in testing (jessie) and
-`wheezy-backports <http://packages.debian.org/source/wheezy-backports/txtorcon>`_ thanks
-to Lunar::
-
-    echo "deb http://ftp.ca.debian.org/debian/ wheezy-backports main" >> /etc/apt/sources.list
-    apt-get update
-    apt-get install python-txtorcon
+Documentation
+-------------
 
-It also `appears txtorcon is in Gentoo
-<http://packages.gentoo.org/package/net-libs/txtorcon>`_ but I don't
-use Gentoo (if anyone has a shell-snippet that installs it, send a
-pull-request).
+.. toctree::
+   :maxdepth: 3
 
-**Installing the wheel files** requires a recent pip and
-setuptools. At least on Debian, it is important to upgrade setuptools
-*before* pip. This procedure appears to work fine::
+   introduction
+   installing
+   guide
+   examples
+   hacking
 
-   virtualenv foo
-   . foo/bin/activate
-   pip install --upgrade setuptools
-   pip install --upgrade pip
-   pip install path/to/txtorcon-0.9.0-py27-none-any.whl
 
+Official Releases:
+------------------
 
-Known Users:
-------------
+All official releases are tagged in Git, and signed by my key. All official releases on PyPI have a corresponding GPG signature of the build. Please be aware that ``pip`` does *not* check GPG signatures by default; please see `this ticket <https://github.com/pypa/pip/issues/1035>`_ if you care.
 
- - txtorcon received a brief mention `at 29C3 <http://media.ccc.de/browse/congress/2012/29c3-5306-en-the_tor_software_ecosystem_h264.html>`_ starting at 12:20 (or via `youtube <http://youtu.be/yG2-ci95h78?t=12m27s>`_).
- - `carml <https://github.com/meejah/carml>`_ command-line utilities for Tor
- - `APAF <https://github.com/globaleaks/APAF>`_ anonymous Python application framework
- - `OONI <https://ooni.torproject.org/>`_ the Open Observatory of Network Interference
- - `exitaddr <https://github.com/arlolra/exitaddr>`_ scan Tor exit addresses
+The most reliable way to verify you got what I intended is to clone the Git repository, ``git checkout`` a tag and verify its signature. The second-best would be to download a release + tag from PyPI and verify that.
 
 
-Official Releases:
-------------------
-
 .. toctree::
    :maxdepth: 2
 
    releases
 
-Documentation
--------------
-
-.. toctree::
-   :maxdepth: 2
 
-   introduction
-   howtos
-   walkthrough
-   README
-   examples
+API Documentation
+-----------------
 
-API Docs:
----------
+These are the lowest-level documents, directly from the doc-strings in
+the code with some minimal organization; if you're just getting
+started with txtorcon **the** ":ref:`programming_guide`" **is a better
+place to start**.
 
 .. toctree::
    :maxdepth: 3
 
    txtorcon
 
+
 Indices and tables
 ==================
 
diff --git a/docs/installing.rst b/docs/installing.rst
new file mode 100644
index 0000000..5a3453f
--- /dev/null
+++ b/docs/installing.rst
@@ -0,0 +1,213 @@
+.. _installing:
+
+Installing txtorcon
+===================
+
+Latest Release
+--------------
+
+txtorcon is on PyPI and in Debian since `jessie
+<https://packages.debian.org/jessie/python-txtorcon>`_ (thanks to
+Lunar and now `irl
+<https://qa.debian.org/developer.php?login=irl@debian.org>`_!). So,
+one of these should work:
+
+- install latest release: ``pip install txtorcon``
+- Debian or Ubuntu: ``apt-get install python-txtorcon``
+- Watch an `asciinema demo <http://asciinema.org/a/5654>`_ for an overview.
+
+Rendered documentation for the latest release is at
+`txtorcon.readthedocs.org <https://txtorcon.readthedocs.org/en/latest/>`_. What exists for
+release-notes are in ":ref:`releases`".
+
+If you're still using wheezy, ``python-txtorcon`` is also in `wheezy-backports <http://packages.debian.org/source/wheezy-backports/txtorcon>`_. To install, do this as root:
+
+.. sourcecode:: shell-session
+
+    # echo "deb http://ftp.ca.debian.org/debian/ wheezy-backports main" >> /etc/apt/sources.list
+    # apt-get update
+    # apt-get install python-txtorcon
+
+It also `appears txtorcon is in Gentoo
+<http://packages.gentoo.org/package/net-libs/txtorcon>`_ but I don't
+use Gentoo (if anyone has a shell-snippet that installs it, send a
+pull-request). I am told this package also needs a maintainer;
+see XXX.
+
+**Installing the wheel files** requires a recent pip and
+setuptools. At least on Debian, it is important to upgrade setuptools
+*before* pip. This procedure appears to work fine::
+
+   virtualenv foo
+   . foo/bin/activate
+   pip install --upgrade setuptools
+   pip install --upgrade pip
+   pip install path/to/txtorcon-*.whl
+
+
+Compatibility
+-------------
+
+txtorcon runs all tests cleanly under Python2, Python3 and PyPy on:
+
+  -  Debian: "squeeze", "wheezy" and "jessie"
+  -  OS X: 10.4 (naif), 10.8 (lukas lueg), 10.9 (kurt neufeld)
+  -  Fedora 18 (lukas lueg)
+  -  FreeBSD 10 (enrique fynn) (**needed to install "lsof"**)
+  -  RHEL6
+  -  **Reports from other OSes appreciated.**
+
+
+.. _configure_tor:
+
+Tor Configuration
+-----------------
+
+Using Tor's cookie authentication is the most convenient way to
+connect; this proves that your user can read a cookie file written by
+Tor. To enable this, you'll want to have the following options on in
+your ``torrc``::
+
+   CookieAuthentication 1
+   CookieAuthFileGroupReadable 1
+
+Note that "Tor BrowserBundle" is configured this way by default, on
+port 9151.  If you want to use unix sockets to speak to tor (highly
+recommended) add this to your config (Debian is already set up like
+this)::
+
+   ControlSocketsGroupWritable 1
+   ControlSocket /var/run/tor/control
+
+
+Source Code
+-----------
+
+Most people will use the code from https://github.com/meejah/txtorcon
+The canonical URI is http://timaq4ygg2iegci7.onion
+I sign tags with my public key (:download:`meejah.asc <../meejah.asc>`)
+
+- ``git clone https://github.com/meejah/txtorcon.git``
+- ``torsocks git clone git://timaq4ygg2iegci7.onion/meejah/txtorcon.git``
+
+Rendered documentation for the latest release is at `txtorcon.readthedocs.org <https://txtorcon.readthedocs.org/en/latest/>`_.
+
+See :ref:`hacking` if you wish to contribute back to txtorcon :)
+
+
+Development Environment
+-----------------------
+
+I like to set up my Python development like this:
+
+.. code-block:: shell-session
+
+    $ git clone https://github.com/meejah/txtorcon.git
+    $ echo "if you later fork it on github, do this:"
+    $ git remote add -f github git+ssh://git@github.com/<your github handle>/txtorcon.git
+    $ cd txtorcon
+    $ virtualenv venv
+    $ source venv/bin/activate
+    (venv)$ pip install --editable .[dev]  # "dev" adds more deps, like Sphinx
+    (venv)$ make doc
+    (venv)$ make test
+    (venv)$ tox  # run all tests, in all supported configs
+
+You can now edit code in the repository as normal. To submit a patch,
+the easiest way is to "clone" the txtorcon project, then "fork" on
+github and add a remote called "github" with your copy of the code to
+which you can push (``git remote add -f github
+git+ssh://git@github.com/<your github handle>/txtorcon.git``). The
+``-f`` is so you don't have to run ``git fetch`` right after.
+
+Now, you can push a new branch you've made to GitHub with ``git push
+github branch-name`` and then examine it and open a pull-request. This
+will trigger Travis to run the tests, after which coverage will be
+produced (and a bot comments on the pull-request). If you require any
+more changes, the easiest thing to do is just commit them and push
+them. (If you know how, re-basing/re-arranging/squashing etc is nice
+to do too). See :ref:`hacking` for more.
+
+
+Integration Tests
+-----------------
+
+There are a couple of simple integration tests using Docker in the
+``integration/`` directory; these make a ``debootstrap``-built base
+image and then do the test inside containers cloned from this -- no
+trusting ``https://docker.io`` required. See ``integration/README``
+for more information.
+
+If you're on Debian, there's a decent chance running ``make
+txtorcon-tester`` followed by ``make integration`` from the root of
+the checkout will work (the first commands ultimately runs
+``debootstrap`` and some ``apt`` commands besides ``docker`` things).
+
+
+.. _dependencies:
+
+Dependencies / Requirements
+---------------------------
+
+These should have been installed by whichever method you chose above,
+but are listed here for completeness. You can get all the development
+requirements with e.g. ``pip install txtorcon[dev]``.
+
+- `twisted <http://twistedmatrix.com>`_: txtorcon should work with any
+  Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so
+  does txtorcon (if you find something broken on Py3 please file a
+  bug).
+
+- `automat <https://github.com/glyph/automat>`_: "a library for
+  concise, idiomatic Python expression of finite-state automata
+  (particularly deterministic finite-state transducers)."
+
+- `ipaddress <https://docs.python.org/3/library/ipaddress.html>`_: a
+  standard module in Python3, but requires installing the backported
+  package on Python2.
+
+- **dev only**: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to
+  build the documentation. In that case you'll also need something
+  called ``python-repoze.sphinx.autointerface`` (at least in Debian)
+  to build the Interface-derived docs properly.
+
+- **dev only**: `coverage <http://nedbatchelder.com/code/coverage/>`_
+  to run the code-coverage metrics.
+
+- **dev only** `cuv'ner <https://cuvner.readthedocs.io/en/latest/>`_
+  for coverage visualization
+
+- **dev only**: `Tox <https://testrun.org/tox/latest/>`_ to run
+  different library revisions.
+
+- **dev optional**: `GraphViz <http://www.graphviz.org/>`_ is used in the
+  tests (and to generate state-machine diagrams, if you like) but
+  those tests are skipped if "dot" isn't in your path
+
+.. BEGIN_INSTALL
+
+In any case, on a `Debian <http://www.debian.org/>`_ wheezy, squeeze or
+Ubuntu system, this should work (as root):
+
+.. sourcecode:: shell-session
+
+  # apt-get install -y python-setuptools python-twisted python-ipaddress graphviz tor
+  # echo "for development:"
+  # apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage libgeoip-dev
+
+.. END_INSTALL
+
+Using pip this would be:
+
+.. sourcecode:: shell-session 
+
+  $ pip install --user Twisted ipaddress pygeoip
+  $ echo "for development:"
+  $ pip install --user GeoIP Sphinx repoze.sphinx.autointerface coverage
+
+or:
+
+.. sourcecode:: shell-session
+		
+    $ pip install -r requirements.txt
+    $ pip install -r dev-requirements.txt
diff --git a/docs/introduction.rst b/docs/introduction.rst
index def2708..78fba65 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -1,31 +1,111 @@
+.. _introduction:
+
 Introduction
 ============
 
-txtorcon is an implementation of the `control-spec <URL>`_ for `Tor
-<https://www.torproject.org/>`_ using the `Twisted
-<https://twistedmatrix.com/trac/>`_ networking library for `Python
-<http://python.org/>`_.
-
-This would be of interest to anyone wishing to write event-based
-software in Python that talks to a Tor program. Currently, txtorcon is
-capable of:
-
- * maintaining up-to-date state information about Tor (Circuits, Streams and Routers)
- * maintaining current configuration information
- * maintaining representation of Tor's address mappings (with expiry)
- * interrogating initial state of all three of the above
- * listing for and altering stream to circuit mappings
- * building custom circuits
- * Circuit and Stream state listeners
- * uses `GeoIP <https://www.maxmind.com/app/geolitecity>`_ to provide location and ASN information for Routers
- * uses `psutil <http://code.google.com/p/psutil/>`_ (optional) to locate processes creating Streams
- * listening for any Tor EVENT
+txtorcon is an implementation of the `control-spec
+<https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+for `Tor <https://www.torproject.org/projects/projects.html.en>`_
+using the `Twisted <https://twistedmatrix.com/trac/>`_ networking
+library for `Python <http://python.org/>`_.
+
+With txtorcon you can launch tor; connect to already-running tor
+instances; use tor as a client (via SOCKS5); set up services over tor;
+change all aspects of configuration; track live state (active circuits
+and streams, etc); do DNS via Tor; and query other information from
+the tor daemon.
+
+txtorcon would be of interest to anyone wishing to write event-based
+software in Python that uses the Tor network as a client or a service
+(or just wants to display information about a locally running
+tor). Twisted already provides many robust protocol implementations,
+deployment, logging and integration with GTK, Qt and other graphics
+frameworks -- so txtorcon can be used for command-line or GUI
+applications or integrate with long-lived daemons easily.
+
+In fact, due to support for endpoints (adding the ``tor:`` and
+``onion:`` plugins), many Twisted applications can now integrate with
+Tor with **no code changes**. For example, you can use the existing
+Twisted webserver via ``twistd`` to serve your ``~/public_html``
+directory over an onion service:
+
+.. code-block:: shell-session
+
+   $ sudo apt-get install python-txtorcon
+   $ twistd web --port "onion:80" --path ~/public_html
+
+txtorcon strives to provide sane and **safe** defaults. txtorcon is `a
+Tor project <https://www.torproject.org/projects/projects.html.en>`_.
+
+
+.. _features:
+
+Features Overview
+-----------------
+
+Currently, txtorcon is capable of:
+
+- making arbitrary client connections to other services over Tor;
+- configuring `twisted.web.client.Agent <https://twistedmatrix.com/documents/current/web/howto/client.html>`_ instances to do Web requests over Tor;
+- doing both of the above over specific circuits;
+- listening as an Onion service;
+- maintaining up-to-date (live) state information about Tor: Circuits, Streams and Routers (relays);
+- maintaining current (live) configuration information;
+- maintaining representation of Tor's address mappings (with expiry);
+- interrogating initial state of all three of the above;
+- listening for and altering stream -> circuit mappings;
+- building custom circuits;
+- Circuit and Stream state listeners;
+- listening for any Tor EVENT;
+- launching and/or controlling a Tor instance (including Tor Browser Bundle);
+- complete Twisted endpoint support (both "onion"/server side and
+  client-side). This means you may be able to use *existing* Twisted
+  software via Tor with **no code changes**. It also is the preferred
+  way to connect (or listen) in Twisted.
 
 Comments (positive or negative) appreciated. Even better if they come
-with patches.
+with patches 😉
+
+
+Shell-cast Overview
+-------------------
+
+A text-only screencast-type overview of some of txtorcon's features,
+from asciinema.org:
 
 .. role:: raw-html(raw)
    :format: html
 
-:raw-html:`<script type="text/javascript" src="https://asciinema.org/a/5654.js" id="asciicast-5654" async></script>`
+:raw-html:`<script type="text/javascript" src="https://asciinema.org/a/eh2gxfz3rc1ztgapkcol47d6o.js" id="asciicast-eh2gxfz3rc1ztgapkcol47d6o" async></script>`
+
+
+Example Code
+------------
+
+`download <examples/readme.py>`_
+(also `python3 style <examples/readme3.py>`_)
+
+.. literalinclude:: ../examples/readme.py
+
+
+.. _known_users:
+
+Known Users
+-----------
 
+- `magic-wormhole <https://github.com/warner/magic-wormhole>`_ "get things from one computer to another, safely"
+- `Tahoe-LAFS <https://tahoe-lafs.org>`_ a Free and Open encrypted distributed storage system
+- txtorcon received a brief mention `at 29C3 <http://media.ccc.de/browse/congress/2012/29c3-5306-en-the_tor_software_ecosystem_h264.html>`_ starting at 12:20 (or via `youtube <http://youtu.be/yG2-ci95h78?t=12m27s>`_).
+- `carml <https://github.com/meejah/carml>`_ command-line utilities for Tor
+- `foolscap <https://github.com/warner/foolscap/>`_ RPC system inspired by Twisted's built-in "Perspective Broker" package.
+- `bwscanner <https://github.com/TheTorProject/bwscanner>`_ next-gen bandwidth scanner for Tor network
+- `unmessage <https://github.com/AnemoneLabs/unmessage>`_ Privacy enhanced instant messenger
+- `APAF <https://github.com/globaleaks/APAF>`_ anonymous Python application framework
+- `OONI <https://ooni.torproject.org/>`_ the Open Observatory of Network Interference
+- `exitaddr <https://github.com/arlolra/exitaddr>`_ scan Tor exit addresses
+- `txtorhttpproxy <https://github.com/david415/txtorhttpproxy>`_ simple HTTP proxy in Twisted
+- `bulb <https://github.com/arlolra/bulb>`_ Web-based Tor status monitor
+- `onionvpn <https://github.com/david415/onionvpn>`_  "ipv6 to onion service virtual public network adapter"
+- `torperf2 <https://github.com/gsathya/torperf2>`_ new Tor node network performance measurement service
+- `torweb <https://github.com/coffeemakr/torweb>`_ web-based Tor controller/monitor
+- `potator <https://github.com/mixxorz/potator>`_ "A Tor-based Decentralized Virtual Private Network Application"
diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst
index d1df98c..b102ffd 100644
--- a/docs/release-checklist.rst
+++ b/docs/release-checklist.rst
@@ -19,13 +19,14 @@ Release Checklist
    * update heading, date
 
 * on both signing-machine and build-machine shells:
-   * export VERSION=0.16.1
+   * export VERSION=0.19.3
 
-* (if on signing machine) "make dist" and "make dist-sig"
+* (if on signing machine) "make dist" and "make dist-sigs"
    * creates:
      dist/txtorcon-${VERSION}.tar.gz.asc
-     dist/txtorcon-${VERSION}-py2-none-any.whl.asc
+     dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc
    * add the signatures to "signatues/"
+     cp dist/txtorcon-${VERSION}.tar.gz.asc dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc signatures/
    * add ALL FOUR files to dist/ (OR fix twine commands)
 
 * (if not on signing machine) do "make dist"
@@ -35,11 +36,11 @@ Release Checklist
      * gpg --no-version --detach-sign --armor --local-user meejah at meejah.ca txtorcon-${VERSION}.tar.gz
   * copy signatures back to build machine, in dist/
   * double-check that they validate
-     * gpg --verify dist/txtorcon-${VERSION}-py2-none-any.whl
-     * gpg --verify dist/txtorcon-${VERSION}.tar.gz
+     * gpg --verify dist/txtorcon-${VERSION}-py2-none-any.whl.asc
+     * gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc
 
 * generate sha256sum for each:
-     sha256sum dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}-py2-none-any.whl
+     sha256sum dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}-py2.py3-none-any.whl
 
 * copy signature files to <root of dist>/signatures and commit them
   along with the above changes for versions, etc.
@@ -50,7 +51,7 @@ Release Checklist
    * copy-paste release notes, un-rst-format them
    * include above sha256sums
    * clear-sign the announcement
-   * gpg --armor --clearsign -u meejah at meejah.ca path/to/release-announcement-X-Y-Z
+   * gpg --armor --clearsign -u meejah at meejah.ca release-announce-${VERSION}
    * Example boilerplate:
 
            I'm [adjective] to announce txtorcon 0.10.0. This adds
@@ -99,17 +100,20 @@ Release Checklist
 * download both distributions + signatures from hidden-service
    * verify sigs
    * verify sha256sums versus announcement text
-   * verify tag (git tag -v v0.9.2) on machine other than signing-machine
+   * verify tag (git tag --verify v${VERSION}) on machine other than signing-machine
+   * run: ./scripts/download-release-onion.sh
 
 * upload release
    * to PyPI: "make release" (which uses twine so this isn't the same step as "sign the release")
       * make sure BOTH the .tar.gz and .tar.gz.asc (ditto for .whl) are in the dist/ directory first!!)
       * ls dist/txtorcon-${VERSION}*
       * note this depends on a ~/.pypirc file with [server-login] section containing "username:" and "password:"
-   * git push --tags github master
+   * git push origin master
+   * git push origin v${VERSION}
    * to github: use web-upload interface to upload the 4 files (both dists, both signature)
 
 * make announcement
    * post to tor-dev@ the clear-signed release announcement
    * post to twisted-python@ the clear-signed release announcement
+   * tweet as @txtorcon 
    * tell #tor-dev??
diff --git a/docs/releases.rst b/docs/releases.rst
index ac889e7..4b1ddce 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -1,25 +1,107 @@
+.. _releases:
+
 Releases
 ========
 
 There isn't a "release schedule" in any sense. If there is something
 in master your project depends upon, let me know and I'll do a
-release. Starting with v0.8.0 versions are following `semantic
-versioning <http://semver.org/>`_.
-
-release-1.x
------------
+release.
 
-A branch is in development fixing, updating and/or breaking some APIs
-and adding a new object to represent the running tor instance and a
-completel documentation re-write. This is not ready for release quite
-yet, but feedback appreciated, especially on the documentation!
-Rendered docs on `txtorcon.readthedocs <http://txtorcon.readthedocs.io/en/release-1.x/>`_.
+Starting after v0.19.0 versions are following `calendar versioning
+<http://calver.org/>`_ with the major version being the 2-digit
+year. The second digit will be "non-trivial" releases and the third
+will be for bugfix releases.
 
 
 unreleased
 ----------
 
-`git master <https://github.com/meejah/txtorcon>`_ *will likely become v0.18.0*
+`git master <https://github.com/meejah/txtorcon>`_ *will likely become v17.0.0*
+
+
+v0.19.3
+-------
+
+May 24, 2017
+
+ * `txtorcon-0.19.3.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.19.3.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.19.3>`_ (:download:`local-sig </../signatues/txtorcon-0.19.3.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.19.3.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.19.3.tar.gz>`_)
+
+ * Incorrect parsing of SocksPort options (see `Issue 237 <https://github.com/meejah/txtorcon/issues/237>`_)
+
+
+v0.19.2
+-------
+
+May 11, 2017
+
+ * `txtorcon-0.19.2.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.19.2.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.19.2>`_ (:download:`local-sig </../signatues/txtorcon-0.19.2.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.19.2.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.19.2.tar.gz>`_)
+
+ * Work around a bug in `incremental` (see `Issue 233 <https://github.com/meejah/txtorcon/issues/233>`_)
+ * Fix for `Issue 190 <https://github.com/meejah/txtorcon/issues/190>`_ from Felipe Dau.
+ * add :meth:`txtorcon.Circuit.when_built`.
+
+
+v0.19.1
+-------
+
+April 26, 2017
+
+ * `txtorcon-0.19.1.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.19.1.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.19.1>`_ (:download:`local-sig </../signatues/txtorcon-0.19.1.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.19.1.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.19.1.tar.gz>`_)
+
+ * Fix a regression in ``launch_tor``, see `Issue 227 <https://github.com/meejah/txtorcon/issues/227>`_
+
+
+v0.19.0
+-------
+
+April 20, 2017
+
+ * `txtorcon-0.19.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.19.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.19.0>`_ (:download:`local-sig </../signatues/txtorcon-0.19.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.19.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.19.0.tar.gz>`_)
+
+ * Full Python3 support
+ * Drop `txsocksx` and use a custom implementation (this also
+   implements the custom Tor SOCKS5 methods RESOLVE and RESOLVE_PTR
+ * Drop support for older Twisted releases (12, 13 and 14 are no
+   longer supported).
+ * Add a top-level API object, :class:`txtorcon.Tor` that abstracts a
+   running Tor. Instances of this class are created with
+   :meth:`txtorcon.connect` or :meth:`txtorcon.launch`. These
+   instances are intended to be "the" high-level API and most users
+   shouldn't need anything else.
+ * Integrated support for `twisted.web.client.Agent`, baked into
+   :class:`txtorcon.Tor`. This allows simple, straightforward use of
+   treq_ or "raw" `twisted.web.client` for making client-type Web
+   requests via Tor. Automatically handles configuration of SOCKS
+   ports. See :meth:`txtorcon.Tor.web_agent`
+ * new high-level API for putting streams on specific Circuits. This
+   adds :meth:`txtorcon.Circuit.stream_via` and
+   :meth:`txtorcon.Circuit.web_agent` methods that work the same as
+   the "Tor" equivalent methods except they use a specific
+   circuit. This makes :meth:`txtorcon.TorState.set_attacher` the
+   "low-level" / "expert" interface. Most users should only need the
+   new API.
+ * big revamp / re-write of the documentation, including the new
+   `Programming Guide
+   <https://txtorcon.readthedocs.io/en/latest/guide.html>`_
+ * `Issue 203 <https://github.com/meejah/txtorcon/issues/203>`_
+ * new helper: :meth:`txtorcon.Router.get_onionoo_details`_
+ * new helper: :func:`txtorcon.util.create_tbb_web_headers`_
+ * `Issue 72 <https://github.com/meejah/txtorcon/issues/72>`_
+ * `Felipe Dau <https://github.com/felipedau>`_ added specific
+   `SocksError` subclasses for all the available SOCKS5 errors.
+ * (more) Python3 fixes from `rodrigc <https://github.com/rodrigc>`_
+
+.. _Automat: https://github.com/glyph/automat
+.. _treq: https://pypi.python.org/pypi/treq
+
+
+v0.18.0
+-------
+
+January 11, 2017
+
+ * `txtorcon-0.18.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.18.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.18.0>`_ (:download:`local-sig </../signatues/txtorcon-0.18.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.18.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.18.0.tar.gz>`_)
+ * `issue 200 <https://github.com/meejah/txtorcon/issues/200>`_: better feedback if the cookie data can't be read
 
 
 v0.17.0
@@ -81,9 +163,9 @@ v0.15.0
    ``build_timeout_circuit`` method which provides a Deferred that
    callbacks only when the circuit is completely built and errbacks if
    the provided timeout expires. This is useful because
-   :doc:`TorState.build_circuit` callbacks as soon as a Circuit
+   :meth:`txtorcon.TorState.build_circuit` callbacks as soon as a Circuit
    instance can be provided (and then you'd use
-   :doc:`Circuit.when_built` to find out when it's done building).
+   :meth:`txtorcon.Circuit.when_built` to find out when it's done building).
  * new feature from `coffeemakr <https://github.com/coffeemakr>`_
    falling back to password authentication if cookie authentication
    isn't available (or fails, e.g. because the file isn't readable).
@@ -200,10 +282,10 @@ v0.10.0
  * In collaboration with `David Stainton <https://github.com/david415>`_ after a pull-request, we
    have endpoint parser plugins for Twisted! This means code like
    ``serverFromString("onion:80").listen(...)`` is enough to start a
-   service. See the **4-line example** :ref:`hello_darkweb.py`
+   service.
  * The above **also** means that **any** endpoint-using Twisted program can immediately offer its TCP services via Hidden Service with **no code changes**.    For example, using Twisted Web to serve a WSGI web application would be simply: ``twistd web --port onion:80 --wsgi web.app``
  * switch to a slightly-modified `Alabaster Sphinx theme <https://github.com/bitprophet/alabaster>`_
- * added :doc:`howtos` to documentation (see :ref:`howto-endpoint`, with demo "video")
+ * added howtos to documentation
 
 
 v0.9.2
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
deleted file mode 100644
index 5ef0528..0000000
--- a/docs/tutorial.rst
+++ /dev/null
@@ -1,262 +0,0 @@
-txtorcon Tutorial
-=================
-
-.. note::
-
-    **NOT COMPLETE** this is a work-in-progress against an in-development API.
-
-This builds up, step-by-step, a tiny program that uses the main
-features of txtorcon. We will:
-
-  - copy-pasta some Twisted boilerplate;
-  - launch our own Tor instance (or connect to one);
-  - show all the circuits;
-  - add a simple Onion service;
-  - change our Tor's configuration;
-  - shut down the Tor instance;
-
-We concentrate on the highest-level API, the :class:`txtorcon.Tor`
-class. There are lower-level APIs available for everything that's done
-here, but this is the easiest. To see the other available APIs you can
-use the API-docs, some of the HOWTOs and the many :ref:`examples`.
-
-
-Before We Begin
----------------
-
-All the file-listings quoted below are in ``./tutorial/*.py`` for
-convenience.
-
-If you do not yet have a development environment with txtorcon
-installed, please see :ref:`installing`. You can prove your
-environment is working by opening a file called `tutorial0.py` with
-your favourite editor and typing:
-
-Listing :download:`tutorial0.py <../tutorial/tutorial0.py>`:
-
-.. literalinclude:: ../tutorial/tutorial0.py
-
-
-Then, run ``python tutorial0.py`` in a terminal. If everything is set
-up properly, your terminal should show something like this:
-
-.. code-block:: shell-session
-
-    $ python ./tutorial/tutorial0.py
-    it worked!
-    $
-
-If you are having problems, the best thing is to contact me on IRC;
-see :ref:`getting help`.
-
-
-Twisted's main
---------------
-
-If you do not already know how to do event-based programming in
-Twisted, you should learn that first. Start with `Twisted's narrative
-documentation
-<http://twistedmatrix.com/documents/current/core/howto/index.html>`_
-or the very fine `Twisted tutorial
-<http://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/>`_
-by Dave Peticolas.
-
-We use the `task <https://twistedmatrix.com/documents/current/api/twisted.internet.task.html>`_ and `endpoints <https://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html>`_ APIs from Twisted and
-`@inlineCallbacks <http://twistedmatrix.com/documents/current/core/howto/defer-intro.html#inline-callbacks-using-yield>`_ extensively; if you're used to calling
-``reactor.run`` you'll want to read about `task.react
-<http://twistedmatrix.com/documents/current/api/twisted.internet.task.html#react>`_.
-
-In any case, we need some Twisted boilerplate to get a running
-"client"-style program; if any of this looks confusing please see the
-documentation linked above.
-
-Listing :download:`tutorial1.py <../tutorial/tutorial1.py>`:
-
-.. literalinclude:: ../tutorial/tutorial1.py
-
-
-Default Values
---------------
-
-txtorcon strives to have sane, safe defaults. For all APIs if you're
-in doubt about whether to pass an optional argument, it is most likely
-that you can simply allow the default value to be assigned. If you
-think you've found a case where this *isn't* true, please file a bug!
-
-
-Launching (or Connecting to) Tor
---------------------------------
-
-Obviously, txtorcon depends on having a running tor daemon to talk to;
-if you're having trouble with installing and running tor itself, see
-`Tor's documentation
-<https://www.torproject.org/docs/documentation.html.en>`_.
-
-"A running tor" is abstracted in txtorcon with instances of the
-:class:`txtorcon.Tor` class. This can be either a Tor that we
-ourselves launched, or one that was already running that we connect
-to. From there, we can ask this instance to create other useful
-txtorcon objects.
-
-.. note::
-
-    Tor allows multiple control connections, and txtorcon can
-    instantiate multiple "Tor" class instances talking to the same
-    underlying tor daemon. This complicates a few things, but for the
-    most part will work "as expected" from the outside. See the
-    :class:`txtorcon.Tor` API documentation for how shutdown is
-    handled. See :class:`txtorcon.TorState` for a caveat about what
-    "up to date" means.
-
-For simplicity, we will launch our own Tor instance.  So, building on
-the "Twisted boilerplate" from above, we do the simplest possible
-launching (don't panic if this doesn't immediately work; we add more
-information + debugging :ref:`immediately below`):
-
-Listing :download:`tutorial2.py <../tutorial/tutorial2.py>`:
-
-.. literalinclude:: ../tutorial/tutorial2.py
-
-The launching may take a while (e.g. several minutes) depending upon
-your connection. Although the above looks simple, there's a lot going
-on under the hood. Before explaining that, lets at least add three
-very useful things:
-
-  - progress message updates (1);
-  - an explicit ``DataDirectory`` for Tor (2);
-  - Tor's raw stdout output (useful for debugging) (3).
-
-
-.. _immediately below:
-
-Listing :download:`tutorial3.py <../tutorial/tutorial3.py>`:
-
-.. literalinclude:: ../tutorial/tutorial3.py
-
-Now we can at least see what Tor is doing while it launches! Giving an
-explicit ``DataDirectory`` means that txtorcon will no longer delete
-the whole thing when we exit; this saves the Tor directory authorities
-some bandwidth and makes our startup time much shorter. Try running
-the above twice in a row to illustrate this.
-
-You can pass any file-like object to ``stdout=`` and ``stderr=``. See
-:func:`txtorcon.launch_tor` for all options.
-
-Now that we know how to create our :class:`txtorcon.Tor` instance
-we'll want to do something with it. "Something" can be broadly
-categorized as "server-side" or "client-side". Either use may or may
-not include monitoring some aspects of Tor's current state.
-
-In Tor, "service-side" things are called "Onion Services" (or before
-that "Hidden Services"). In Twisted, a server-side endpoint (see
-`Twisted docs
-<http://twistedmatrix.com/documents/current/core/howto/endpoints.html>`_)
-must implement IServerStreamEndpoint_ and a client-side one implements
-IClientStreamEndpoint_.
-
-.. _IServerStreamEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamServerEndpoint.html
-.. _IClientStreamEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html
-
-First, however, we'll look at listening to and changing Tor's state.
-
-
-Protocol, State and Configuration
----------------------------------
-
-Each :class:`txtorcon.Tor` instance contains one instance that
-implements :class:`txtorcon.ITorControlProtocol` (this is currently
-always a :class:`txtorcon.TorControlProtocol` but the interface
-declares the public API).
-
-If you want to queue tor control commands directly or add listeners
-for Tor events directly, you can use this class. However, this is
-pretty "low level" stuff and you should be able to use one of
-:class:`txtorcon.TorState` or :class:`txtorcon.TorConfig`.
-
-These instances are created with :meth:`txtorcon.Tor.create_state` or
-:meth:`txtorcon.Tor.create_config`. It is also possible to create a
-``TorConfig`` instance without a protocol connection -- used when
-launching a fresh Tor instance, for example. (Note that when used in
-this manner, the very same TorConfig instance as passed to ``launch``
-will be returned by ``create_config``).
-
-So, here's an example which shows all current circuits and streams in
-Tor, utilizing :class:`txtorcon.TorState`. Note that this provides
-"live" Tor state -- ``TorState`` subscribes to certain events and
-updates structures accordingly.
-
-Listing :download:`tutorial4.py <../tutorial/tutorial4.py>`:
-
-.. literalinclude:: ../tutorial/tutorial4.py
-
-Some things to notice:
-
- - all of the "accessing data" usage of the API is attribute-access
-   style;
- - "doing things" to data is a method call, often returning Deferred;
- - all the data is "live", so we can keep printing things in a loop
-   and they'll represent current Tor state;
- - no need to yield for each bit of data/state required.
-
-If you're really interested in when circuits appear or disappear (or
-streams), it's much better to use the "listener" API. That means
-implementing a :class:`txtorcon.IStreamListener` (or
-subclassing :class:`txtorcon.StreamListenerMixin`). There
-are similar ones for circuits:
-:class:`txtorcon.ICircuitListener` and
-:class:`txtorcon.CircuitListenerMixin`.
-
-You can add instances implementing these interfaces with
-:meth:`txtorcon.TorState.add_circuit_listener` or
-:meth:`txtorcon.TorState.add_stream_listener`.
-
-Modifying the example to listen for stream events:
-
-Listing :download:`tutorial5.py <../tutorial/tutorial5.py>`:
-
-.. literalinclude:: ../tutorial/tutorial5.py
-
-To make some streams through this launched Tor instance, we need to
-speak SOCKS5 to port 9999, for example with ``curl``::
-
-    curl --socks5-hostname localhost:9999 https://check.torproject.org/api/ip
-
-You'll note that the ``tutorial5.py`` code causes every second stream
-to be immediately closed, which should manifest as an error on the
-``curl`` side for ever other request.
-
-
-Server-Side Endpoints
----------------------
-
-To create an object that implements IServerStreamEndpoint_, you call :meth:`txtorcon.Tor.create_onion_endpoint`. This creates a "disk-based" onion service, where the configuration (i.e. public/private keys) are in a ``HiddenServiceDir`` someone on the filesystem. This is in contrast to :meth:`txtorcon.Tor.create_ephemeral_onion_endpoint` which instead uses the ``ADD_ONION`` API from Tor to create an onion service whose keys are only ever communicated over the Tor control protocol.
-
-In either case, when you call ``.listen()`` on the endpoint, the
-appropriate things will happen such that the Tor we're talking to has
-a new hidden-service added to it. You get back a
-:class:`txtorcon.TorOnionListeningPort` when that Deferred fires
-(shhhh! you're only supposed to know it's a Twisted IListeningPort_!)
-which gives a :class:`txtorcon.TorOnionAddress` back from
-``.getHost``. This is how you gain access to the public-key hash which
-makes up the ``timaq4ygg2iegci7.onion`` address.
-
-It sounds more complex than in practice, where it works naturally with
-things like Twisted's ``Site`` for Web.
-
-Listing :download:`tutorial6.py <../tutorial/tutorial6.py>`:
-
-.. literalinclude:: ../tutorial/tutorial6.py
-
-One thing that happens after the ``.listen()`` call is that a
-"descrpitor" is uploaded to the Tor network, which can take a bit of
-time. For details of how the hidden-service system works overall, see
-Tor's `hidden service protocol
-<https://www.torproject.org/docs/hidden-services.html.en>`_ page.
-
-
-
-**Connecting, instead**: all of this tutorial and any use for the
-:class:`txtorcon.Tor` APIs will work the same with a "launched" or
-"connected to" tor -- so if you prefer, you should be able to follow
-along by calling :meth:`txtorcon.connect` instead of
-:meth:`txtorcon.launch`; see the API documentation for details.
diff --git a/docs/txtorcon-config.rst b/docs/txtorcon-config.rst
index 64428fe..a63e81a 100644
--- a/docs/txtorcon-config.rst
+++ b/docs/txtorcon-config.rst
@@ -1,5 +1,5 @@
-Configuration Classes
-=====================
+Reading and Writing Live Tor Configuration
+==========================================
 
 
 TorConfig
diff --git a/docs/txtorcon-controller.rst b/docs/txtorcon-controller.rst
new file mode 100644
index 0000000..15c166e
--- /dev/null
+++ b/docs/txtorcon-controller.rst
@@ -0,0 +1,24 @@
+High Level API
+==============
+
+This is the recommended API. See the :ref:`programming_guide` for
+"prose" documentation of these (and other) APIs.
+
+
+Tor
+----
+.. autoclass:: txtorcon.Tor
+
+
+connect
+-------
+
+# FIXME why doesn't "txtorcon.connect" work here with automethod??
+
+.. method:: txtorcon.connect
+
+
+launch
+------
+
+.. method:: txtorcon.launch
diff --git a/docs/txtorcon-endpoints.rst b/docs/txtorcon-endpoints.rst
index c8a7326..8afd8f6 100644
--- a/docs/txtorcon-endpoints.rst
+++ b/docs/txtorcon-endpoints.rst
@@ -1,31 +1,39 @@
-Endpoint Related Classes
-========================
+Endpoints and Related Classes
+=============================
 
 TCPHiddenServiceEndpoint
 ------------------------
 
 .. autoclass:: txtorcon.TCPHiddenServiceEndpoint
 
+
+.. autofunction:: txtorcon.get_global_tor
+
+
 TCPHiddenServiceEndpointParser
 ------------------------------
 
 .. autoclass:: txtorcon.TCPHiddenServiceEndpointParser
 
+
 TorOnionAddress
 ---------------
 
 .. autoclass:: txtorcon.TorOnionAddress
 
+
 TorOnionListeningPort
 ---------------------
 
 .. autoclass:: txtorcon.TorOnionListeningPort
 
+
 IProgressProvider
 -----------------
 
 .. autointerface:: txtorcon.IProgressProvider
 
+
 IHiddenService
 --------------
 
diff --git a/docs/txtorcon-launching.rst b/docs/txtorcon-launching.rst
deleted file mode 100644
index c3f9a20..0000000
--- a/docs/txtorcon-launching.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-.. _launching_tor:
-
-Launching Tor
-=============
-
-.. note::
-
-  Please see :class:`txtorcon.TCPHiddenServiceEndpoint` In general, endpoints
-  are an easier way to interact with launching Tor. However, if you do
-  need configuration control, you can do that too and you're in the
-  right spot.
-
-
-get_global_tor
---------------
-
-.. autofunction:: txtorcon.get_global_tor
-
-
-TorProcessProtocol
-------------------
-.. autoclass:: txtorcon.TorProcessProtocol
-
-
-launch_tor
-----------
-.. autofunction:: txtorcon.launch_tor
diff --git a/docs/txtorcon-protocol.rst b/docs/txtorcon-protocol.rst
index aed6176..4f593be 100644
--- a/docs/txtorcon-protocol.rst
+++ b/docs/txtorcon-protocol.rst
@@ -1,9 +1,16 @@
-Protocol and Helper Classes
-===========================
+.. _protocol:
 
-connect
--------
-.. autofunction:: txtorcon.connect
+Low-Level Protocol Classes
+==========================
+
+build_tor_connection
+--------------------
+.. autofunction:: txtorcon.build_tor_connection
+
+
+build_local_tor_connection
+--------------------------
+.. autofunction:: txtorcon.build_local_tor_connection
 
 
 TorControlProtocol
@@ -20,7 +27,3 @@ TorProcessProtocol
 ------------------
 .. autoclass:: txtorcon.TorProcessProtocol
 
-
-TCPHiddenServiceEndpoint
-------------------------
-.. autoclass:: txtorcon.TCPHiddenServiceEndpoint
diff --git a/docs/txtorcon-socks.rst b/docs/txtorcon-socks.rst
new file mode 100644
index 0000000..bfb7f8c
--- /dev/null
+++ b/docs/txtorcon-socks.rst
@@ -0,0 +1,72 @@
+.. _socks:
+
+:mod:`txtorcon.socks` Module
+============================
+
+SOCKS5 Errors
+-------------
+
+SocksError
+~~~~~~~~~~
+.. autoclass:: txtorcon.socks.SocksError
+
+
+GeneralServerFailureError
+~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.GeneralServerFailureError
+
+
+ConnectionNotAllowedError
+~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.ConnectionNotAllowedError
+
+
+NetworkUnreachableError
+~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.NetworkUnreachableError
+
+
+HostUnreachableError
+~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.HostUnreachableError
+
+
+ConnectionRefusedError
+~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.ConnectionRefusedError
+
+
+TtlExpiredError
+~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.TtlExpiredError
+
+
+CommandNotSupportedError
+~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.CommandNotSupportedError
+
+
+AddressTypeNotSupportedError
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: txtorcon.socks.AddressTypeNotSupportedError
+
+
+.. note::
+    The following sections present low-level APIs. If you are able
+    to work with :class:`txtorcon.Tor`'s corresponding high-level
+    APIs, you should do so.
+
+
+resolve
+-------
+.. autofunction:: txtorcon.socks.resolve
+
+
+resolve_ptr
+-----------
+.. autofunction:: txtorcon.socks.resolve_ptr
+
+
+TorSocksEndpoint
+----------------
+.. autoclass:: txtorcon.socks.TorSocksEndpoint
diff --git a/docs/txtorcon-state.rst b/docs/txtorcon-state.rst
index eb4c656..6c60e01 100644
--- a/docs/txtorcon-state.rst
+++ b/docs/txtorcon-state.rst
@@ -1,29 +1,25 @@
-State-Tracking Classes
-======================
+Tracking and Changing Live Tor State
+====================================
 
 .. comment::
 	launch_tor is documented in txtorcon-launching.rst
 
-build_tor_connection
---------------------
-.. autofunction:: txtorcon.build_tor_connection
-
-build_local_tor_connection
---------------------------
-.. autofunction:: txtorcon.build_local_tor_connection
 
 TorState
 --------
 .. autoclass:: txtorcon.TorState
 
+
 Circuit
 -------
 .. autoclass:: txtorcon.Circuit
 
+
 Stream
 ------
 .. autoclass:: txtorcon.Stream
 
+
 Router
 ------
 .. autoclass:: txtorcon.Router
diff --git a/docs/txtorcon.rst b/docs/txtorcon.rst
index 2f9ab6b..afee871 100644
--- a/docs/txtorcon.rst
+++ b/docs/txtorcon.rst
@@ -1,11 +1,20 @@
-txtorcon Package
-================
+.. _api_documentation:
+
+API Documentation
+=================
+
+
+These are the lowest-level documents, directly from the doc-strings in
+the code with some minimal organization; if you're just getting
+started with txtorcon **the** ":ref:`programming_guide`" **is a better
+place to start**.
 
 .. toctree::
-    txtorcon-protocol
+    txtorcon-controller
     txtorcon-state
     txtorcon-config
     txtorcon-endpoints
-    txtorcon-launching
+    txtorcon-protocol
+    txtorcon-socks
     txtorcon-interface
     txtorcon-util
diff --git a/docs/walkthrough.rst b/docs/walkthrough.rst
deleted file mode 100644
index cdf3db0..0000000
--- a/docs/walkthrough.rst
+++ /dev/null
@@ -1,334 +0,0 @@
-Walkthrough
-===========
-
-.. _Twisted: https://twistedmatrix.com/documents/current/
-.. _virtualenv: http://www.virtualenv.org/en/latest/
-
-If this is your first time using a Tor controller library, you're in
-the right spot. I presume at least some `familiarity <http://krondo.com/?page_id=1327>`_
-with Twisted_ and asynchronous programming.
-
-
-What We'll Learn
-----------------
-.. _NEWNYM: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n379
-.. _walkthrough directory: https://github.com/meejah/txtorcon/tree/master/walkthrough
-
-In this tutorial, I will go through several examples building up a
-small program. We will:
-
- * connect to a running Tor;
- * launch our own Tor;
- * change the configuration;
- * get some information from Tor;
- * listen for events;
- * and send a NEWNYM_ signal.
-
-All the code examples are also in the `walkthrough directory`_.
-
-Install txtorcon in a virtualenv
---------------------------------
-
-First we need to be able to ``import txtorcon`` in a Python shell. We
-will accomplish that in a virtualenv_.
-
-.. note:: If you're using Debian or Ubuntu, ``pip install python-txtorcon`` may just work.
-
-To try the latest released version of txtorcon in a virtualenv_ is
-similar to other Python packages::
-
-   virtualenv /tmp/txtorcon-venv
-   /tmp/txtorcon-venv/bin/pip install txtorcon
-   source /tmp/txtorcon-venv/bin/activate
-
-You should now be able to run "import txtorcon" in a python shell, for
-example::
-
-   python -c "import txtorcon"
-
-The above should produce no output. If you got an exception, or
-something else went wrong, read up on virtualenv or ask "meejah" in
-#tor-dev for help.
-
-
-Connect to a Running Tor
-------------------------
-
-If you've got a system-wide Tor running, it defaults to port 9051 if
-you have the control interface turned on. ``/etc/tor/torrc`` should
-contain lines similar to this::
-
-   ControlPort 9051
-   CookieAuthentication 1
-
-Alternatively, if you're currently running the Tor Browser Bundle, it
-defaults to a port of 9151 and doesn't turn on cookie
-authentication. Change the options to turn on cookie authentication
-and change "9051" to "9151" in the following examples.
-
-
-We will use the :meth:`txtorcon.build_tor_connection` API call, which
-returns a Deferred that callbacks with a :class:`TorControlProtocol
-<txtorcon.TorControlProtocol>` or :class:`TorState
-<txtorcon.TorState>` instance (depending on whether the
-``build_state`` kwarg was True -- the default -- or False).
-
-The TorState instance takes a second or two to get built as it queries
-Tor for all the current relays and creates a :class:`Router <txtorcon.Router>` instance of
-which there are currently about 5000. TorControlProtocol alone is much
-faster (dozens of milliseconds).
-
-The code to do this would look something like:
-
-.. sourcecode:: python
-
-    from __future__ import print_function
-    from twisted.internet.task import react
-    from twisted.internet.defer import inlineCallbacks
-    from twisted.internet.endpoints import TCP4ClientEndpoint
-    import txtorcon
-
-    @inlineCallbacks
-    def main(reactor):
-        # change the port to 9151 for Tor Browser Bundle
-        connection = TCP4ClientEndpoint(reactor, "localhost", 9051)
-
-        state = yield txtorcon.build_tor_connection(connection)
-        print("Connected to tor {state.protocol.version}".format(state=state))
-        print("Current circuits:")
-        for circ in state.circuits.values():
-            path = '->'.join([r.name for r in circ.path])
-            print("  {circ.id}: {circ.state}, {path}".format(circ=circ, path=path))
-
-        # can also do "low level" things with the protocol
-        proto = state.protocol
-        answer = yield proto.queue_command("GETINFO version")
-        print("GETINFO version: {answer}".format(answer=answer))
-
-    react(main)
-
-If all is well, you should see some output like this::
-
-    python walkthrough/0_connection.py
-    Connected to tor 0.2.5.12 (git-3731dd5c3071dcba)
-    Current circuits:
-      16929: BUILT, someguard->ecrehd->aTomicRelayFR1
-      16930: BUILT, someguard->Ferguson->NLNode1EddaiSu
-    GETINFO version: version=0.2.5.12 (git-3731dd5c3071dcba)
-
-
-Launch Our Own Tor
-------------------
-
-.. _GETINFO: https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt#l444
-.. _mkdtemp: https://docs.python.org/2/library/tempfile.html?highlight=mkdtem#tempfile.mkdtemp
-
-For some use-cases you will want to launch a private Tor
-instance. txtorcon provides :meth:`txtorcon.launch_tor` to do just that. This also
-uses some Tor commands to link the controller to the Tor instance, so
-that if the connection is lost Tor will shut itself down.
-
-The main difference between connecting and launching is that you have
-to provide a configuration to launch Tor with. This is provided via a
-:class:`TorConfig<txtorcon.TorConfig>` instance. This class is a
-little "magic" in order to provide a nice API, and so you simply set
-configuration options as members. A minimal configuration to launch a
-Tor might be::
-
-   config = txtorcon.TorConfig()
-   config.ORPort = 0
-   config.SocksPort = 9999
-
-The ``launch_tor`` method itself also adds several necessary
-configuration options but *only if* they aren't supplied already. For
-example, if you want to maintain state (or hidden service keys)
-between launches, provide your own ``DataDirectory``. The configuration
-keys ``launch_tor`` adds are:
-
- * ``DataDirectory`` a mkdtemp_ directory in ``/tmp/`` (which is deleted at
-   exit, unless it was user-specified)
- * ``ControlPort`` is set to 9052 unless already specified
- * ``CookieAuthentication`` is set to 1
- * ``__OwningControllerProcess`` is set to our PID
-
-Check out the :meth:`txtorcon.launch_tor` documentation. You'll likely want
-to provide a ``progress_updates`` listener to provide interesting
-information to your user. Here's a full example::
-
-    #!/usr/bin/env python
-
-    from __future__ import print_function
-    import os
-    from twisted.internet.defer import inlineCallbacks
-    from twisted.internet.task import react
-    from twisted.internet.endpoints import TCP4ClientEndpoint
-    import txtorcon
-
-    def progress(percent, tag, summary):
-        """
-        Progress update from tor; we print a cheezy progress bar and the
-        message.
-        """
-        ticks = int((percent/100.0) * 10.0)
-        prog = (ticks * '#') + ((10 - ticks) * '.')
-        print('{} {}'.format(prog, summary))
-
-    @inlineCallbacks
-    def main(reactor):
-        config = txtorcon.TorConfig()
-        config.ORPort = 0
-        config.SocksPort = 9998
-        try:
-            os.mkdir('tor-data')
-        except OSError:
-            pass
-        config.DataDirectory = './tor-data'
-
-        try:
-            process = yield txtorcon.launch_tor(
-                config, reactor, progress_updates=progress
-            )
-        except Exception as e:
-            print("Error launching tor:", e)
-            return
-
-        protocol = process.tor_protocol
-        print("Tor has launched.")
-        print("Protocol:", protocol)
-        info = yield protocol.get_info('traffic/read', 'traffic/written')
-        print(info)
-
-        # explicitly stop tor by either disconnecting our protocol or the
-        # Twisted IProcessProtocol (or just exit our program)
-        print("Killing our tor, PID={pid}".format(pid=process.transport.pid))
-        yield process.transport.signalProcess('TERM')
-
-    react(main)
-
-If you've never seen the ``inlineCallbacks`` decorator, then you
-should `read up on it
-<https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#inlineCallbacks>`_.
-Once we get the Tor instance launched, we just make two GETINFO_ calls
-and then explicitly kill it. You can also simply exit, which will
-cause the underlying Tor to also exit.
-
-
-Putting It All Together
------------------------
-
-So, now we've gotten a basic connection to Tor (either by launching
-one or connecting to a running one) and basically done nothing but
-exit.
-
-Let's do something slightly more interesting. We will connect to a
-running Tor (like the first example), issue the NEWNYM_ signal (which
-tells Tor to no longer use any existing circuits for new connections)
-and then continuously monitor two events: circuit events via
-``TorState`` interfaces and ``INFO`` messages via a raw
-``add_event_listener``.
-
-First, we add a simple implementation of :class:`txtorcon.ICircuitListener`::
-
-    @implementer(txtorcon.ICircuitListener)
-    class MyCircuitListener(object):
-
-        def circuit_new(self, circuit):
-            print("\n\nnew", circuit)
-
-        def circuit_launched(self, circuit):
-            print("\n\nlaunched", circuit)
-
-        def circuit_extend(self, circuit, router):
-            print("\n\nextend", circuit)
-
-        def circuit_built(self, circuit):
-            print("\n\nbuilt", circuit)
-
-        def circuit_closed(self, circuit, **kw):
-            print("\n\nclosed", circuit, kw)
-
-        def circuit_failed(self, circuit, **kw):
-            print("\n\nfailed", circuit, kw)
-
-Next, to illustrate setting up TorState from a TorControlProtocol
-directly we first make a "bare" protocol connection, and then use a
-TorState classmethod (with the protocol instance) to query Tor's state
-(this instance also adds listeners to stay updated).
-
-Then we use ``TorControlProtocol.signal`` to send a NEWNYM_
-request. After that we create a ``TorState`` instance, print out all
-existing circuits and set up listeners for circuit events (an instance
-of ``MyCircuitListener``) and INFO messages (via our own method).
-
-Note there is a :class:`txtorcon.CircuitListenerMixin`_ class -- and
-similar interfaces for :class:`txtorcon.Stream`_ as well -- which
-makes it easier to write a listener subclass.
-
-Here is the full listing::
-
-    from __future__ import print_function
-    from twisted.internet.task import react
-    from twisted.internet.defer import inlineCallbacks, Deferred
-    from twisted.internet.endpoints import TCP4ClientEndpoint
-    from zope.interface import implementer
-    import txtorcon
-
-
-    @implementer(txtorcon.ICircuitListener)
-    class MyCircuitListener(object):
-
-        def circuit_new(self, circuit):
-            print("new", circuit)
-
-        def circuit_launched(self, circuit):
-            print("launched", circuit)
-
-        def circuit_extend(self, circuit, router):
-            print("extend", circuit)
-
-        def circuit_built(self, circuit):
-            print("built", circuit)
-
-        def circuit_closed(self, circuit, **kw):
-            print("closed", circuit, kw)
-
-        def circuit_failed(self, circuit, **kw):
-            print("failed", circuit, kw)
-
-
-    @inlineCallbacks
-    def main(reactor):
-        # change the port to 9151 for Tor Browser Bundle
-        tor_ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
-        connection = yield txtorcon.build_tor_connection(tor_ep, build_state=False)
-        version = yield connection.get_info('version', 'events/names')
-        print("Connected to Tor {version}".format(**version))
-        print("Events:", version['events/names'])
-
-        print("Building state.")
-        state = yield txtorcon.TorState.from_protocol(connection)
-
-        print("listening for circuit events")
-        state.add_circuit_listener(MyCircuitListener())
-
-        print("Issuing NEWNYM.")
-        yield connection.signal('NEWNYM')
-        print("OK.")
-
-        print("Existing circuits:")
-        for c in state.circuits.values():
-            print(' ', c)
-
-        print("listening for INFO events")
-        def print_info(i):
-            print("INFO:", i)
-        connection.add_event_listener('INFO', print_info)
-
-        done = Deferred()
-        yield done  # never callback()s so infinite loop
-
-    react(main)
-
-If your Tor instance has been dormant for a while, try something like
-``torsocks curl https://www.torprojec.org`` in another termainl so you
-can see some more logging and circuit events.
diff --git a/examples/add_hiddenservice_to_system_tor.py b/examples/add_hiddenservice_to_system_tor.py
deleted file mode 100755
index 94dba17..0000000
--- a/examples/add_hiddenservice_to_system_tor.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python
-
-# This connects to the system Tor (by default on control port 9151)
-# and adds a new hidden service configuration to it.
-
-import os
-import functools
-import shutil
-
-from twisted.internet import reactor, defer
-from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
-from twisted.web import server, resource
-from twisted.internet.task import react
-
-import txtorcon
-
-
-class Simple(resource.Resource):
-    isLeaf = True
-
-    def render_GET(self, request):
-        return "<html>Hello, world! I'm a hidden service!</html>"
-
-
- at defer.inlineCallbacks
-def main(reactor):
-    ep = TCP4ClientEndpoint(reactor, "localhost", 9251)
-    tor_protocol = yield txtorcon.build_tor_connection(ep, build_state=False)
-    print "Connected to Tor"
-
-    hs_public_port = 80
-    hs_port = yield txtorcon.util.available_tcp_port(reactor)
-    hs_string = '%s 127.0.0.1:%d' % (hs_public_port, hs_port)
-    print "Adding ephemeral service", hs_string
-    print "(this can take some time; please be patient)"
-    hs = txtorcon.EphemeralHiddenService([hs_string])
-    yield hs.add_to_tor(tor_protocol)
-    print "Added ephemeral HS to Tor:", hs.hostname
-
-    print "Starting site"
-    site = server.Site(Simple())
-    hs_endpoint = TCP4ServerEndpoint(reactor, hs_port, interface='127.0.0.1')
-    yield hs_endpoint.listen(site)
-
-    # in 5 seconds, remove the hidden service -- obviously this is
-    # where you'd do your "real work" or whatever.
-    d = defer.Deferred()
-
-    @defer.inlineCallbacks
-    def remove():
-        print "Removing the hiddenservice. Private key was"
-        print hs.private_key
-        yield hs.remove_from_tor(tor_protocol)
-        d.callback(None)
-    if False:
-        reactor.callLater(5, remove)
-        print "waiting 5 seconds"
-    else:
-        print "waiting forever"
-    yield d
-
-
-react(main)
diff --git a/examples/add_hiddenservice_to_system_tor.py.orig b/examples/add_hiddenservice_to_system_tor.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/attach_streams_by_country.py b/examples/attach_streams_by_country.py
deleted file mode 100755
index 85f4a00..0000000
--- a/examples/attach_streams_by_country.py
+++ /dev/null
@@ -1,223 +0,0 @@
-#!/usr/bin/env python
-
-#
-# This uses a custom txtorcon.IStreamAttacher to force streams to use
-# circuits that exit in the same country (as supplied by GeoIP) and
-# builds such a circuit if one isn't available yet.
-#
-# Note that you can do something very similar to this with Tor's
-# config file as well by setting something like:
-#
-# ExitNodes {us},{ca}
-#
-# ...in your torrc. The above just exits from those countries, not
-# the one in which the Web server is located, however. So, this is a
-# little redundant, but gives you the idea of how to do these sorts
-# of things.
-#
-# Another thing to note is that the DNS lookup is a stream before the
-# name is looked up, so the DNS lookup may occur from whatever stream
-# Tor chose for that (we return None, which causes the attacher to
-# tell Tor to attach that stream itself). This presents a problem for
-# sites which optimize the server they deliver based on DNS -- if you
-# lookup from X you'll get a server near/in X, which for our next
-# step will make "the site" appear to be there.
-#
-# The only "solution" for this would be to do the lookup locally, but
-# that defeats the purpose of Tor.
-#
-
-import random
-
-from twisted.python import log
-from twisted.internet import reactor, defer
-from zope.interface import implements
-
-import txtorcon
-
-
-class MyStreamListener(txtorcon.StreamListenerMixin):
-
-    def stream_new(self, stream):
-        print "new stream:", stream.id, stream.target_host
-
-    def stream_succeeded(self, stream):
-        print "successful stream:", stream.id, stream.target_host
-
-    def stream_attach(self, stream, circuit):
-        print "stream", stream.id, " attached to circuit", circuit.id,
-        print "with path:", '->'.join(map(lambda x: x.location.countrycode,
-                                          circuit.path))
-
-
-class MyAttacher(txtorcon.CircuitListenerMixin):
-    implements(txtorcon.IStreamAttacher)
-
-    def __init__(self, state):
-        # pointer to our TorState object
-        self.state = state
-        # circuits for which we are awaiting completion so we can
-        # finish our attachment to them.
-        self.waiting_circuits = []
-
-    def waiting_on(self, circuit):
-        for (circid, d, stream_cc) in self.waiting_circuits:
-            if circuit.id == circid:
-                return True
-        return False
-
-    def circuit_extend(self, circuit, router):
-        "ICircuitListener"
-        if circuit.purpose != 'GENERAL':
-            return
-        # only output for circuits we're waiting on
-        if self.waiting_on(circuit):
-            path = '->'.join(map(lambda x: x.location.countrycode,
-                                 circuit.path))
-            print "  circuit %d (%s). Path now %s" % (circuit.id,
-                                                      router.id_hex,
-                                                      path)
-
-    def circuit_built(self, circuit):
-        "ICircuitListener"
-        if circuit.purpose != 'GENERAL':
-            return
-
-        path = '->'.join(map(lambda r: r.location.countrycode,
-                             circuit.path))
-        print "circuit built", circuit.id, path
-        for (circid, d, stream_cc) in self.waiting_circuits:
-            if circid == circuit.id:
-                self.waiting_circuits.remove((circid, d, stream_cc))
-                d.callback(circuit)
-
-    def circuit_failed(self, circuit, kw):
-        if self.waiting_on(circuit):
-            print "A circuit we requested", circuit.id,
-            print "has failed. Reason:", kw['REASON']
-
-            circid, d, stream_cc = None, None, None
-            for x in self.waiting_circuits:
-                if x[0] == circuit.id:
-                    circid, d, stream_cc = x
-            if d is None:
-                raise Exception("Expected to find circuit.")
-
-            self.waiting_circuits.remove((circid, d, stream_cc))
-            print "Trying a new circuit build for", circid
-            self.request_circuit_build(stream_cc, d)
-
-    def attach_stream(self, stream, circuits):
-        """
-        IStreamAttacher API
-        """
-        if stream.target_host not in self.state.addrmap.addr:
-            print "No AddrMap entry for", stream.target_host,
-            print "so I don't know where it exits; get Tor to attach stream."
-            return None
-
-        ip = str(self.state.addrmap.addr[stream.target_host].ip)
-        stream_cc = txtorcon.util.NetLocation(ip).countrycode
-        print "Stream to", ip, "exiting in", stream_cc
-
-        if stream_cc is None:
-            # returning None tells TorState to ask Tor to select a
-            # circuit instead
-            print "   unknown country, Tor will assign stream"
-            return None
-
-        for circ in circuits.values():
-            if circ.state != 'BUILT' or circ.purpose != 'GENERAL':
-                continue
-
-            circuit_cc = circ.path[-1].location.countrycode
-            if circuit_cc is None:
-                print "warning: don't know where circuit", circ.id, "exits"
-
-            if circuit_cc == stream_cc:
-                print "  found suitable circuit:", circ
-                return circ
-
-        # if we get here, we haven't found a circuit that exits in
-        # the country GeoIP claims our target server is in, so we
-        # need to build one.
-        print "Didn't find a circuit, building one"
-
-        # we need to return a Deferred which will callback with our
-        # circuit, however built_circuit only callbacks with the
-        # message from Tor saying it heard about our request. So when
-        # that happens, we push our real Deferred into the
-        # waiting_circuits list which will get pop'd at some point
-        # when the circuit_built() listener callback happens.
-
-        d = defer.Deferred()
-        self.request_circuit_build(stream_cc, d)
-        return d
-
-    def request_circuit_build(self, stream_cc, deferred_to_callback):
-        # for exits, we can select from any router that's in the
-        # correct country.
-        last = filter(lambda x: x.location.countrycode == stream_cc,
-                      self.state.routers.values())
-
-        # start with an entry guard, put anything in the middle and
-        # put one of our exits at the end.
-        path = [random.choice(self.state.entry_guards.values()),
-                random.choice(self.state.routers.values()),
-                random.choice(last)]
-
-        print "  requesting a circuit:", '->'.join(map(lambda r:
-                                                       r.location.countrycode,
-                                                       path))
-
-        class AppendWaiting:
-            def __init__(self, attacher, d, stream_cc):
-                self.attacher = attacher
-                self.d = d
-                self.stream_cc = stream_cc
-
-            def __call__(self, circ):
-                """
-                return from build_circuit is a Circuit. However, we
-                want to wait until it is built before we can issue an
-                attach on it and callback to the Deferred we issue
-                here.
-                """
-                print "  my circuit is in progress", circ.id
-                self.attacher.waiting_circuits.append((circ.id, self.d,
-                                                       self.stream_cc))
-
-        d = self.state.build_circuit(path)
-        d.addCallback(AppendWaiting(self, deferred_to_callback, stream_cc))
-        d.addErrback(log.err)
-        return d
-
-
-def do_setup(state):
-    print "Connected to a Tor version", state.protocol.version
-
-    attacher = MyAttacher(state)
-    state.set_attacher(attacher, reactor)
-    state.add_circuit_listener(attacher)
-
-    state.add_stream_listener(MyStreamListener())
-
-    print "Existing state when we connected:"
-    print "Streams:"
-    for s in state.streams.values():
-        print ' ', s
-
-    print
-    print "General-purpose circuits:"
-    for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()):
-        print ' ', c.id, '->'.join(map(lambda x: x.location.countrycode,
-                                       c.path))
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-d = txtorcon.build_local_tor_connection(reactor)
-d.addCallback(do_setup).addErrback(setup_failed)
-reactor.run()
diff --git a/examples/attach_streams_by_country.py.orig b/examples/attach_streams_by_country.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/circuit_failure_rates.py b/examples/circuit_failure_rates.py
deleted file mode 100755
index 2c9c002..0000000
--- a/examples/circuit_failure_rates.py
+++ /dev/null
@@ -1,219 +0,0 @@
-#!/usr/bin/env python
-
-#
-# This example uses ICircuitListener to monitor how many circuits have
-# failed since the monitor started up. If this figure is more than 50%,
-# a warning-level message is logged.
-#
-# Like the :ref:`stream_circuit_logger.py` example, we also log all new
-# circuits.
-#
-
-import functools
-import sys
-import time
-from twisted.internet import reactor, task
-from twisted.python import usage
-import txtorcon
-
-
-class Options(usage.Options):
-    """
-    command-line options we understand
-    """
-
-    optParameters = [
-        ['failed', 'f', 0, 'Starting value for number of failed circuits.',
-         int],
-        ['built', 'b', 0,
-         'Starting value for the total number of built cicuits.', int],
-        ['connect', 'c', None, 'Tor control socket to connect to in '
-         'host:port format, like "localhost:9051" (the default).'],
-        ['delay', 'n', 60, 'Seconds to wait between status updates.', int]]
-
-    def __init__(self):
-        usage.Options.__init__(self)
-        self['guards'] = []
-        self.docs['guard'] = 'Specify the name, built and failed rates ' \
-            'like "SomeTorNode,10,42". Can be specified multiple times.'
-
-    def opt_guard(self, value):
-        name, built, failed = value.split(',')
-        self['guards'].append((name, int(built), int(failed)))
-
-
-class CircuitFailureWatcher(txtorcon.CircuitListenerMixin):
-
-    built_circuits = 0
-    failed_circuits = 0
-    percent = 0.0
-    failed_circuit_ids = []
-    per_guard_built = {}
-    per_guard_failed = {}
-
-    def print_update(self):
-        print time.ctime(reactor.seconds()) + ': ' + self.information()
-
-    def update_percent(self):
-        self.percent = 100.0 * (float(self.failed_circuits) /
-                                float(self.built_circuits +
-                                      self.failed_circuits))
-        if self.percent > 50.0:
-            print 'WARNING: %02.1f percent of all routes' % self.percent
-            print ' have failed: %d failed, %d built' % (self.failed_circuits,
-                                                         self.built_circuits)
-
-    def information(self):
-        rtn = '%02.1f%% of all circuits' % self.percent
-        rtn += 'have failed: %d failed, %d built' % (self.failed_circuits,
-                                                     self.built_circuits)
-        for g in self.per_guard_built.keys():
-            per_guard_percent = 100.0 * (self.per_guard_failed[g] /
-                                         (self.per_guard_built[g] +
-                                          self.per_guard_failed[g]))
-            current = ' '
-            for guard in self.state.entry_guards.values():
-                if g == guard.name or g == guard.id_hex:
-                    current = '*'
-                    break
-            rtn = rtn + '\n %s %s: %d built, %d failed: %02.1f%%' % \
-                (current,
-                 g,
-                 self.per_guard_built[g],
-                 self.per_guard_failed[g],
-                 per_guard_percent)
-        return rtn
-
-    def circuit_built(self, circuit):
-        """ICircuitListener API"""
-        # older tor versions will have empty build_flags
-        if 'ONEHOP_TUNNEL' in circuit.build_flags:
-            return
-
-        if circuit.purpose == 'GENERAL':
-            if len(circuit.path) > 0:
-                if circuit.path[0] not in self.state.entry_guards.values():
-                    print "WEIRD: first circuit hop not in entry guards:",
-                    print circuit, circuit.path, circuit.purpose
-                    return
-
-            self.built_circuits += 1
-            self.update_percent()
-
-            if len(circuit.path) != 3 and len(circuit.path) != 4:
-                print "WEIRD: circuit has odd pathlength:",
-                print circuit, circuit.path
-            try:
-                self.per_guard_built[circuit.path[0].unique_name] += 1.0
-            except KeyError:
-                self.per_guard_built[circuit.path[0].unique_name] = 1.0
-                self.per_guard_failed[circuit.path[0].unique_name] = 0.0
-
-    def circuit_failed(self, circuit, kw):
-        """ICircuitListener API"""
-
-        if kw['REASON'] != 'MEASUREMENT_EXPIRED':
-            return
-
-        # older tor versions will have empty build_flags
-        if 'ONEHOP_TUNNEL' in circuit.build_flags:
-            return
-
-        if circuit.purpose == 'GENERAL':
-            if len(circuit.path) > 1:
-                if circuit.path[0] not in self.state.entry_guards.values():
-                    # note that single-hop circuits are built for various
-                    # internal reasons (and it seems they somtimes use
-                    # GENERAL anyway)
-                    print "WEIRD: first circuit hop not in entry guards:",
-                    print circuit, circuit.path
-                    return
-
-            self.failed_circuits += 1
-            print "failed", circuit.id
-            if circuit.id not in self.failed_circuit_ids:
-                self.failed_circuit_ids.append(circuit.id)
-            else:
-                print "WARNING: duplicate message for", circuit
-
-            if len(circuit.path) > 0:
-                try:
-                    self.per_guard_failed[circuit.path[0].unique_name] += 1.0
-                except KeyError:
-                    self.per_guard_failed[circuit.path[0].unique_name] = 1.0
-                    self.per_guard_built[circuit.path[0].unique_name] = 0.0
-
-            self.update_percent()
-
-
-def setup(options, listener, state):
-    print 'Connected to a Tor version', state.protocol.version,
-    print 'at', state.protocol.transport.addr
-
-    listener.failed_circuits = int(options['failed'])
-    listener.built_circuits = int(options['built'])
-    listener.state = state  # FIXME use ctor (ditto for options, probably)
-    for name, built, failed in options['guards']:
-        listener.per_guard_built[name] = float(built)
-        listener.per_guard_failed[name] = float(failed)
-
-    for circ in filter(lambda x: x.purpose == 'GENERAL',
-                       state.circuits.values()):
-        if circ.state == 'BUILT':
-            listener.circuit_built(circ)
-    state.add_circuit_listener(listener)
-    # print an update every minute
-    task.LoopingCall(listener.print_update).start(options['delay'])
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    print arg
-    reactor.stop()
-
-
-options = Options()
-try:
-    options.parseOptions(sys.argv[1:])
-except usage.UsageError:
-    print "This monitors circuit failure rates on multi-hop PURPOSE_GENERAL circuits only."
-    print "Tor internally uses other circuit types or GENERAL single-hop circuits for"
-    print "internal use and we try to ignore these."
-    print
-    print "Every minute, the summary is printed out. For each entry-guard your Tor is"
-    print "currently using, a separate count and summary is printed."
-    print
-    print "Nothing is saved to disc. If you wish to start again with the same totals"
-    print "as a previous run, use the options below. On exit, a command-line suitable"
-    print "to do this is printed."
-    print
-    print options.getUsage()
-    sys.exit(-1)
-
-
-def on_shutdown(listener, *args):
-    print '\nTo carry on where you left off, run:'
-    print '  %s --failed %d --built %d' % (sys.argv[0],
-                                           listener.failed_circuits,
-                                           listener.built_circuits),
-    for name in listener.per_guard_built.keys():
-        print '--guard %s,%d,%d' % (name, listener.per_guard_built[name],
-                                    listener.per_guard_failed[name]),
-    print
-
-listener = CircuitFailureWatcher()
-
-reactor.addSystemEventTrigger('before', 'shutdown',
-                              functools.partial(on_shutdown, listener))
-
-if options['connect']:
-    host, port = options['connect'].split(':')
-    port = int(port)
-    print 'Connecting to %s:%i...' % (host, port)
-    d = txtorcon.build_local_tor_connection(reactor, host=host, port=port)
-else:
-    d = txtorcon.build_local_tor_connection(reactor)
-d.addCallback(functools.partial(setup, options, listener))
-d.addErrback(setup_failed)
-
-reactor.run()
diff --git a/examples/circuit_failure_rates.py.orig b/examples/circuit_failure_rates.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/circuit_for_next_stream.py b/examples/circuit_for_next_stream.py
deleted file mode 100755
index 66f40ab..0000000
--- a/examples/circuit_for_next_stream.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env python
-
-#
-# This allows you to create a particular circuit, which is then used
-# for the very next (non-Tor-internal) stream created. The use-case
-# here might be something like, "I'm going to connect a long-lived
-# stream in a moment *cough*IRC*cough*, so I'd like a circuit through
-# high-uptime nodes"
-#
-
-import sys
-import functools
-import random
-
-from twisted.python import log
-from twisted.internet import reactor
-from zope.interface import implements
-
-import txtorcon
-
-
-class MyStreamListener(txtorcon.StreamListenerMixin):
-
-    def stream_new(self, stream):
-        print "new stream:", stream.id, stream.target_host
-
-    def stream_succeeded(self, stream):
-        print "successful stream:", stream.id, stream.target_host
-
-
-class MyAttacher(txtorcon.CircuitListenerMixin, txtorcon.StreamListenerMixin):
-    implements(txtorcon.IStreamAttacher)
-
-    def __init__(self, state):
-        self.state = state
-        # the circuit which we will use to attach the next stream to
-        self.circuit = None
-
-    def set_circuit(self, circuit):
-        self.circuit = circuit
-
-    def circuit_built(self, circuit):
-        "ICircuitListener"
-
-        if self.circuit is None:
-            return
-
-        if circuit != self.circuit:
-            return
-
-        print "Circuit built, awaiting next stream."
-
-    def attach_stream(self, stream, circuits):
-        """
-        IStreamAttacher API
-        """
-
-        if self.circuit is not None:
-            print "Attaching", stream, "to", self.circuit
-            return self.circuit
-
-        # let Tor connect this stream how it likes
-        return None
-
-    def stream_attach(self, stream, circuit):
-        print "stream", stream.id, "attached to circuit", circuit.id,
-        print "with path:", '->'.join(map(lambda x: x.location.countrycode,
-                                          circuit.path))
-        if self.circuit is circuit:
-            print "...so we're done."
-            reactor.stop()
-
-
-def do_setup(path, state):
-    print "Connected to a Tor version", state.protocol.version
-
-    attacher = MyAttacher(state)
-    state.set_attacher(attacher, reactor)
-    state.add_circuit_listener(attacher)
-    state.add_stream_listener(attacher)
-
-    print "Existing state when we connected:"
-    print "Streams:"
-    for s in state.streams.values():
-        print ' ', s
-
-    print
-    print "General-purpose circuits:"
-    for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()):
-        path = '->'.join(map(lambda x: x.location.countrycode, c.path))
-        print ' ', c.id, path
-
-    print "Building our Circuit:", path
-    real_path = []
-    try:
-        for name in path:
-            print name
-            if name == 'X':
-                if len(real_path) == 0:
-                    g = random.choice(state.entry_guards.values())
-                    real_path.append(g)
-
-                else:
-                    g = random.choice(state.routers.values())
-                    real_path.append(g)
-
-            else:
-                real_path.append(state.routers[name])
-
-    except KeyError, e:
-        print "Couldn't find router:", e
-        sys.exit(1)
-
-    print "...using routers:", real_path
-    d = state.build_circuit(real_path)
-    d.addCallback(attacher.set_circuit).addErrback(log.err)
-    return d
-
-
-def setup_failed(arg):
-    print "Setup Failed:", arg.getErrorMessage()
-    reactor.stop()
-
-if len(sys.argv) == 1:
-    print "usage: %s router [router] [router] ..." % sys.argv[0]
-    print
-    print "       You may use X for a router name, in which case a random one will"
-    print "       be selected (a random one of your entry guards if its in the first"
-    print "       position)."
-    sys.exit(1)
-
-path = sys.argv[1:]
-
-d = txtorcon.build_local_tor_connection(reactor)
-d.addCallback(functools.partial(do_setup, path)).addErrback(setup_failed)
-reactor.run()
diff --git a/examples/circuit_for_next_stream.py.orig b/examples/circuit_for_next_stream.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/connect.py b/examples/connect.py
new file mode 100644
index 0000000..c7b7473
--- /dev/null
+++ b/examples/connect.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import TCP4ClientEndpoint
+import txtorcon
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+    # or (e.g. on Debian):
+    # ep = UNIXClientEndpoint(reactor, "/var/run/tor/control")
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected to Tor {version}".format(version=tor.protocol.version))
+
+    state = yield tor.create_state()
+    # or:
+    # state = yield txtorcon.TorState.from_protocol(tor.protocol)
+    print("Tor state created. Circuits:")
+    for circuit in state.circuits.values():
+        print("  {circuit.id}: {circuit.path}".format(circuit=circuit))
diff --git a/examples/connect.py.orig b/examples/connect.py.orig
new file mode 100644
index 0000000..c7b7473
--- /dev/null
+++ b/examples/connect.py.orig
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import TCP4ClientEndpoint
+import txtorcon
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+    # or (e.g. on Debian):
+    # ep = UNIXClientEndpoint(reactor, "/var/run/tor/control")
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected to Tor {version}".format(version=tor.protocol.version))
+
+    state = yield tor.create_state()
+    # or:
+    # state = yield txtorcon.TorState.from_protocol(tor.protocol)
+    print("Tor state created. Circuits:")
+    for circuit in state.circuits.values():
+        print("  {circuit.id}: {circuit.path}".format(circuit=circuit))
diff --git a/examples/disallow_streams_by_port.py b/examples/disallow_streams_by_port.py
index 7766c5c..1fe744f 100755
--- a/examples/disallow_streams_by_port.py
+++ b/examples/disallow_streams_by_port.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-
+from __future__ import print_function
 #
 # This uses a very simple custom txtorcon.IStreamAttacher to disallow
 # certain streams based solely on their port; by default it closes
@@ -11,31 +10,40 @@
 #
 
 from twisted.python import log
-from twisted.internet import reactor
-from zope.interface import implements
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.internet.endpoints import clientFromString
+from zope.interface import implementer
 
 import txtorcon
 
 
-def stream_closed(x):
-    print "Stream closed:", x
-
-
+ at implementer(txtorcon.IStreamAttacher)
 class PortFilterAttacher:
-    implements(txtorcon.IStreamAttacher)
 
     def __init__(self, state):
         self.state = state
         self.disallow_ports = [80, 25]
-        print "Disallowing all streams to ports:",
-        print ",".join(map(str, self.disallow_ports))
+        print(
+            "Disallowing all streams to ports: {ports}".format(
+                ports=",".join(map(str, self.disallow_ports)),
+            )
+        )
 
     def attach_stream(self, stream, circuits):
         """
         IStreamAttacher API
         """
+
+        def stream_closed(x):
+            print("Stream closed:", x)
+
         if stream.target_port in self.disallow_ports:
-            print "Disallowing", stream, "to port", stream.target_port
+            print(
+                "Disallowing {stream} to port {stream.target_port}".format(
+                    stream=stream,
+                )
+            )
             d = self.state.close_stream(stream)
             d.addCallback(stream_closed)
             d.addErrback(log.err)
@@ -45,21 +53,18 @@ class PortFilterAttacher:
         return None
 
 
-def do_setup(state):
-    print "Connected to a Tor version", state.protocol.version
+ at react
+ at inlineCallbacks
+def main(reactor):
+    control_ep = clientFromString(reactor, "tcp:localhost:9051")
+    tor = yield txtorcon.connect(reactor, control_ep)
+    print("Connected to a Tor version={version}".format(
+        version=tor.protocol.version,
+    ))
+    state = yield tor.create_state()
+    yield state.set_attacher(PortFilterAttacher(state), reactor)
 
-    state.set_attacher(PortFilterAttacher(), reactor)
-
-    print "Existing streams:"
+    print("Existing streams:")
     for s in state.streams.values():
-        print ' ', s
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-
-d = txtorcon.build_local_tor_connection(reactor)
-d.addCallback(do_setup).addErrback(setup_failed)
-reactor.run()
+        print("  ", s)
+    yield Deferred()
diff --git a/examples/dns_lookups.py b/examples/dns_lookups.py
new file mode 100644
index 0000000..f49dc32
--- /dev/null
+++ b/examples/dns_lookups.py
@@ -0,0 +1,20 @@
+from __future__ import print_function
+
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import clientFromString
+import txtorcon
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    control_ep = clientFromString(reactor, "tcp:localhost:9051")
+    tor = yield txtorcon.connect(reactor, control_ep)
+    for domain in ['torproject.org', 'meejah.ca']:
+        print("Looking up '{}' via Tor".format(domain))
+        ans = yield tor.dns_resolve(domain)
+        print("...got answer: {}".format(ans))
+        print("Doing PTR on {}".format(ans))
+        ans = yield tor.dns_resolve_ptr(ans)
+        print("...got answer: {}".format(ans))
diff --git a/examples/dns_lookups.py.orig b/examples/dns_lookups.py.orig
new file mode 100644
index 0000000..f49dc32
--- /dev/null
+++ b/examples/dns_lookups.py.orig
@@ -0,0 +1,20 @@
+from __future__ import print_function
+
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import clientFromString
+import txtorcon
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    control_ep = clientFromString(reactor, "tcp:localhost:9051")
+    tor = yield txtorcon.connect(reactor, control_ep)
+    for domain in ['torproject.org', 'meejah.ca']:
+        print("Looking up '{}' via Tor".format(domain))
+        ans = yield tor.dns_resolve(domain)
+        print("...got answer: {}".format(ans))
+        print("Doing PTR on {}".format(ans))
+        ans = yield tor.dns_resolve_ptr(ans)
+        print("...got answer: {}".format(ans))
diff --git a/examples/dump_config.py b/examples/dump_config.py
deleted file mode 100755
index 4810011..0000000
--- a/examples/dump_config.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python
-
-# Simple usage example of TorConfig
-
-import sys
-import types
-from twisted.internet import reactor
-from txtorcon import build_local_tor_connection, TorConfig, DEFAULT_VALUE
-
-
-def setup_complete(config):
-    print "Got config"
-    keys = config.config.keys()
-    keys.sort()
-    defaults = []
-    for k in keys:
-        if k == 'HiddenServices':
-            for hs in config.config[k]:
-                for xx in ['dir', 'version', 'authorize_client']:
-                    if getattr(hs, xx):
-                        print 'HiddenService%s %s' % (xx.capitalize(),
-                                                      getattr(hs, xx))
-                for port in hs.ports:
-                    print 'HiddenServicePort', port
-            continue
-
-        v = getattr(config, k)
-        if isinstance(v, types.ListType):
-            for val in v:
-                if val != DEFAULT_VALUE:
-                    print k, val
-
-        elif v == DEFAULT_VALUE:
-            defaults.append(k)
-
-        else:
-            print k, v
-
-    if 'defaults' in sys.argv:
-        print "Set to default value:"
-        for k in defaults:
-            print "# %s" % k
-
-    reactor.stop()
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-
-def bootstrap(c):
-    conf = TorConfig(c)
-    conf.post_bootstrap.addCallback(setup_complete).addErrback(setup_failed)
-    print "Connection is live, bootstrapping state..."
-
-
-d = build_local_tor_connection(reactor, build_state=False,
-                               wait_for_proto=False)
-# do not use addCallbacks() here, in case bootstrap has an error
-d.addCallback(bootstrap).addErrback(setup_failed)
-
-reactor.run()
diff --git a/examples/dump_config.py.orig b/examples/dump_config.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/ephemeral_endpoint.py b/examples/ephemeral_endpoint.py
deleted file mode 100644
index 4d8aa5b..0000000
--- a/examples/ephemeral_endpoint.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from __future__ import print_function
-
-# This connects to the system Tor (by default on control port 9151)
-# and adds a new hidden service configuration to it.
-
-from twisted.internet import defer
-from twisted.internet.endpoints import TCP4ClientEndpoint
-from twisted.web import server, resource
-from twisted.internet.task import react
-
-import txtorcon
-from txtorcon.util import default_control_port
-
-
-# Ephemeral endpoints. You own all the bits.
-securely_stored_keyblob = '''RSA1024:MIICWwIBAAKBgQCsEuaUlvU651/lEl986XfX4QylkQCLhA9Nc19LTTt38oDeHRl3i5VgNsfsXyLrnk2iWapOsc3nmvxMt9vhFFanDB9p/rZTonERnTAw50M7PP4H4E8MDkPm6yZJSES7TPEI9u7WfSdq/HsNk4bsQU9Q3Vndy+hPZtPeGl+rs+3MawIDAQABAoGAGHPDIoBlLs6sWOAIg7almh7X7jsxyaGljwsDEq9R8RSb7XRTJyLFwltmg5dtXfAr9hMp2W745J2olrpV26FJQs4LFQBFawUwytvSV9IanpOew02yjvUQ0zqQUUbuR8rNHhzxJrvfJLDEzCmB8RBb1fE6BcUdv5t8xCu0/BwJdCkCQQDaL1ZJQ4aVHLcqru3IqiAwLsnA62aMNUPOO7twJ4YArX7Q6ZscqOPp8eLLoRzCYpMODcBX7kAOmuHxW8X3AKq [...]
-
-
-class Simple(resource.Resource):
-    isLeaf = True
-
-    def render_GET(self, request):
-        return "<html>Hello, world! I'm a hidden service!</html>"
-
-
- at defer.inlineCallbacks
-def main(reactor):
-    ep = txtorcon.TCPHiddenServiceEndpoint.system_tor(
-        reactor,
-        public_port=8080,
-        control_endpoint=TCP4ClientEndpoint(reactor, 'localhost', default_control_port()),
-        private_key=securely_stored_keyblob,
-        ephemeral=True
-    )
-
-    def on_progress(percent, tag, msg):
-        print('{:3.0f}%: {}'.format(percent, msg))
-    txtorcon.IProgressProvider(ep).add_progress_listener(on_progress)
-
-    print("Starting site")
-    port = yield ep.listen(server.Site(Simple()))
-    host = port.getHost()
-
-    print("Site started. Available at http://{}:{}".format(host.onion_uri, host.onion_port))
-    print("Private key:\n{}".format(host.onion_key))
-
-    # if you have need of the actual IOnionService object (e.g. an
-    # EphemeralHiddenService or FilesystemHiddenService instance) then
-    # you can get it via the port (which itself is a
-    # TorOnionListeningPort instance)
-    service = port.onion_service
-
-    print("Ports:", service.ports)
-    print("Hostname:", service.hostname)
-    print("Private key:", service.private_key)
-
-    # wait forever; obviously you could do other work here
-    d = defer.Deferred()
-    yield d
-
-
-if __name__ == '__main__':
-    react(main)
diff --git a/examples/gui-boom.py b/examples/gui-boom.py
deleted file mode 100644
index 08f46d3..0000000
--- a/examples/gui-boom.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import sys
-
-from gi.repository import Clutter
-
-
-# highlight handler
-def hover(source, event):
-    source.set_background_color(Clutter.Color.new(96, 224, 96, 255))
-
-def unhover(source, event):
-    source.set_background_color(Clutter.Color.new(128, 128, 128, 255))
-
-
-Clutter.init(sys.argv)
-
-# init stage
-stage = Clutter.Stage()
-
-# issue occurs only in fullscreen
-stage.set_fullscreen(True)
-
-stage.set_layout_manager(Clutter.BoxLayout())
-stage.connect("destroy", lambda _: Clutter.main_quit())
-
-# create vertical stripes
-for _ in range(8):
-    actor = Clutter.Actor()
-
-    actor.set_x_expand(True)
-    actor.set_background_color(Clutter.Color.new(128, 128, 128, 255))
-    actor.set_reactive(True)
-    actor.connect("enter-event", hover)
-    actor.connect("leave-event", unhover)
-
-    stage.add_child(actor)
-
-# start the app
-stage.show_all()
-Clutter.main()
diff --git a/examples/gui-cairo.py b/examples/gui-cairo.py
deleted file mode 100644
index e4eec38..0000000
--- a/examples/gui-cairo.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# coding: utf-8
-# fooling around w/ a GUI
-
-# need to boot up GTK first
-try:
-    import pgi
-    pgi.install_as_gi()
-except ImportError:
-    pass
-import gi
-gi.require_version('Gtk', '3.0')
-
-# ...then "really soon" (before any other twisted reactor imports) get
-# the correct reactor
-
-from twisted.internet import gtk3reactor
-gtk3reactor.install()
-
-# normal imports follow
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.task import react
-
-from twisted.internet.endpoints import TCP4ClientEndpoint
-import txtorcon
-
-from gi.repository import Gtk, Gdk
-
-def draw_area(widget, cr, state):
-
-    w, h = widget.get_allocation().width, widget.get_allocation().height
-    cr.set_source_rgb(0, 0, 0)
-    cr.set_line_width(1.0)
-
-    if False:
-        for lng in range(0, 360, 5):
-            for lat in range(-160, 160, 5):
-                x, y = lng * 2, (lat + 160) * 2
-                cr.rectangle(x + 10, y + 10, 6, 6)
-                cr.stroke()
-
-
-    cr.set_source_rgb(0, 0, 0)
-    cr.select_font_face('Source Code Pro')
-    cr.set_font_size(22.0)
-    size = cr.font_extents()
-    height = size[2]
-
-    y = height
-    x = 50
-    for circ in state.circuits.values():
-        cr.move_to(x, y)
-        msg = '{circuit.id}: {path}'.format(
-            circuit=circ,
-            path='→'.join([r.location.countrycode for r in circ.path]),
-        )
-        cr.show_text(msg)
-        y += height
-        x = 50
-
-
-def create_win(tor):
-    win = Gtk.Window()
-    win.connect("delete-event", Gtk.main_quit)
-
-    area = Gtk.DrawingArea()
-    area.set_size_request(320, 240)
-    area.connect('draw', draw_area, tor)
-#    win.connect("motion-notify-event", motion)
-    win.add(area)
-    return win
-
-app = Gtk.Application()
-reactor.registerGApplication(app)
-
- at inlineCallbacks
-def main(reactor):
-    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
-    tor = yield txtorcon.connect(reactor, ep)
-    state = yield tor.create_state()
-
-    win = create_win(state)
-    win.show_all()
-    app.add_window(win)
-
-    def redraw():
-        print("redraw")
-        win.queue_draw()
-        reactor.callLater(1, redraw)
-    redraw()
-
-    yield Deferred()
-react(main)
diff --git a/examples/gui-map.py b/examples/gui-map.py
deleted file mode 100644
index 9b895d4..0000000
--- a/examples/gui-map.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# fooling around w/ a GUI
-
-# need to boot up GTK first
-try:
-    import pgi
-    pgi.install_as_gi()
-except ImportError:
-    pass
-import gi
-gi.require_version('Gtk', '3.0')
-
-# ...then "really soon" (before any other twisted reactor imports) get
-# the correct reactor
-
-from twisted.internet import gtk3reactor
-gtk3reactor.install()
-
-# normal imports follow
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.task import react
-
-from twisted.internet.endpoints import TCP4ClientEndpoint
-import txtorcon
-
-from gi.repository import Gtk
-
-def draw_area(widget, cr):
-    print("ding", widget, cr)
-    for x in dir(widget):
-        print(x)
-    print("ddd", widget.get_size_request())
-    print("XXX", dir(widget.get_allocation()))
-    w, h = widget.get_allocation().width, widget.get_allocation().height
-    cr.set_source_rgb(0, 0, 0)
-    cr.set_line_width(1.0)
-
-    for lng in range(0, 360, 5):
-        for lat in range(-160, 160, 5):
-            x, y = lng * 2, (lat + 160) * 2
-            cr.rectangle(x + 10, y + 10, 6, 6)
-            cr.stroke()
-
-def create_win():
-    win = Gtk.Window()
-    win.connect("delete-event", Gtk.main_quit)
-
-    area = Gtk.DrawingArea()
-    area.set_size_request(320, 240)
-    area.connect('draw', draw_area)
-    win.add(area)
-    return win
-
-app = Gtk.Application()
-reactor.registerGApplication(app)
-
- at inlineCallbacks
-def main(reactor):
-    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
-    tor = yield txtorcon.connect(reactor, ep)
-    win = create_win()
-    win.show_all()
-    app.add_window(win)
-    yield Deferred()
-react(main)
diff --git a/examples/gui.py b/examples/gui.py
deleted file mode 100644
index 7c9082f..0000000
--- a/examples/gui.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# fooling around w/ a GUI
-
-# need to boot up GTK first
-try:
-    import pgi
-    pgi.install_as_gi()
-except ImportError:
-    pass
-import gi
-gi.require_version('Gtk', '3.0')
-
-# ...then "really soon" (before any other twisted reactor imports) get
-# the correct reactor
-
-from twisted.internet import gtk3reactor
-gtk3reactor.install()
-
-# normal imports follow
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.task import react
-
-from twisted.internet.endpoints import TCP4ClientEndpoint
-import txtorcon
-
-from gi.repository import Gtk
-
-def create_win():
-    #win = Gtk.Window()
-    win = Gtk.Dialog()
-    win.connect("delete-event", lambda a, b: reactor.stop())#Gtk.main_quit)
-    win.set_border_width(10)
-    win.set_default_size(320, 220)
-
-    def got_response(dialog, response_code):
-        if response_code == 12:
-            print("Cancelling; Tor not launched")
-            reactor.stop()
-    win.connect("response", got_response)
-
-#    cancel = Gtk.Button.new_with_label("Cancel")
-
-    #vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
-    vbox = win.get_content_area()
-    win.add(vbox)
-    win.add_button('Cancel', 12)
-    progress = Gtk.ProgressBar()
-    progress.set_property('show-text', True)
-    progress.set_show_text(True)
-    progress.set_text(None)
-    label = Gtk.Label("Launching Tor")
-    log = Gtk.Label("")
-
-    messages = []
-    def progress_updates(prog, tag, msg):
-        progress.set_fraction(prog / 100.0)
-        if len(messages) == 0 or msg != messages[0]:
-            messages.insert(0, msg)
-        markup = ''
-        for n, msg in enumerate(messages[:6]):
-            percent = n / 6.0
-            val = '%x' % int(255 * percent)
-            markup += '<span foreground="#{0}{0}{0}">{1}</span>\n'.format(val, msg)
-        #log.set_text('\n'.join(messages))
-        log.set_markup(markup)
-    
-    vbox.pack_start(label, expand=False, fill=False, padding=5)
-    vbox.pack_start(progress, expand=False, fill=False, padding=0)
-    vbox.pack_start(log, expand=False, fill=False, padding=0)
-    vbox.pack_start(Gtk.Label(), expand=True, fill=True, padding=0)
-    return win, progress_updates
-
- at inlineCallbacks
-def main(reactor):
-    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
-    win, prog = create_win()
-    win.show_all()
-    tor = yield txtorcon.launch(reactor, progress_updates=prog)
-    print("tor launched", tor)
-    win.destroy()
-    yield Deferred()
-react(main)
diff --git a/examples/gui2.py b/examples/gui2.py
deleted file mode 100644
index e1c8eb6..0000000
--- a/examples/gui2.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# fooling around w/ a GUI
-
-# need to boot up GTK first
-try:
-    import pgi
-    pgi.install_as_gi()
-except ImportError:
-    pass
-import gi
-gi.require_version('Gtk', '3.0')
-
-# ...then "really soon" (before any other twisted reactor imports) get
-# the correct reactor
-
-from twisted.internet import gtk3reactor
-gtk3reactor.install()
-
-# normal imports follow
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.task import react
-
-from twisted.internet.endpoints import TCP4ClientEndpoint
-import txtorcon
-
-from gi.repository import Gtk
-
-def create_win():
-    win = Gtk.Window()
-    win.connect("delete-event", Gtk.main_quit)
-
-    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
-    win.add(vbox)
-    progress = Gtk.ProgressBar()
-    label = Gtk.Label("no progress yet")
-    vbox.pack_start(progress, True, True, 0)
-    vbox.pack_start(label, True, True, 0)
-    return win
-
-app = Gtk.Application()
-reactor.registerGApplication(app)
-
- at inlineCallbacks
-def main(reactor):
-    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
-    tor = yield txtorcon.connect(reactor, ep)
-#    win = create_main(tor)
-#    win.show_all()
-#    app.add_window(win)
-#    yield Deferred()
-react(main)
diff --git a/examples/hello_darkweb.py b/examples/hello_darkweb.py
deleted file mode 100755
index fa182ac..0000000
--- a/examples/hello_darkweb.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python
-
-# This shows how to leverage the endpoints API to get a new hidden
-# service up and running quickly. You can pass along this API to your
-# users by accepting endpoint strings as per Twisted recommendations.
-#
-# http://twistedmatrix.com/documents/current/core/howto/endpoints.html#maximizing-the-return-on-your-endpoint-investment
-#
-# note that only the progress-updates needs the "import txtorcon" --
-# you do still need it installed so that Twisted finds the endpoint
-# parser plugin but code without knowledge of txtorcon can still
-# launch a Tor instance using it. cool!
-
-from __future__ import print_function
-from twisted.internet import reactor, endpoints
-from twisted.web import server, static
-import txtorcon
-
-res = static.Data("<html>Hello, hidden-service world!</html>", 'text/html')
-ep = endpoints.serverFromString(reactor, "onion:80")
-txtorcon.IProgressProvider(ep).add_progress_listener(lambda p, tag, msg: print(msg))
-ep.listen(server.Site(res)).addCallback(lambda port: print(str(port.getHost()))).addErrback(print)
-
-reactor.run()
diff --git a/examples/hello_darkweb.py.orig b/examples/hello_darkweb.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/hidden-service-systemd.service b/examples/hidden-service-systemd.service
deleted file mode 100644
index 5f3a524..0000000
--- a/examples/hidden-service-systemd.service
+++ /dev/null
@@ -1,35 +0,0 @@
-# see http://twistedmatrix.com/documents/current/core/howto/systemd.html
-# started from that, and changed a few options
-
-[Unit]
-Description=Hidden-Service Web Server
-
-[Service]
-ExecStart=/srv/hiddenservice/venv/bin/twistd \
-    --nodaemon \  # recommended by Twisted
-    --pidfile= \  # systemd doesn't need a PID-file
-    web --port onion:80:hiddenServiceDir=/srv/hiddenservice/venv/hostkeys --path .
-
-WorkingDirectory=/srv/hiddenservice/html
-
-User=nobody
-Group=nobody
-
-Restart=always
-
-[Install]
-WantedBy=multi-user.target
-Wants=network-online.target
-
-# usage, as root:
-#
-# put this in /etc/systemd/system/hidden-service-name.service
-#
-# test:
-# systemctl daemon-reload
-# systemctl start hidden-service-name
-# systemctl status hidden-service-name  # <-- check if it's running
-#
-# enable at boot:
-# systemctl enable hidden-service-name.service
-# ln -s '/etc/systemd/system/hidden-service-name.service' '/etc/systemd/system/multi-user.target.wants/hidden-service-name.service'
diff --git a/examples/hidden_echo.py b/examples/hidden_echo.py
new file mode 100644
index 0000000..3f527bb
--- /dev/null
+++ b/examples/hidden_echo.py
@@ -0,0 +1,36 @@
+from __future__ import print_function
+from twisted.internet import protocol, reactor, endpoints
+
+# like the echo-server example on the front page of
+# https://twistedmatrix.com except this makes a Tor onion-service
+# that's an echo server.
+#
+# Note the *only* difference is the string we give to "serverFromString"!
+
+
+class Echo(protocol.Protocol):
+    def connectionMade(self):
+        print("Connection from {}".format(self.transport.getHost()))
+
+    def dataReceived(self, data):
+        print("echoing: '{}'".format(repr(data)))
+        self.transport.write(data)
+
+
+class EchoFactory(protocol.Factory):
+    def buildProtocol(self, addr):
+        return Echo()
+
+
+print("Starting Tor, and onion service (can take a few minutes)")
+d = endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory())
+
+
+def listening(port):
+    # port is a Twisted IListeningPort
+    print("Listening on: {} port 1234".format(port.getHost()))
+    print("Try: torsocks telnet {} 1234".format(port.getHost().onion_uri))
+
+
+d.addCallback(listening)
+reactor.run()
diff --git a/examples/hidden_echo.py.orig b/examples/hidden_echo.py.orig
new file mode 100644
index 0000000..3f527bb
--- /dev/null
+++ b/examples/hidden_echo.py.orig
@@ -0,0 +1,36 @@
+from __future__ import print_function
+from twisted.internet import protocol, reactor, endpoints
+
+# like the echo-server example on the front page of
+# https://twistedmatrix.com except this makes a Tor onion-service
+# that's an echo server.
+#
+# Note the *only* difference is the string we give to "serverFromString"!
+
+
+class Echo(protocol.Protocol):
+    def connectionMade(self):
+        print("Connection from {}".format(self.transport.getHost()))
+
+    def dataReceived(self, data):
+        print("echoing: '{}'".format(repr(data)))
+        self.transport.write(data)
+
+
+class EchoFactory(protocol.Factory):
+    def buildProtocol(self, addr):
+        return Echo()
+
+
+print("Starting Tor, and onion service (can take a few minutes)")
+d = endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory())
+
+
+def listening(port):
+    # port is a Twisted IListeningPort
+    print("Listening on: {} port 1234".format(port.getHost()))
+    print("Try: torsocks telnet {} 1234".format(port.getHost().onion_uri))
+
+
+d.addCallback(listening)
+reactor.run()
diff --git a/examples/launch_tor.py b/examples/launch_tor.py
index 0c6cf7b..367cd89 100755
--- a/examples/launch_tor.py
+++ b/examples/launch_tor.py
@@ -1,42 +1,60 @@
-#!/usr/bin/env python
+from __future__ import print_function
 
-# Launch a slave Tor by first making a TorConfig object.
+"""
+Launch a private Tor instance.
+"""
 
-from sys import stdout
+import sys
+import txtorcon
+from twisted.web.client import readBody
 from twisted.internet.task import react
 from twisted.internet.defer import inlineCallbacks
-import txtorcon
 
 
+ at react
 @inlineCallbacks
 def main(reactor):
-    config = txtorcon.TorConfig()
-    config.OrPort = 1234
-    config.SocksPort = 9999
-    try:
-        yield txtorcon.launch_tor(config, reactor, stdout=stdout)
-
-    except RuntimeError as e:
-        print "Error:", e
-        return
-
-    proto = config.protocol
-    print "Connected to Tor version", proto.version
-
-    state = yield txtorcon.TorState.from_protocol(proto)
-    print "This Tor has PID", state.tor_pid
-    print "This Tor has the following %d Circuits:" % len(state.circuits)
+    # note that you can pass a few options as kwargs
+    # (e.g. data_directory=, or socks_port= ). For other torrc
+    # changes, see below.
+    tor = yield txtorcon.launch(
+        reactor,
+        data_directory="./tordata",
+        stdout=sys.stdout,
+        socks_port='unix:/tmp/tor2/socks',
+    )
+    # tor = yield txtorcon.connect(
+    #     reactor,
+    #     clientFromString(reactor, "unix:/var/run/tor/control"),
+    # )
+    print("Connected to Tor version '{}'".format(tor.protocol.version))
+
+    config = yield tor.get_config()
+    state = yield tor.create_state()
+    # or state = yield txtorcon.TorState.from_protocol(tor.protocol)
+
+    print("This Tor has PID {}".format(state.tor_pid))
+    print("This Tor has the following {} Circuits:".format(len(state.circuits)))
     for c in state.circuits.values():
-        print c
-
-    print "Changing our config (SOCKSPort=9876)"
-    config.SOCKSPort = 9876
+        print("  {}".format(c))
+
+    endpoint_d = config.socks_endpoint(reactor, u'unix:/tmp/tor2/socks')
+    agent = tor.web_agent(socks_endpoint=endpoint_d)
+    uri = b'https://www.torproject.org'
+    print("Downloading {}".format(uri))
+    resp = yield agent.request(b'GET', uri)
+    print("Response has {} bytes".format(resp.length))
+    body = yield readBody(resp)
+    print("received body ({} bytes)".format(len(body)))
+    print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
+
+    # SOCKSPort is 'really' a list of SOCKS ports in Tor now, so we
+    # have to set it to a list ... :/
+    print("Changing our config (SOCKSPort=9876)")
+    # config.SOCKSPort = ['unix:/tmp/foo/bar']
+    config.SOCKSPort = ['9876']
     yield config.save()
 
-    print "Querying to see it changed:"
-    socksport = yield proto.get_conf("SOCKSPort")
-    print "SOCKSPort", socksport
-
-
-if __name__ == '__main__':
-    react(main)
+    print("Querying to see it changed:")
+    socksport = yield tor.protocol.get_conf("SOCKSPort")
+    print("SOCKSPort", socksport)
diff --git a/examples/launch_tor.py.orig b/examples/launch_tor.py.orig
new file mode 100755
index 0000000..367cd89
--- /dev/null
+++ b/examples/launch_tor.py.orig
@@ -0,0 +1,60 @@
+from __future__ import print_function
+
+"""
+Launch a private Tor instance.
+"""
+
+import sys
+import txtorcon
+from twisted.web.client import readBody
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # note that you can pass a few options as kwargs
+    # (e.g. data_directory=, or socks_port= ). For other torrc
+    # changes, see below.
+    tor = yield txtorcon.launch(
+        reactor,
+        data_directory="./tordata",
+        stdout=sys.stdout,
+        socks_port='unix:/tmp/tor2/socks',
+    )
+    # tor = yield txtorcon.connect(
+    #     reactor,
+    #     clientFromString(reactor, "unix:/var/run/tor/control"),
+    # )
+    print("Connected to Tor version '{}'".format(tor.protocol.version))
+
+    config = yield tor.get_config()
+    state = yield tor.create_state()
+    # or state = yield txtorcon.TorState.from_protocol(tor.protocol)
+
+    print("This Tor has PID {}".format(state.tor_pid))
+    print("This Tor has the following {} Circuits:".format(len(state.circuits)))
+    for c in state.circuits.values():
+        print("  {}".format(c))
+
+    endpoint_d = config.socks_endpoint(reactor, u'unix:/tmp/tor2/socks')
+    agent = tor.web_agent(socks_endpoint=endpoint_d)
+    uri = b'https://www.torproject.org'
+    print("Downloading {}".format(uri))
+    resp = yield agent.request(b'GET', uri)
+    print("Response has {} bytes".format(resp.length))
+    body = yield readBody(resp)
+    print("received body ({} bytes)".format(len(body)))
+    print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
+
+    # SOCKSPort is 'really' a list of SOCKS ports in Tor now, so we
+    # have to set it to a list ... :/
+    print("Changing our config (SOCKSPort=9876)")
+    # config.SOCKSPort = ['unix:/tmp/foo/bar']
+    config.SOCKSPort = ['9876']
+    yield config.save()
+
+    print("Querying to see it changed:")
+    socksport = yield tor.protocol.get_conf("SOCKSPort")
+    print("SOCKSPort", socksport)
diff --git a/examples/launch_tor2web.py b/examples/launch_tor2web.py
index cf13b41..f83e2c3 100644
--- a/examples/launch_tor2web.py
+++ b/examples/launch_tor2web.py
@@ -5,6 +5,8 @@
 # running tor supports, *without* resorting to looking at version
 # numbers.
 
+from __future__ import print_function
+
 import sys
 from twisted.internet.task import react
 from twisted.internet.defer import inlineCallbacks, Deferred
@@ -19,7 +21,7 @@ def main(reactor, tor_binary):
     config.Tor2WebMode = 1
     # leaving ControlPort unset; launch_tor will choose one
 
-    print "Launching tor...", tor_binary
+    print("Launching tor...", tor_binary)
     try:
         yield txtorcon.launch_tor(
             config,
@@ -27,14 +29,14 @@ def main(reactor, tor_binary):
             tor_binary=tor_binary,
             stdout=sys.stdout
         )
-        print "success! We support Tor2Web mode"
+        print("success! We support Tor2Web mode")
 
     except RuntimeError as e:
-        print "There was a problem:", str(e)
-        print "We do NOT support Tor2Web mode"
+        print("There was a problem:", str(e))
+        print("We do NOT support Tor2Web mode")
         return
 
-    print "quitting in 5 seconds"
+    print("quitting in 5 seconds")
     reactor.callLater(5, lambda: reactor.stop())
     yield Deferred()  # wait forever because we never .callback()
 
diff --git a/examples/launch_tor_endpoint.py b/examples/launch_tor_endpoint.py
index abdc881..7346242 100755
--- a/examples/launch_tor_endpoint.py
+++ b/examples/launch_tor_endpoint.py
@@ -1,79 +1,91 @@
-#!/usr/bin/env python
+from __future__ import print_function
 
-# Here we set up a Twisted Web server and then launch a slave tor
-# with a configured hidden service directed at the Web server we set
-# up. This uses serverFromString to translate the "onion" endpoint descriptor
-# into a TCPHiddenServiceEndpoint object...
+# Here we set up a Twisted Web server and then launch our own tor with
+# a configured hidden service directed at the Web server we set
+# up. This uses serverFromString to translate the "onion" endpoint
+# descriptor into a TCPHiddenServiceEndpoint object...
 
-from twisted.internet import reactor
 from twisted.web import server, resource
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react, deferLater
 from twisted.internet.endpoints import serverFromString
 
 import txtorcon
 
 
 class Simple(resource.Resource):
+    """
+    A really simple Web site.
+    """
     isLeaf = True
 
     def render_GET(self, request):
         return "<html>Hello, world! I'm a hidden service!</html>"
 
 
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-
-
-def setup_complete(port):
-    # the port we get back should implement this (as well as IListeningPort)
-    port = txtorcon.IHiddenService(port)
-    print "I have set up a hidden service, advertised at:",
-    print "http://%s:%d" % (port.getHost().onion_uri, port.getHost().onion_port)
-    print "locally listening on", port.local_address.getHost()
-    print "Will stop in 60 seconds..."
-
-    def blam(x):
-        print "%d..." % x
-    reactor.callLater(50, blam, 10)
-    reactor.callLater(55, blam, 5)
-    reactor.callLater(56, blam, 4)
-    reactor.callLater(57, blam, 3)
-    reactor.callLater(58, blam, 2)
-    reactor.callLater(59, blam, 1)
-    reactor.callLater(60, reactor.stop)
-
-
-def progress(percent, tag, message):
-    bar = int(percent / 10)
-    print '[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message)
-
-# several ways to proceed here and what they mean:
-#
-# ep0:
-#    launch a new Tor instance, configure a hidden service on some
-#    port and pubish descriptor for port 80
-# ep1:
-#    connect to existing Tor via control-port 9051, configure a hidden
-#    service listening locally on 8080, publish a descriptor for port
-#    80 and use an explicit hiddenServiceDir (where "hostname" and
-#    "private_key" files are put by Tor). We set SOCKS port
-#    explicitly, too.
-# ep2:
-#    all the same as ep1, except we launch a new Tor (because no
-#    "controlPort=9051")
-#
-
-ep0 = "onion:80"
-ep1 = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
-ep2 = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
-
-hs_endpoint = serverFromString(reactor, ep0)
-txtorcon.IProgressProvider(hs_endpoint).add_progress_listener(progress)
-
-# create our Web server and listen on the endpoint; this does the
-# actual launching of (or connecting to) tor.
-site = server.Site(Simple())
-d = hs_endpoint.listen(site)
-d.addCallback(setup_complete)
-d.addErrback(setup_failed)
-
-reactor.run()
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # several ways to proceed here and what they mean:
+    #
+    # "onion:80":
+    #    launch a new Tor instance, configure a hidden service on some
+    #    port and pubish descriptor for port 80
+    #
+    # "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv":
+    #    connect to existing Tor via control-port 9051, configure a hidden
+    #    service listening locally on 8080, publish a descriptor for port
+    #    80 and use an explicit hiddenServiceDir (where "hostname" and
+    #    "private_key" files are put by Tor). We set SOCKS port
+    #    explicitly, too.
+    #
+    # "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv":
+    #    all the same as above, except we launch a new Tor (because no
+    #    "controlPort=9051")
+
+    ep = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
+    ep = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
+    ep = "onion:80"
+    hs_endpoint = serverFromString(reactor, ep)
+
+    def progress(percent, tag, message):
+        bar = int(percent / 10)
+        print("[{}{}] {}".format("#" * bar, "." * (10 - bar), message))
+    txtorcon.IProgressProvider(hs_endpoint).add_progress_listener(progress)
+
+    # create our Web server and listen on the endpoint; this does the
+    # actual launching of (or connecting to) tor.
+    site = server.Site(Simple())
+    port = yield hs_endpoint.listen(site)
+    # XXX new accessor in newer API
+    hs = port.onion_service
+
+    # "port" is an IAddress implementor, in this case TorOnionAddress
+    # so you can get most useful information from it -- but you can
+    # also access .onion_service (see below)
+    print(
+        "I have set up a hidden service, advertised at:\n"
+        "http://{host}:{port}\n"
+        "locally listening on {local_address}\n"
+        "Will stop in 60 seconds...".format(
+            host=port.getHost().onion_uri,  # or hs.hostname
+            port=port.public_port,
+            # port.local_address will be a twisted.internet.tcp.Port
+            # or a twisted.internet.unix.Port -- both have .getHost()
+            local_address=port.local_address.getHost(),
+        )
+    )
+
+    # if you prefer, hs (port.onion_service) is an instance providing
+    # IOnionService (there's no way to do authenticated services via
+    # endpoints yet, but if there was then this would implement
+    # IOnionClients instead)
+    print("private key:\n{}".format(hs.private_key))
+
+    def sleep(s):
+        return deferLater(reactor, s, lambda: None)
+
+    yield sleep(50)
+    for i in range(10):
+        print("Stopping in {}...".format(10 - i))
+        yield sleep(1)
diff --git a/examples/launch_tor_endpoint.py.orig b/examples/launch_tor_endpoint.py.orig
new file mode 100755
index 0000000..7346242
--- /dev/null
+++ b/examples/launch_tor_endpoint.py.orig
@@ -0,0 +1,91 @@
+from __future__ import print_function
+
+# Here we set up a Twisted Web server and then launch our own tor with
+# a configured hidden service directed at the Web server we set
+# up. This uses serverFromString to translate the "onion" endpoint
+# descriptor into a TCPHiddenServiceEndpoint object...
+
+from twisted.web import server, resource
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react, deferLater
+from twisted.internet.endpoints import serverFromString
+
+import txtorcon
+
+
+class Simple(resource.Resource):
+    """
+    A really simple Web site.
+    """
+    isLeaf = True
+
+    def render_GET(self, request):
+        return "<html>Hello, world! I'm a hidden service!</html>"
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # several ways to proceed here and what they mean:
+    #
+    # "onion:80":
+    #    launch a new Tor instance, configure a hidden service on some
+    #    port and pubish descriptor for port 80
+    #
+    # "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv":
+    #    connect to existing Tor via control-port 9051, configure a hidden
+    #    service listening locally on 8080, publish a descriptor for port
+    #    80 and use an explicit hiddenServiceDir (where "hostname" and
+    #    "private_key" files are put by Tor). We set SOCKS port
+    #    explicitly, too.
+    #
+    # "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv":
+    #    all the same as above, except we launch a new Tor (because no
+    #    "controlPort=9051")
+
+    ep = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
+    ep = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv"
+    ep = "onion:80"
+    hs_endpoint = serverFromString(reactor, ep)
+
+    def progress(percent, tag, message):
+        bar = int(percent / 10)
+        print("[{}{}] {}".format("#" * bar, "." * (10 - bar), message))
+    txtorcon.IProgressProvider(hs_endpoint).add_progress_listener(progress)
+
+    # create our Web server and listen on the endpoint; this does the
+    # actual launching of (or connecting to) tor.
+    site = server.Site(Simple())
+    port = yield hs_endpoint.listen(site)
+    # XXX new accessor in newer API
+    hs = port.onion_service
+
+    # "port" is an IAddress implementor, in this case TorOnionAddress
+    # so you can get most useful information from it -- but you can
+    # also access .onion_service (see below)
+    print(
+        "I have set up a hidden service, advertised at:\n"
+        "http://{host}:{port}\n"
+        "locally listening on {local_address}\n"
+        "Will stop in 60 seconds...".format(
+            host=port.getHost().onion_uri,  # or hs.hostname
+            port=port.public_port,
+            # port.local_address will be a twisted.internet.tcp.Port
+            # or a twisted.internet.unix.Port -- both have .getHost()
+            local_address=port.local_address.getHost(),
+        )
+    )
+
+    # if you prefer, hs (port.onion_service) is an instance providing
+    # IOnionService (there's no way to do authenticated services via
+    # endpoints yet, but if there was then this would implement
+    # IOnionClients instead)
+    print("private key:\n{}".format(hs.private_key))
+
+    def sleep(s):
+        return deferLater(reactor, s, lambda: None)
+
+    yield sleep(50)
+    for i in range(10):
+        print("Stopping in {}...".format(10 - i))
+        yield sleep(1)
diff --git a/examples/launch_tor_endpoint2.py b/examples/launch_tor_endpoint2.py
index 1adc178..d57d299 100755
--- a/examples/launch_tor_endpoint2.py
+++ b/examples/launch_tor_endpoint2.py
@@ -5,7 +5,7 @@
 # up. This uses serverFromString to translate the "onion" endpoint descriptor
 # into a TCPHiddenServiceEndpoint object...
 
-import shutil
+from __future__ import print_function
 
 from twisted.internet import reactor
 from twisted.web import server, resource
@@ -20,22 +20,24 @@ class Simple(resource.Resource):
     def render_GET(self, request):
         return "<html>Hello, world! I'm a hidden service!</html>"
 
+
 site = server.Site(Simple())
 
 
 def setup_failed(arg):
-    print "SETUP FAILED", arg
+    print("SETUP FAILED", arg)
 
 
 def setup_complete(port):
     local = txtorcon.IHiddenService(port).local_address.getHost()
-    print "Hidden serivce:", port.getHost()
-    print "    locally at:", local
+    print("Hidden serivce:", port.getHost())
+    print("    locally at:", local)
 
 
 def progress(percent, tag, message):
     bar = int(percent / 10)
-    print '[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message)
+    print('[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message))
+
 
 hs_endpoint1 = serverFromString(reactor, "onion:80")
 hs_endpoint2 = serverFromString(reactor, "onion:80")
diff --git a/examples/launch_tor_endpoint2.py b/examples/launch_tor_endpoint2.py.orig
similarity index 85%
copy from examples/launch_tor_endpoint2.py
copy to examples/launch_tor_endpoint2.py.orig
index 1adc178..d57d299 100755
--- a/examples/launch_tor_endpoint2.py
+++ b/examples/launch_tor_endpoint2.py.orig
@@ -5,7 +5,7 @@
 # up. This uses serverFromString to translate the "onion" endpoint descriptor
 # into a TCPHiddenServiceEndpoint object...
 
-import shutil
+from __future__ import print_function
 
 from twisted.internet import reactor
 from twisted.web import server, resource
@@ -20,22 +20,24 @@ class Simple(resource.Resource):
     def render_GET(self, request):
         return "<html>Hello, world! I'm a hidden service!</html>"
 
+
 site = server.Site(Simple())
 
 
 def setup_failed(arg):
-    print "SETUP FAILED", arg
+    print("SETUP FAILED", arg)
 
 
 def setup_complete(port):
     local = txtorcon.IHiddenService(port).local_address.getHost()
-    print "Hidden serivce:", port.getHost()
-    print "    locally at:", local
+    print("Hidden serivce:", port.getHost())
+    print("    locally at:", local)
 
 
 def progress(percent, tag, message):
     bar = int(percent / 10)
-    print '[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message)
+    print('[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message))
+
 
 hs_endpoint1 = serverFromString(reactor, "onion:80")
 hs_endpoint2 = serverFromString(reactor, "onion:80")
diff --git a/examples/launch_tor_unix_sockets.py b/examples/launch_tor_unix_sockets.py
new file mode 100644
index 0000000..560fc1a
--- /dev/null
+++ b/examples/launch_tor_unix_sockets.py
@@ -0,0 +1,61 @@
+from __future__ import print_function
+
+"""
+Use the 'global_tor' instance from txtorcon; this is a Tor
+instance that either doesn't exist or is unique to this process'
+txtorcon library (i.e. a singleton for this process)
+"""
+
+import sys
+import txtorcon
+import tempfile
+import shutil
+from os import mkdir, chmod
+from os.path import join
+from twisted.web.client import readBody
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # we must have a directory owned by us with 0700 permissions to
+    # contain our Unix sockets or Tor is sad.
+    tmp = tempfile.mkdtemp()
+    reactor.addSystemEventTrigger(
+        'after', 'shutdown',
+        shutil.rmtree, tmp,
+    )
+
+    control_path = join(tmp, 'control_socket')
+    socks_path = join(tmp, 'socks')
+    # note that you can pass a few options as kwargs
+    # (e.g. data_directory=, or socks_port= ). For other torrc
+    # changes, see below.
+    tor = yield txtorcon.launch(
+        reactor,
+        data_directory="./tordata",
+        stdout=sys.stdout,
+        control_port='unix:{}'.format(control_path),
+        socks_port='unix:{}'.format(socks_path),
+    )
+    print("Connected to Tor version '{}'".format(tor.protocol.version))
+
+    state = yield tor.create_state()
+
+    print("This Tor has PID {}".format(state.tor_pid))
+    print("This Tor has the following {} Circuits:".format(len(state.circuits)))
+    for c in state.circuits.values():
+        print("  {}".format(c))
+
+    config = yield tor.get_config()
+    socks_ep = config.create_socks_endpoint(reactor, u'unix:{}'.format(socks_path))
+    agent = tor.web_agent(socks_endpoint=socks_ep)
+    uri = b'https://www.torproject.org'
+    print("Downloading {} via Unix socket".format(uri))
+    resp = yield agent.request(b'GET', uri)
+    print("Response has {} bytes".format(resp.length))
+    body = yield readBody(resp)
+    print("received body ({} bytes)".format(len(body)))
+    print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
diff --git a/examples/launch_tor_with_hiddenservice.py b/examples/launch_tor_with_hiddenservice.py
deleted file mode 100755
index c50e073..0000000
--- a/examples/launch_tor_with_hiddenservice.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env python
-
-# Here we set up a Twisted Web server and then launch a slave tor
-# with a configured hidden service directed at the Web server we set
-# up.
-
-import tempfile
-import functools
-
-from twisted.internet import reactor
-from twisted.internet.endpoints import TCP4ServerEndpoint
-from twisted.web import server, resource
-
-import txtorcon
-
-
-class Simple(resource.Resource):
-    isLeaf = True
-
-    def render_GET(self, request):
-        return "<html>Hello, world! I'm a hidden service!</html>"
-
-
-def updates(prog, tag, summary):
-    print "%d%%: %s" % (prog, summary)
-
-
-def setup_complete(config, proto):
-    print "Protocol completed"
-
-    onion_address = config.HiddenServices[0].hostname
-
-    print "I have a hidden (web) service running at:"
-    print "http://%s (port %d)" % (onion_address, hs_public_port)
-    print "The temporary directory for it is at:", config.HiddenServices[0].dir
-    print
-    print "For example, you should be able to visit it via:"
-    print "  torsocks lynx http://%s" % onion_address
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-hs_port = 9876
-hs_public_port = 80
-hs_temp = tempfile.mkdtemp(prefix='torhiddenservice')
-
-# register something to clean up our tempdir
-reactor.addSystemEventTrigger(
-    'before', 'shutdown',
-    functools.partial(
-        txtorcon.util.delete_file_or_tree,
-        hs_temp
-    )
-)
-
-# configure the hidden service we want.
-# obviously, we'd want a more-persistent place to keep the hidden
-# service directory for a "real" setup. If the directory is empty at
-# startup as here, Tor creates new keys etcetera (which IS the .onion
-# address). That is, every time you run this script you get a new
-# hidden service URI, which is probably not what you want.
-# The launch_tor method adds other needed config directives to give
-# us a minimal config.
-config = txtorcon.TorConfig()
-config.SOCKSPort = 0
-config.ORPort = 9089
-config.HiddenServices = [
-    txtorcon.HiddenService(
-        config,
-        hs_temp,
-        ["%d 127.0.0.1:%d" % (hs_public_port, hs_port)]
-    )
-]
-config.save()
-
-# next we set up our service to listen on hs_port which is forwarded
-# (via the HiddenService options) from the hidden service address on
-# port hs_public_port
-site = server.Site(Simple())
-hs_endpoint = TCP4ServerEndpoint(reactor, hs_port, interface='127.0.0.1')
-hs_endpoint.listen(site)
-
-# we've got our Twisted service listening locally and our options
-# ready to go, so we now launch Tor. Once it's done (see above
-# callbacks) we print out the .onion URI and then do "nothing"
-# (i.e. let the Web server do its thing). Note that the way we've set
-# up the slave Tor process, when we close the connection to it tor
-# will exit.
-
-d = txtorcon.launch_tor(config, reactor, progress_updates=updates)
-d.addCallback(functools.partial(setup_complete, config))
-d.addErrback(setup_failed)
-reactor.run()
diff --git a/examples/launch_tor_with_hiddenservice.py.orig b/examples/launch_tor_with_hiddenservice.py.orig
new file mode 100644
index 0000000..e69de29
diff --git a/examples/launch_tor_with_simplehttpd.py b/examples/launch_tor_with_simplehttpd.py
index 5842b3a..1d6e23d 100755
--- a/examples/launch_tor_with_simplehttpd.py
+++ b/examples/launch_tor_with_simplehttpd.py
@@ -11,6 +11,8 @@ Example:
     ./launch_tor_with_simplehttpd.py -p 8080 -d /opt/files/
 '''
 
+from __future__ import print_function
+
 import SimpleHTTPServer
 import SocketServer
 import functools
@@ -26,23 +28,23 @@ import txtorcon
 
 
 def print_help():
-    print __doc__
+    print(__doc__)
 
 
 def print_tor_updates(prog, tag, summary):
     # Prints some status messages while booting tor
-    print 'Tor booting [%d%%]: %s' % (prog, summary)
+    print('Tor booting [%d%%]: %s' % (prog, summary))
 
 
 def start_httpd(httpd):
     # Create a new thread to serve requests
-    print 'Starting httpd...'
+    print('Starting httpd...')
     return thread.start_new_thread(httpd.serve_forever, ())
 
 
 def stop_httpd(httpd):
     # Kill the httpd
-    print 'Stopping httpd...'
+    print('Stopping httpd...')
     httpd.shutdown()
 
 
@@ -51,16 +53,16 @@ def setup_complete(config, port, proto):
     # We create a reference to this function via functools.partial that
     # provides us with a reference to 'config' and 'port', twisted then adds
     # the 'proto' argument
-    print '\nTor is now running. The hidden service is available at'
-    print '\n\thttp://%s:%i\n' % (config.HiddenServices[0].hostname, port)
+    print('\nTor is now running. The hidden service is available at')
+    print('\n\thttp://%s:%i\n' % (config.HiddenServices[0].hostname, port))
     # This is probably more secure than any other httpd...
-    print '### DO NOT RELY ON THIS SERVER TO TRANSFER FILES IN A SECURE WAY ###'
+    print('## DO NOT RELY ON THIS SERVER TO TRANSFER FILES IN A SECURE WAY ##')
 
 
 def setup_failed(arg):
     # Callback from twisted if tor could not boot. Nothing to see here, move
     # along.
-    print 'Failed to launch tor', arg
+    print('Failed to launch tor', arg)
     reactor.stop()
 
 
@@ -69,12 +71,12 @@ def main():
     try:
         opts, args = getopt.getopt(sys.argv[1:], 'hd:p:')
     except getopt.GetoptError as excp:
-        print str(excp)
+        print(str(excp))
         print_help()
         return 1
 
     serve_directory = '.'  # The default directory to serve files from
-    hs_public_port = 8011  # The default port the hidden service is available on
+    hs_public_port = 8011  # The port the hidden service is available on
     web_port = 4711  # The real server's local port
     web_host = '127.0.0.1'  # The real server is bound to localhost
     for o, a in opts:
@@ -86,39 +88,46 @@ def main():
             print_help()
             return
         else:
-            print 'Unknown option "%s"' % (o, )
+            print('Unknown option "%s"' % (o, ))
             return 1
 
     # Sanitize path and set working directory there (for SimpleHTTPServer)
     serve_directory = os.path.abspath(serve_directory)
     if not os.path.exists(serve_directory):
-        print 'Path "%s" does not exists, can\'t serve from there...' % \
-            (serve_directory, )
+        print('Path "%s" does not exists, can\'t serve from there...' % \
+            (serve_directory, ))
         return 1
     os.chdir(serve_directory)
 
     # Create a new SimpleHTTPServer and serve it from another thread.
     # We create a callback to Twisted to shut it down when we exit.
-    print 'Serving "%s" on %s:%i' % (serve_directory, web_host, web_port)
+    print('Serving "%s" on %s:%i' % (serve_directory, web_host, web_port))
     httpd = SocketServer.TCPServer((web_host, web_port),
                                    SimpleHTTPServer.SimpleHTTPRequestHandler)
     start_httpd(httpd)
-    reactor.addSystemEventTrigger('before', 'shutdown', stop_httpd, httpd=httpd)
+    reactor.addSystemEventTrigger(
+        'before', 'shutdown',
+        stop_httpd, httpd=httpd,
+    )
 
     # Create a directory to hold our hidden service. Twisted will unlink it
     # when we exit.
     hs_temp = tempfile.mkdtemp(prefix='torhiddenservice')
-    reactor.addSystemEventTrigger('before', 'shutdown',
-                                  functools.partial(txtorcon.util.delete_file_or_tree, hs_temp))
+    reactor.addSystemEventTrigger(
+        'before', 'shutdown',
+        functools.partial(txtorcon.util.delete_file_or_tree, hs_temp)
+    )
 
     # Add the hidden service to a blank configuration
     config = txtorcon.TorConfig()
     config.SOCKSPort = 0
     config.ORPort = 9089
-    config.HiddenServices = [txtorcon.HiddenService(config, hs_temp,
-                                                    ['%i %s:%i' % (hs_public_port,
-                                                                   web_host,
-                                                                   web_port)])]
+    config.HiddenServices = [
+        txtorcon.HiddenService(
+            config, hs_temp,
+            ports=['%i %s:%i' % (hs_public_port, web_host, web_port)]
+        )
+    ]
     config.save()
 
     # Now launch tor
diff --git a/examples/minimal_endpoint.py b/examples/minimal_endpoint.py
index 821e160..8a037bf 100755
--- a/examples/minimal_endpoint.py
+++ b/examples/minimal_endpoint.py
@@ -5,5 +5,7 @@ from twisted.internet import reactor
 from twisted.internet.endpoints import serverFromString
 from twisted.web import server, static
 
-serverFromString(reactor, "onion:80").listen(server.Site(static.Data("Hello, world!", "text/plain"))).addCallback(print)
+serverFromString(reactor, "onion:80").listen(
+    server.Site(static.Data("Hello, world!", "text/plain"))
+).addCallback(print)
 reactor.run()
diff --git a/examples/monitor.py b/examples/monitor.py
index 1ce8034..8907aa9 100755
--- a/examples/monitor.py
+++ b/examples/monitor.py
@@ -3,25 +3,25 @@
 # Just listens for a few EVENTs from Tor (INFO NOTICE WARN ERR) and
 # prints out the contents, so functions like a log monitor.
 
-from twisted.internet import reactor
-import txtorcon
+from __future__ import print_function
 
+from twisted.internet import task, defer
+from twisted.internet.endpoints import UNIXClientEndpoint
+import txtorcon
 
-def log(msg):
-    print msg
 
+ at task.react
+ at defer.inlineCallbacks
+def main(reactor):
+    ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    tor = yield txtorcon.connect(reactor, ep)
 
-def setup(proto):
-    print "Connected to a Tor version", proto.version
+    def log(msg):
+        print(msg)
+    print("Connected to a Tor version", tor.protocol.version)
     for event in ['INFO', 'NOTICE', 'WARN', 'ERR']:
-        proto.add_event_listener(event, log)
-    proto.get_info('status/version/current', 'version').addCallback(log)
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-d = txtorcon.build_local_tor_connection(reactor, build_state=False)
-d.addCallback(setup).addErrback(setup_failed)
-reactor.run()
+        tor.protocol.add_event_listener(event, log)
+    is_current = yield tor.protocol.get_info('status/version/current')
+    version = yield tor.protocol.get_info('version')
+    print("Version '{}', is_current={}".format(version, is_current['status/version/current']))
+    yield defer.Deferred()
diff --git a/examples/readme.py b/examples/readme.py
new file mode 100644
index 0000000..bf28776
--- /dev/null
+++ b/examples/readme.py
@@ -0,0 +1,45 @@
+from __future__ import print_function
+
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import UNIXClientEndpoint
+import treq
+import txtorcon
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    tor = yield txtorcon.connect(
+        reactor,
+        UNIXClientEndpoint(reactor, "/var/run/tor/control")
+    )
+
+    print("Connected to Tor version {}".format(tor.version))
+
+    url = 'https://www.torproject.org:443'
+    print("Downloading {}".format(url))
+    resp = yield treq.get(url, agent=tor.web_agent())
+
+    print("   {} bytes".format(resp.length))
+    data = yield resp.text()
+    print("Got {} bytes:\n{}\n[...]{}".format(
+        len(data),
+        data[:120],
+        data[-120:],
+    ))
+
+    print("Creating a circuit")
+    state = yield tor.create_state()
+    circ = yield state.build_circuit()
+    yield circ.when_built()
+    print("  path: {}".format(" -> ".join([r.ip for r in circ.path])))
+
+    print("Downloading meejah's public key via above circuit...")
+    config = yield tor.get_config()
+    resp = yield treq.get(
+        'https://meejah.ca/meejah.asc',
+        agent=circ.web_agent(reactor, config.socks_endpoint(reactor)),
+    )
+    data = yield resp.text()
+    print(data)
diff --git a/examples/readme3.py b/examples/readme3.py
new file mode 100644
index 0000000..73774cc
--- /dev/null
+++ b/examples/readme3.py
@@ -0,0 +1,49 @@
+# this is a Python3 version of the code in readme.py
+
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks, ensureDeferred
+from twisted.internet.endpoints import UNIXClientEndpoint
+
+import treq
+import txtorcon
+
+
+async def main(reactor):
+    tor = await txtorcon.connect(
+        reactor,
+        UNIXClientEndpoint(reactor, "/var/run/tor/control")
+    )
+
+    print("Connected to Tor version {}".format(tor.version))
+
+    url = u'https://www.torproject.org:443'
+    print(u"Downloading {}".format(repr(url)))
+    resp = await treq.get(url, agent=tor.web_agent())
+
+    print(u"   {} bytes".format(resp.length))
+    data = await resp.text()
+    print(u"Got {} bytes:\n{}\n[...]{}".format(
+        len(data),
+        data[:120],
+        data[-120:],
+    ))
+
+    print(u"Creating a circuit")
+    state = await tor.create_state()
+    circ = await state.build_circuit()
+    await circ.when_built()
+    print(u"  path: {}".format(" -> ".join([r.ip for r in circ.path])))
+
+    print(u"Downloading meejah's public key via above circuit...")
+    config = await tor.get_config()
+    resp = await treq.get(
+        u'https://meejah.ca/meejah.asc',
+        agent=circ.web_agent(reactor, config.socks_endpoint(reactor)),
+    )
+    data = await resp.text()
+    print(data)
+
+
+ at react
+def _main(reactor):
+    return ensureDeferred(main(reactor))
diff --git a/examples/schedule_bandwidth.py b/examples/schedule_bandwidth.py
deleted file mode 100755
index acd6e23..0000000
--- a/examples/schedule_bandwidth.py
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env python
-
-# Here, we do something possible-useful and schedule changes to the
-# "BandWidthRate" and optionally "BandWidthBurst" settings in Tor.
-
-import datetime
-from twisted.internet import reactor
-from twisted.internet.interfaces import IReactorTime
-from txtorcon import build_local_tor_connection, TorConfig
-
-
-class BandwidthUpdater:
-
-    def __init__(self, config, scheduler):
-        self.bandwidth = 0
-        self.config = config
-        self.scheduler = IReactorTime(scheduler)
-        self.generator = self.next_update()
-
-    def next_update(self):
-        """
-        Generator that gives out the next time to do a bandwidth update,
-        as well as what the new bandwidth value should be. Here, we toggle
-        the bandwidth every 20 minutes.
-        """
-
-        while True:
-            if self.bandwidth:
-                self.bandwidth = 0
-                self.burst = 0
-            else:
-                self.bandwidth = 20 * 1024 * 1024
-                self.burst = self.bandwidth
-            yield (datetime.datetime.now() + datetime.timedelta(minutes=20),
-                   self.bandwidth, self.burst)
-
-    def do_update(self):
-        x = self.generator.next()
-        future = x[0]
-        self.new_bandwidth = x[1]
-        self.new_burst = x[2]
-
-        tm = (future - datetime.datetime.now()).seconds
-        self.scheduler.callLater(tm, self.really_update)
-        print "waiting", tm, "seconds to adjust bandwidth"
-
-    def really_update(self):
-        print "setting bandwidth + burst to", self.new_bandwidth, self.new_burst
-        self.config.set_config('BandWidthBurst', self.new_burst,
-                               'BandWidthRate', self.new_bandwidth)
-        self.doUpdate()
-
-
-def setup_complete(conf):
-    print "Connected."
-    bwup = BandwidthUpdater(conf, reactor)
-    bwup.do_update()
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    reactor.stop()
-
-
-def bootstrap(proto):
-    config = TorConfig(proto)
-    config.post_bootstrap.addCallback(setup_complete).addErrback(setup_failed)
-    print "Connection is live, bootstrapping config..."
-
-
-d = build_local_tor_connection(reactor, build_state=False,
-                               wait_for_proto=False)
-d.addCallback(bootstrap).addErrback(setup_failed)
-
-reactor.run()
diff --git a/examples/stem_relay_descriptor.py b/examples/stem_relay_descriptor.py
index 76fd702..855c56a 100755
--- a/examples/stem_relay_descriptor.py
+++ b/examples/stem_relay_descriptor.py
@@ -1,38 +1,50 @@
 #!/usr/bin/env python
 
 # This shows how to get the detailed information about a
-# relay descriptor and parse it into Stem's Relay Descriptor
+# relay descriptor and parse it into Stem's RelayDescriptor
 # class. More about the class can be read from
 #
 # https://stem.torproject.org/api/descriptor/server_descriptor.html#stem.descriptor.server_descriptor.RelayDescriptor
 #
-# We need to pass the nickname or the fingerprint of the onion
-# router for which we need the the descriptor information,
+# We need to pass the nickname or the fingerprint of the onion router
+# for which we need the the descriptor information.
+#
+# Also you need to configure Tor to actually download these
+# descriptors -- by default Tor only downloads "microdescriptors"
+# (whose information is already available live via txtorcon.Router
+# instances). Set "UseMicrodescriptors 0" to download "full" descriptors
+from __future__ import print_function
 
 from twisted.internet.task import react
 from twisted.internet.defer import inlineCallbacks
 import txtorcon
 
+try:
+    from stem.descriptor.server_descriptor import RelayDescriptor
+except ImportError:
+    print("You must install 'stem' to use this example:")
+    print("  pip install stem")
+    raise SystemExit(1)
+
 
+ at react
 @inlineCallbacks
 def main(reactor):
-    proto = yield txtorcon.build_local_tor_connection(reactor, build_state=False)
+    tor = yield txtorcon.connect(reactor)
 
     or_nickname = "moria1"
-    print "Trying to get decriptor information about", or_nickname
+    print("Trying to get decriptor information about '{}'".format(or_nickname))
     # If the fingerprint is used in place of nickname then, desc/id/<OR identity>
     # should be used.
-    descriptor_info = yield proto.get_info('desc/name/' + or_nickname)
-
-    descriptor_info = descriptor_info['desc/name/' + or_nickname]
     try:
-        from stem.descriptor.server_descriptor import RelayDescriptor
-        relay_info = RelayDescriptor(descriptor_info)
-        print "The relay's fingerprint is:", relay_info.fingerprint
-        print "Time in UTC when the descriptor was made:", relay_info.published
-    except ImportError as e:
-        print "Error:", e
-
-
-if __name__ == '__main__':
-    react(main)
+        descriptor_info = yield tor.protocol.get_info('desc/name/' + or_nickname)
+    except txtorcon.TorProtocolError:
+        print("No information found. Enable descriptor downloading by setting:")
+        print("  UseMicrodescritors 0")
+        print("In your torrc")
+        raise SystemExit(1)
+
+    descriptor_info = descriptor_info.values()[0]
+    relay_info = RelayDescriptor(descriptor_info)
+    print("The relay's fingerprint is: {}".format(relay_info.fingerprint))
+    print("Time in UTC when the descriptor was made: {}".format(relay_info.published))
diff --git a/examples/stream_circuit_logger.py b/examples/stream_circuit_logger.py
index 381edff..260bfee 100755
--- a/examples/stream_circuit_logger.py
+++ b/examples/stream_circuit_logger.py
@@ -3,27 +3,30 @@
 # This uses an IStreamListener and an ICircuitListener to log all
 # built circuits and all streams that succeed.
 
+from __future__ import print_function
+
 import sys
 from twisted.python import log
 from twisted.internet import reactor
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks, Deferred
 import txtorcon
 
 
-def logCircuit(circuit):
+def log_circuit(circuit):
     path = '->'.join(map(lambda x: str(x.location.countrycode), circuit.path))
     log.msg('Circuit %d (%s) is %s for purpose "%s"' %
             (circuit.id, path, circuit.state, circuit.purpose))
 
 
-def logStream(stream, state):
+def log_stream(stream):
     circ = ''
     if stream.circuit:
-        path = '->'.join(map(lambda x: x.location.countrycode, stream.circuit.path))
+        path = '->'.join(map(lambda x: str(x.location.countrycode), stream.circuit.path))
         circ = ' via circuit %d (%s)' % (stream.circuit.id, path)
     proc = txtorcon.util.process_from_address(
         stream.source_addr,
         stream.source_port,
-        state
     )
     if proc:
         proc = ' from process "%s"' % (proc, )
@@ -41,50 +44,41 @@ def logStream(stream, state):
 class StreamCircuitLogger(txtorcon.StreamListenerMixin,
                           txtorcon.CircuitListenerMixin):
 
-    def __init__(self, state):
-        self.state = state
-
     def stream_attach(self, stream, circuit):
-        logStream(stream, self.state)
+        log_stream(stream)
 
     def stream_failed(self, stream, reason='', remote_reason='', **kw):
-        print 'Stream %d failed because "%s"' % (stream.id, remote_reason)
+        print('Stream %d failed because "%s"' % (stream.id, remote_reason))
 
     def circuit_built(self, circuit):
-        logCircuit(circuit)
+        log_circuit(circuit)
 
     def circuit_failed(self, circuit, **kw):
         log.msg('Circuit %d failed "%s"' % (circuit.id, kw['REASON']))
 
 
-def setup(state):
-    log.msg('Connected to a Tor version %s' % state.protocol.version)
+ at react
+ at inlineCallbacks
+def main(reactor):
+    log.startLogging(sys.stdout)
 
-    listener = StreamCircuitLogger(state)
+    tor = yield txtorcon.connect(reactor)
+    log.msg('Connected to a Tor version %s' % tor.protocol.version)
+    state = yield tor.create_state()
+
+    listener = StreamCircuitLogger()
     state.add_circuit_listener(listener)
     state.add_stream_listener(listener)
 
-    state.protocol.add_event_listener('STATUS_GENERAL', log.msg)
-    state.protocol.add_event_listener('STATUS_SERVER', log.msg)
-    state.protocol.add_event_listener('STATUS_CLIENT', log.msg)
+    tor.protocol.add_event_listener('STATUS_GENERAL', log.msg)
+    tor.protocol.add_event_listener('STATUS_SERVER', log.msg)
+    tor.protocol.add_event_listener('STATUS_CLIENT', log.msg)
 
     log.msg('Existing state when we connected:')
     for s in state.streams.values():
-        logStream(s, state)
+        log_stream(s)
 
     log.msg('Existing circuits:')
     for c in state.circuits.values():
-        logCircuit(c)
-
-
-def setup_failed(arg):
-    print "SETUP FAILED", arg
-    log.err(arg)
-    reactor.stop()
-
-
-log.startLogging(sys.stdout)
-
-d = txtorcon.build_local_tor_connection(reactor)
-d.addCallback(setup).addErrback(setup_failed)
-reactor.run()
+        log_circuit(c)
+    yield Deferred()
diff --git a/examples/systemd.service b/examples/systemd.service
deleted file mode 100644
index 6ae3743..0000000
--- a/examples/systemd.service
+++ /dev/null
@@ -1,43 +0,0 @@
-# see http://twistedmatrix.com/documents/current/core/howto/systemd.html
-# started from that, and changed a few options
-
-[Unit]
-Description=Hidden-Service Web Server
-
-[Service]
-ExecStart=/srv/hiddenservice/venv/bin/twistd \
-    --nodaemon \  # recommended by Twisted
-    --pidfile= \  # systemd doesn't need a PID-file
-    web --port onion:80:hiddenServiceDir=/srv/hiddenservice/venv/hostkeys --path .
-
-WorkingDirectory=/srv/hiddenservice/html
-
-User=nobody
-Group=nobody
-
-Restart=always
-
-[Install]
-WantedBy=multi-user.target
-Wants=network-online.target
-
-# usage (using root as needed):
-#
-# mkdir -p /srv/hiddenservice/html
-# >copy all web content to ^^^^^^^:
-#
-# cd /srv/hiddenservice
-# virtualenv venv
-# ./venv/bin/pip install txtorcon
-#
-# >put this file (that you're reading) in:
-#   /etc/systemd/system/hidden-service-name.service
-#
-# >test:
-# systemctl daemon-reload
-# systemctl start hidden-service-name
-# systemctl status hidden-service-name  # <-- check if it's running
-#
-# >enable at boot:
-# systemctl enable hidden-service-name.service
-# ln -s '/etc/systemd/system/hidden-service-name.service' '/etc/systemd/system/multi-user.target.wants/hidden-service-name.service'
diff --git a/examples/tor_info.py b/examples/tor_info.py
index 9cd9e08..3018835 100755
--- a/examples/tor_info.py
+++ b/examples/tor_info.py
@@ -15,13 +15,15 @@
 # transaction, you'll have to use TorControlProtocol's get_info
 # instead.
 
+from __future__ import print_function
+
 import sys
 from twisted.internet import reactor, defer
 from txtorcon import TorInfo, build_local_tor_connection
 
 
 def error(x):
-    print "ERROR", x
+    print("ERROR", x)
     return x
 
 
@@ -29,7 +31,7 @@ def error(x):
 def recursive_dump(indent, obj, depth=0):
     if callable(obj):
         try:
-            print "%s: " % obj,
+            print("%s: " % obj, end=' ')
             sys.stdout.flush()
             if obj.takes_arg:
                 v = yield obj('arrrrrg')
@@ -37,9 +39,9 @@ def recursive_dump(indent, obj, depth=0):
             v = v.replace('\n', '\\')
             if len(v) > 60:
                 v = v[:50] + '...' + v[-7:]
-        except Exception, e:
+        except Exception as e:
             v = 'ERROR: ' + str(e)
-        print v
+        print(v)
 
     else:
         indent = indent + '  '
@@ -49,7 +51,7 @@ def recursive_dump(indent, obj, depth=0):
 
 @defer.inlineCallbacks
 def setup_complete(info):
-    print "Top-Level Things:", dir(info)
+    print("Top-Level Things:", dir(info))
 
     if True:
         # some examples of getting specific GETINFO callbacks
@@ -59,11 +61,11 @@ def setup_complete(info):
         ns = yield info.ns.name('moria1')
         guards = yield info.entry_guards()
 
-        print 'version:', v
-        print '1.2.3.4 is in', ip
-        print 'bootstrap-phase:', boot_phase
-        print 'moria1:', ns
-        print 'entry guards:', guards
+        print('version:', v)
+        print('1.2.3.4 is in', ip)
+        print('bootstrap-phase:', boot_phase)
+        print('moria1:', ns)
+        print('entry guards:', guards)
 
     # now we dump everything, one at a time
     d = recursive_dump('', info)
@@ -72,7 +74,7 @@ def setup_complete(info):
 
 
 def setup_failed(arg):
-    print "SETUP FAILED", arg
+    print("SETUP FAILED", arg)
     reactor.stop()
 
 
diff --git a/examples/torflow_path_selection.py b/examples/torflow_path_selection.py
deleted file mode 100644
index 0204230..0000000
--- a/examples/torflow_path_selection.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python
-
-# playing around with some ideas as far as bwauth (inside TorFlow)
-# works, and the path-selection stuff from TorCtl which has been
-# mentioned as a desireable txtorcon feature.
-
-import random
-from functools import partial
-from twisted.internet import defer
-from twisted.internet import task
-import txtorcon
-
-
-def node_selector(routers,  # torstate,
-                  node_filter=lambda x: True,
-                  chooser=random.choice):
-    nodes = filter(node_filter, routers)
-    if len(nodes) == 0:
-        raise RuntimeError("No nodes left after filtering.")
-    return chooser(nodes)
-
-
-def flag_filter(flag, router):
-    return flag in router.flags
-
-
-def cmp_bandwidth(router_a, router_b):
-    return cmp(router_a.bandwidth, router_b.bandwidth)
-
-guard_filter = partial(flag_filter, 'guard')
-exit_filter = partial(flag_filter, 'exit')
-
-
-def percent_filter(torstate, min_pct, max_pct):
-    if min_pct <= 0.0:
-        raise RuntimeError("Minimum is too low")
-    if max_pct >= 100.0:
-        raise RuntimeError("Maximum is too high")
-
-    def actual_filter(router):
-        return
-
-
-def create_paths(routers):
-    '''
-    This is a generator that produces paths the way Tor does: with a
-    single guard node, a single middle node and a single exit
-    node.
-
-    Every time the generator runs it returns a tuple of Router
-    instances representing the chosen path.
-
-    FIXME need to consider when underlying router list
-    changes. e.g. between any iteration it's possible all our cached
-    data became stale...
-    '''
-
-    guard_selector = lambda: node_selector(routers, node_filter=guard_filter)
-    middle_selector = lambda: node_selector(routers)
-    exit_selector = lambda: node_selector(routers, node_filter=exit_filter)
-    while True:
-        yield (guard_selector(), middle_selector(), exit_selector())
-
-
- at defer.inlineCallbacks
-def main(reactor):
-    state = yield txtorcon.build_local_tor_connection(reactor)
-    yield state.post_bootstrap
-
-    # make a generator for selecting from any possible router
-    g = create_paths(state.all_routers)
-    for i in range(10):
-        path = g.next()
-        print map(lambda r: r.name, path)
-
-    # same, except we limit the routers to the top N% fastest ones in
-    # each category: guard, middle, exit
-    guards = filter(guard_filter, torstate.all_routers)
-    guards.sort(key=lambda x: x.bandwidth)
-    middles = list(torstate.all_routers)
-    middles.sort(key=lambda x: x.bandwidth)
-    exits = filter(exit_filter, torstate.all_routers)
-    exits.sort(key=lambda x: x.bandwidth)
-
-    print len(routers_by_bandwidth), "routers"
-    percent = 10.0
-    top_n = int((percent / 100.0) * len(routers_by_bandwidth))
-    g = create_paths(routers_by_bandwidth[:top_n])
-    for i in range(10):
-        path = g.next()
-        print map(lambda r: r.name, path)
-
-task.react(main)
diff --git a/examples/tunnel_tls_through_tor_client.py b/examples/tunnel_tls_through_tor_client.py
deleted file mode 100644
index 942fa85..0000000
--- a/examples/tunnel_tls_through_tor_client.py
+++ /dev/null
@@ -1,30 +0,0 @@
-
-from twisted.internet.task import react
-from twisted.internet.defer import inlineCallbacks
-from twisted.internet.endpoints import TCP4ClientEndpoint
-from twisted.python.filepath import FilePath
-from twisted.internet import ssl, task, protocol, endpoints, defer
-from twisted.python.modules import getModule
-from txsocksx.tls import TLSWrapClientEndpoint
-import txtorcon
-
-import other_proto
-
-
- at defer.inlineCallbacks
-def main(reactor):
-    factory = protocol.Factory.forProtocol(other_proto.DecisionProtocol)
-    ca_data = FilePath(b'ca_cert.pem').getContent()
-    client_data = FilePath(b'a.client.pem').getContent()
-    ca_cert = ssl.Certificate.loadPEM(ca_data)
-    client_key = ssl.PrivateCertificate.loadPEM(client_data)
-    options = ssl.optionsForClientTLS(u'the-authority', ca_cert, client_key)
-    exampleEndpoint = txtorcon.TorClientEndpoint(ip, 8966, socks_hostname="127.0.0.1")
-    tlsEndpoint = TLSWrapClientEndpoint(options, exampleEndpoint)
-    deferred = yield tlsEndpoint.connect(factory)
-    done = defer.Deferred()
-    deferred.connectionLost = lambda reason: done.callback(None)
-    yield done
-
-
-task.react(main)
diff --git a/examples/web_client.py b/examples/web_client.py
new file mode 100644
index 0000000..7f17a7c
--- /dev/null
+++ b/examples/web_client.py
@@ -0,0 +1,42 @@
+# this example shows how to use Twisted's web client with Tor via
+# txtorcon
+
+from __future__ import print_function
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.web.client import readBody
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # use port 9051 for system tor instances, or:
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected to {tor} via localhost:{port}".format(
+        tor=tor,
+        port=default_control_port(),
+    ))
+
+    # create a web.Agent that will talk via Tor. If the socks port
+    # given isn't yet configured, this will do so. It may also be
+    # None, which means "the first configured SOCKSPort"
+    # agent = tor.web_agent(u'9999')
+    agent = tor.web_agent()
+    uri = b'http://surely-this-has-not-been-registered-and-is-invalid.com'
+    uri = b'https://www.torproject.org'
+    uri = b'http://timaq4ygg2iegci7.onion/'  # txtorcon documentation
+    print("Downloading {}".format(uri))
+    resp = yield agent.request(b'GET', uri)
+
+    print("Response has {} bytes".format(resp.length))
+    body = yield readBody(resp)
+    print("received body ({} bytes)".format(len(body)))
+    print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
diff --git a/examples/web_client.py.orig b/examples/web_client.py.orig
new file mode 100644
index 0000000..7f17a7c
--- /dev/null
+++ b/examples/web_client.py.orig
@@ -0,0 +1,42 @@
+# this example shows how to use Twisted's web client with Tor via
+# txtorcon
+
+from __future__ import print_function
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.web.client import readBody
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # use port 9051 for system tor instances, or:
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected to {tor} via localhost:{port}".format(
+        tor=tor,
+        port=default_control_port(),
+    ))
+
+    # create a web.Agent that will talk via Tor. If the socks port
+    # given isn't yet configured, this will do so. It may also be
+    # None, which means "the first configured SOCKSPort"
+    # agent = tor.web_agent(u'9999')
+    agent = tor.web_agent()
+    uri = b'http://surely-this-has-not-been-registered-and-is-invalid.com'
+    uri = b'https://www.torproject.org'
+    uri = b'http://timaq4ygg2iegci7.onion/'  # txtorcon documentation
+    print("Downloading {}".format(uri))
+    resp = yield agent.request(b'GET', uri)
+
+    print("Response has {} bytes".format(resp.length))
+    body = yield readBody(resp)
+    print("received body ({} bytes)".format(len(body)))
+    print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
diff --git a/examples/web_client_custom_circuit.py b/examples/web_client_custom_circuit.py
new file mode 100644
index 0000000..668bd09
--- /dev/null
+++ b/examples/web_client_custom_circuit.py
@@ -0,0 +1,84 @@
+# this example shows how to use specific circuits over Tor (with
+# Twisted's web client or with a custom protocol)
+#
+# NOTE WELL: this functionality is for advanced use-cases and if you
+# do anything "special" to select your circuit hops you risk making it
+# easy to de-anonymize this (and all other) Tor circuits.
+
+from __future__ import print_function
+
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.web.client import readBody
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # use port 9051 for system tor instances, or:
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected:", tor)
+
+    config = yield tor.get_config()
+    state = yield tor.create_state()
+    socks = config.socks_endpoint(reactor)
+
+    # create a custom circuit; in this case we're just letting Tor
+    # decide the path but you *can* select a path (again: for advanced
+    # use cases that will probably de-anonymize you)
+    circ = yield state.build_circuit()
+    print("Building a circuit:", circ)
+
+    # at this point, the circuit will be "under way" but may not yet
+    # be in BUILT state -- and hence usable. So, we wait. (Just for
+    # demo purposes: the underlying connect will wait too)
+    yield circ.when_built()
+    print("Circuit is ready:", circ)
+
+    if True:
+        # create a web.Agent that will use this circuit (or fail)
+        agent = circ.web_agent(reactor, socks)
+
+        uri = 'https://www.torproject.org'
+        print("Downloading {}".format(uri))
+        resp = yield agent.request('GET', uri)
+
+        print("Response has {} bytes".format(resp.length))
+        body = yield readBody(resp)
+        print("received body ({} bytes)".format(len(body)))
+        print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
+
+    if True:
+        # make a plain TCP connection to a thing
+        ep = circ.stream_via(reactor, 'torproject.org', 80, config.socks_endpoint(reactor))
+
+        d = Deferred()
+
+        class ToyWebRequestProtocol(Protocol):
+
+            def connectionMade(self):
+                print("Connected via {}".format(self.transport.getHost()))
+                self.transport.write(
+                    'GET http://torproject.org/ HTTP/1.1\r\n'
+                    'Host: torproject.org\r\n'
+                    '\r\n'
+                )
+
+            def dataReceived(self, d):
+                print("  received {} bytes".format(len(d)))
+
+            def connectionLost(self, reason):
+                print("disconnected: {}".format(reason.value))
+                d.callback(None)
+
+        proto = yield ep.connect(Factory.forProtocol(ToyWebRequestProtocol))
+        yield d
+        print("All done, closing the circuit")
+        yield circ.close()
diff --git a/examples/web_client_custom_circuit.py.orig b/examples/web_client_custom_circuit.py.orig
new file mode 100644
index 0000000..668bd09
--- /dev/null
+++ b/examples/web_client_custom_circuit.py.orig
@@ -0,0 +1,84 @@
+# this example shows how to use specific circuits over Tor (with
+# Twisted's web client or with a custom protocol)
+#
+# NOTE WELL: this functionality is for advanced use-cases and if you
+# do anything "special" to select your circuit hops you risk making it
+# easy to de-anonymize this (and all other) Tor circuits.
+
+from __future__ import print_function
+
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.web.client import readBody
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    # use port 9051 for system tor instances, or:
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected:", tor)
+
+    config = yield tor.get_config()
+    state = yield tor.create_state()
+    socks = config.socks_endpoint(reactor)
+
+    # create a custom circuit; in this case we're just letting Tor
+    # decide the path but you *can* select a path (again: for advanced
+    # use cases that will probably de-anonymize you)
+    circ = yield state.build_circuit()
+    print("Building a circuit:", circ)
+
+    # at this point, the circuit will be "under way" but may not yet
+    # be in BUILT state -- and hence usable. So, we wait. (Just for
+    # demo purposes: the underlying connect will wait too)
+    yield circ.when_built()
+    print("Circuit is ready:", circ)
+
+    if True:
+        # create a web.Agent that will use this circuit (or fail)
+        agent = circ.web_agent(reactor, socks)
+
+        uri = 'https://www.torproject.org'
+        print("Downloading {}".format(uri))
+        resp = yield agent.request('GET', uri)
+
+        print("Response has {} bytes".format(resp.length))
+        body = yield readBody(resp)
+        print("received body ({} bytes)".format(len(body)))
+        print("{}\n[...]\n{}\n".format(body[:200], body[-200:]))
+
+    if True:
+        # make a plain TCP connection to a thing
+        ep = circ.stream_via(reactor, 'torproject.org', 80, config.socks_endpoint(reactor))
+
+        d = Deferred()
+
+        class ToyWebRequestProtocol(Protocol):
+
+            def connectionMade(self):
+                print("Connected via {}".format(self.transport.getHost()))
+                self.transport.write(
+                    'GET http://torproject.org/ HTTP/1.1\r\n'
+                    'Host: torproject.org\r\n'
+                    '\r\n'
+                )
+
+            def dataReceived(self, d):
+                print("  received {} bytes".format(len(d)))
+
+            def connectionLost(self, reason):
+                print("disconnected: {}".format(reason.value))
+                d.callback(None)
+
+        proto = yield ep.connect(Factory.forProtocol(ToyWebRequestProtocol))
+        yield d
+        print("All done, closing the circuit")
+        yield circ.close()
diff --git a/examples/web_client_treq.py b/examples/web_client_treq.py
new file mode 100644
index 0000000..99d1a44
--- /dev/null
+++ b/examples/web_client_treq.py
@@ -0,0 +1,40 @@
+# just copying over most of "carml checkpypi" because it's a good
+# example of "I want a stream over *this* circuit".
+
+from __future__ import print_function
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+try:
+    import treq
+except ImportError:
+    print("To use this example, please install 'treq':")
+    print("pip install treq")
+    raise SystemExit(1)
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected:", tor)
+
+    resp = yield treq.get(
+        'https://www.torproject.org:443',
+        agent=tor.web_agent(),
+    )
+
+    print("Retrieving {} bytes".format(resp.length))
+    data = yield resp.text()
+    print("Got {} bytes:\n{}\n[...]{}".format(
+        len(data),
+        data[:120],
+        data[-120:],
+    ))
diff --git a/examples/web_client_treq.py.orig b/examples/web_client_treq.py.orig
new file mode 100644
index 0000000..99d1a44
--- /dev/null
+++ b/examples/web_client_treq.py.orig
@@ -0,0 +1,40 @@
+# just copying over most of "carml checkpypi" because it's a good
+# example of "I want a stream over *this* circuit".
+
+from __future__ import print_function
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.task import react
+from twisted.internet.endpoints import TCP4ClientEndpoint
+
+import txtorcon
+from txtorcon.util import default_control_port
+
+try:
+    import treq
+except ImportError:
+    print("To use this example, please install 'treq':")
+    print("pip install treq")
+    raise SystemExit(1)
+
+
+ at react
+ at inlineCallbacks
+def main(reactor):
+    ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port())
+    # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control')
+    tor = yield txtorcon.connect(reactor, ep)
+    print("Connected:", tor)
+
+    resp = yield treq.get(
+        'https://www.torproject.org:443',
+        agent=tor.web_agent(),
+    )
+
+    print("Retrieving {} bytes".format(resp.length))
+    data = yield resp.text()
+    print("Got {} bytes:\n{}\n[...]{}".format(
+        len(data),
+        data[:120],
+        data[-120:],
+    ))
diff --git a/examples/webui_server.py b/examples/webui_server.py
index b0255bd..a17c4b1 100755
--- a/examples/webui_server.py
+++ b/examples/webui_server.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python
 
+from __future__ import print_function
+
 from twisted.internet import reactor
 from nevow.appserver import NevowSite
 from nevow import loaders, tags, livepage
@@ -7,7 +9,7 @@ import txtorcon
 
 
 def setup_failed(fail):
-    print "It went sideways!", fail
+    print("It went sideways!", fail)
     return fail
 
 
@@ -49,7 +51,7 @@ class TorPage(livepage.LivePage):
 
     def tor_update(self, percent, tag, summary):
         if self.ctx is None:
-            print "I have no Web client yet, but got a Tor update:", percent, tag, summary
+            print("I have no Web client yet, but got a Tor update:", percent, tag, summary)
             return
 
         point = int(300 * (float(percent) / 100.0))
@@ -87,7 +89,7 @@ d = txtorcon.launch_tor(config, reactor, progress_updates=top_level.tor_update)
 d.addCallback(top_level.set_tor_state)
 d.addErrback(setup_failed)
 
-print "Launching Tor and providing a Web interface on: \nhttp://localhost:8080\n"
+print("Launching Tor and providing a Web interface on: \nhttp://localhost:8080\n")
 
 # Start up the Web server
 site = NevowSite(top_level)
diff --git a/requirements.txt b/requirements.txt
index 4fc8215..241369a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,8 @@
 ## see also dev-requirements.txt to build
 ## hmm, travis-ci doesn't like this since we need a GeoIP-dev package
 ##GeoIP>=1.2.9
-Twisted>=11.1.0
+Twisted[tls]>=15.5.0
 ipaddress>=1.0.16
 zope.interface>=3.6.1
-txsocksx>=1.13.0
+incremental
+automat
diff --git a/setup.cfg b/setup.cfg
index 861a9f5..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,4 @@
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff --git a/setup.py b/setup.py
index 33c051c..38c6421 100644
--- a/setup.py
+++ b/setup.py
@@ -22,6 +22,8 @@ with open('txtorcon/_metadata.py') as f:
 description = '''
     Twisted-based Tor controller client, with state-tracking and
     configuration abstractions.
+    https://txtorcon.readthedocs.org
+    https://github.com/meejah/txtorcon
 '''
 
 sphinx_rst_files = [x for x in listdir('docs') if x[-3:] == 'rst']
@@ -61,7 +63,11 @@ setup(
     author_email=__contact__,
     url=__url__,
     license=__license__,
-    packages=["txtorcon", "twisted.plugins"],
+    packages=[
+        "test",
+        "txtorcon",
+        "twisted.plugins",
+    ],
 
     # I'm a little unclear if I'm doing this "properly", especially
     # the documentation etc. Do we really want "share/txtorcon" for
diff --git a/test/profile_startup.py b/test/profile_startup.py
index 997007a..1699a24 100644
--- a/test/profile_startup.py
+++ b/test/profile_startup.py
@@ -8,14 +8,14 @@ proto = txtorcon.TorControlProtocol()
 state = txtorcon.TorState(proto)
 
 data = open('consensus', 'r').read()
-routers = 4724  # number of routers in above file
-iters = 100
+routers = 5494  # number of routers in above file
+iters = 5
 
 start = time()
 if False:
     cProfile.run('state._update_network_status(data)')
 else:
-    for x in xrange(iters):
+    for x in range(iters):
         state._update_network_status(data)
 diff = time() - start
-print "%fs: %f microdescriptors/second" % (diff, (routers * iters) / diff)
+print("%fs: %f microdescriptors/second" % (diff, (routers * iters) / diff))
diff --git a/test/py3_torstate.py b/test/py3_torstate.py
new file mode 100644
index 0000000..689597a
--- /dev/null
+++ b/test/py3_torstate.py
@@ -0,0 +1,91 @@
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+from twisted.internet import defer
+from twisted.internet.interfaces import IReactorCore
+
+from zope.interface import implementer
+
+from txtorcon import TorControlProtocol
+from txtorcon import TorState
+from txtorcon import Circuit
+from txtorcon.interface import IStreamAttacher
+
+
+ at implementer(IReactorCore)
+class FakeReactor:
+
+    def __init__(self, test):
+        self.test = test
+
+    def addSystemEventTrigger(self, *args):
+        self.test.assertEqual(args[0], 'before')
+        self.test.assertEqual(args[1], 'shutdown')
+        self.test.assertEqual(args[2], self.test.state.undo_attacher)
+        return 1
+
+    def removeSystemEventTrigger(self, id):
+        self.test.assertEqual(id, 1)
+
+    def connectTCP(self, *args, **kw):
+        """for testing build_tor_connection"""
+        raise RuntimeError('connectTCP: ' + str(args))
+
+    def connectUNIX(self, *args, **kw):
+        """for testing build_tor_connection"""
+        raise RuntimeError('connectUNIX: ' + str(args))
+
+
+class FakeCircuit(Circuit):
+
+    def __init__(self, id=-999):
+        self.streams = []
+        self.id = id
+        self.state = 'BOGUS'
+
+
+class TorStatePy3Tests(unittest.TestCase):
+
+    def setUp(self):
+        self.protocol = TorControlProtocol()
+        self.state = TorState(self.protocol)
+        # avoid spew in trial logs; state prints this by default
+        self.state._attacher_error = lambda f: f
+        self.protocol.connectionMade = lambda: None
+        self.transport = proto_helpers.StringTransport()
+        self.protocol.makeConnection(self.transport)
+
+    def send(self, line):
+        self.protocol.dataReceived(line.strip() + b"\r\n")
+
+    def test_attacher_coroutine(self):
+        @implementer(IStreamAttacher)
+        class MyAttacher(object):
+
+            def __init__(self, answer):
+                self.streams = []
+                self.answer = answer
+
+            async def attach_stream(self, stream, circuits):
+                self.streams.append(stream)
+                x = await defer.succeed(self.answer)
+                return x
+
+        self.state.circuits[1] = FakeCircuit(1)
+        self.state.circuits[1].state = 'BUILT'
+        attacher = MyAttacher(self.state.circuits[1])
+        self.state.set_attacher(attacher, FakeReactor(self))
+
+        # boilerplate to finish enough set-up in the protocol so it
+        # works
+        events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'
+        self.protocol._set_valid_events(events)
+        self.state._add_events()
+        for ignored in self.state.event_map.items():
+            self.send(b"250 OK")
+
+        self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
+        self.send(b"650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
+        self.assertEqual(len(attacher.streams), 1)
+        self.assertEqual(attacher.streams[0].id, 1)
+        self.assertEqual(len(self.protocol.commands), 1)
+        self.assertEqual(self.protocol.commands[0][1], b'ATTACHSTREAM 1 1')
diff --git a/test/test_addrmap.py b/test/test_addrmap.py
index 1d50bf9..6e71b9d 100644
--- a/test/test_addrmap.py
+++ b/test/test_addrmap.py
@@ -2,14 +2,14 @@ import datetime
 from twisted.trial import unittest
 from twisted.internet import task
 from twisted.internet.interfaces import IReactorTime
-from zope.interface import implements
+from zope.interface import implementer
 
 from txtorcon.addrmap import AddrMap
 from txtorcon.interface import IAddrListener
 
 
+ at implementer(IAddrListener)
 class AddrMapTests(unittest.TestCase):
-    implements(IAddrListener)
 
     fmt = '%Y-%m-%d %H:%M:%S'
 
diff --git a/test/test_attacher.py b/test/test_attacher.py
new file mode 100644
index 0000000..d3f577a
--- /dev/null
+++ b/test/test_attacher.py
@@ -0,0 +1,41 @@
+from mock import Mock
+from zope.interface import directlyProvides
+
+from twisted.trial import unittest
+
+from txtorcon.attacher import PriorityAttacher
+from txtorcon.interface import IStreamAttacher
+
+
+class PriorityAttacherTest(unittest.TestCase):
+
+    def test_add_remove(self):
+        a = PriorityAttacher()
+        boom = Mock()
+        directlyProvides(boom, IStreamAttacher)
+
+        a.add_attacher(boom)
+        a.remove_attacher(boom)
+        with self.assertRaises(ValueError) as ctx:
+            a.remove_attacher(boom)
+        self.assertTrue('not found' in str(ctx.exception))
+
+    def test_stream_failure(self):
+        a = PriorityAttacher()
+        boom = Mock()
+        directlyProvides(boom, IStreamAttacher)
+
+        a.add_attacher(boom)
+        a.attach_stream_failure(Mock(), Mock())
+
+    def test_attach_stream(self):
+        a = PriorityAttacher()
+        boom = Mock()
+        directlyProvides(boom, IStreamAttacher)
+
+        a.add_attacher(boom)
+        a.attach_stream(Mock(), [])
+
+    def test_attach_stream_nothing(self):
+        a = PriorityAttacher()
+        a.attach_stream(Mock(), [])
diff --git a/test/test_circuit.py b/test/test_circuit.py
index e5b3753..11ed9aa 100644
--- a/test/test_circuit.py
+++ b/test/test_circuit.py
@@ -1,19 +1,20 @@
 import datetime
-import time
+import ipaddress
+from mock import patch
+
 from twisted.trial import unittest
-from twisted.internet import defer, task
+from twisted.internet import defer
 from twisted.python.failure import Failure
-from zope.interface import implements
-from mock import patch
 
-from txtorcon import Circuit
-from txtorcon import build_timeout_circuit
+from zope.interface import implementer
 
+from txtorcon import Circuit
 from txtorcon import Stream
 from txtorcon import TorControlProtocol
 from txtorcon import TorState
 from txtorcon import Router
 from txtorcon.router import hexIdFromHash
+from txtorcon.circuit import TorCircuitEndpoint, _get_circuit_attacher
 from txtorcon.interface import IRouterContainer
 from txtorcon.interface import ICircuitListener
 from txtorcon.interface import ICircuitContainer
@@ -23,8 +24,11 @@ from txtorcon.interface import ITorControlProtocol
 from mock import Mock
 
 
+ at implementer(IRouterContainer)
+ at implementer(ICircuitListener)
+ at implementer(ICircuitContainer)
+ at implementer(ITorControlProtocol)
 class FakeTorController(object):
-    implements(IRouterContainer, ICircuitListener, ICircuitContainer, ITorControlProtocol)
 
     post_bootstrap = defer.Deferred()
     queue_command = Mock()
@@ -81,6 +85,7 @@ class FakeRouter:
         self.id_hex = hexIdFromHash(self.id_hash)
         self.location = FakeLocation()
 
+
 examples = ['CIRC 365 LAUNCHED PURPOSE=GENERAL',
             'CIRC 365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL',
             'CIRC 365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus PURPOSE=GENERAL',
@@ -90,6 +95,82 @@ examples = ['CIRC 365 LAUNCHED PURPOSE=GENERAL',
             'CIRC 365 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT']
 
 
+class TestCircuitEndpoint(unittest.TestCase):
+
+    @defer.inlineCallbacks
+    def test_attach(self):
+
+        @implementer(ICircuitContainer)
+        class FakeContainer(object):
+            pass
+
+        container = FakeContainer()
+        stream = Stream(container)
+        circuit = Mock()
+        target_endpoint = Mock()
+        reactor = Mock()
+        state = Mock()
+
+        TorCircuitEndpoint(
+            reactor, state, circuit, target_endpoint,
+        )
+
+        attacher = yield _get_circuit_attacher(reactor, state)
+        attacher.add_endpoint(target_endpoint, circuit)
+        yield attacher.attach_stream(stream, [])
+        # hmmm, no assert??
+
+    @defer.inlineCallbacks
+    def test_attach_stream_failure(self):
+
+        @implementer(ICircuitContainer)
+        class FakeContainer(object):
+            pass
+
+        container = FakeContainer()
+        stream = Stream(container)
+        stream.source_addr = ipaddress.IPv4Address(u'0.0.0.0')
+        stream.source_port = 12345
+        circuit = Mock()
+        circuit.when_built = Mock(return_value=Failure(Exception('testing1234')))
+        target_endpoint = Mock()
+        src_addr = Mock()
+        src_addr.host = u'0.0.0.0'
+        src_addr.port = 12345
+        target_endpoint._get_address = Mock(return_value=defer.succeed(src_addr))
+        reactor = Mock()
+        state = Mock()
+
+        TorCircuitEndpoint(
+            reactor, state, circuit, target_endpoint,
+        )
+
+        attacher = yield _get_circuit_attacher(reactor, state)
+        d = attacher.add_endpoint(target_endpoint, circuit)
+        self.assertEqual(len(attacher._circuit_targets), 1)
+        # this will fail, but should be ignored
+        yield attacher.attach_stream(stream, [])
+        with self.assertRaises(Exception) as ctx:
+            yield d
+        self.assertTrue("testing1234" in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_attach_failure_unfound(self):
+
+        @implementer(ICircuitContainer)
+        class FakeContainer(object):
+            pass
+
+        reactor = Mock()
+        container = FakeContainer()
+        stream = Stream(container)
+        state = Mock()
+
+        attacher = yield _get_circuit_attacher(reactor, state)
+        attacher.attach_stream_failure(stream, None)
+        # no assert; just making sure this doesn't explode
+
+
 class CircuitTests(unittest.TestCase):
 
     def test_age(self):
@@ -103,7 +184,7 @@ class CircuitTests(unittest.TestCase):
         update = '1 LAUNCHED PURPOSE=GENERAL TIME_CREATED=%s' % now.strftime('%Y-%m-%dT%H:%M:%S')
         circuit.update(update.split())
         diff = circuit.age(now=now)
-        self.assertEquals(diff, 0)
+        self.assertEqual(diff, 0)
         self.assertTrue(circuit.time_created is not None)
 
     @patch('txtorcon.circuit.datetime')
@@ -119,7 +200,7 @@ class CircuitTests(unittest.TestCase):
 
         circuit = Circuit(tor)
         circuit._time_created = datetime.fromtimestamp(0.0)
-        self.assertEquals(circuit.age(), 60)
+        self.assertEqual(circuit.age(), 60)
         self.assertTrue(circuit.time_created is not None)
 
     def test_no_age_yet(self):
@@ -133,7 +214,7 @@ class CircuitTests(unittest.TestCase):
         circuit.update('1 LAUNCHED PURPOSE=GENERAL'.split())
         self.assertTrue(circuit.time_created is None)
         diff = circuit.age(now=now)
-        self.assertEquals(diff, None)
+        self.assertEqual(diff, None)
 
     def test_listener_mixin(self):
         listener = CircuitListenerMixin()
@@ -202,7 +283,8 @@ class CircuitTests(unittest.TestCase):
         circuit.update('1 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=FINISHED'.split())
         circuit.update('1 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT'.split())
         errs = self.flushLoggedErrors()
-        self.assertEqual(len(errs), 2)
+        self.assertEqual(len(errs), 1)
+        self.assertTrue('Circuit is FAILED but still has 1 streams' in str(errs[0]))
 
     def test_updates(self):
         tor = FakeTorController()
@@ -313,26 +395,38 @@ class CircuitTests(unittest.TestCase):
         circuit.update('123 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split())
 
         self.assertEqual(3, len(circuit.path))
-        d = circuit.close()
+        d0 = circuit.close()
         # we already pretended that Tor answered "OK" to the
         # CLOSECIRCUIT call (see close_circuit() in FakeTorController
         # above) however the circuit isn't "really" closed yet...
-        self.assertTrue(not d.called)
+        self.assertTrue(not d0.result.called)
         # not unit-test-y? shouldn't probably delve into internals I
         # suppose...
         self.assertTrue(circuit._closing_deferred is not None)
 
+        # if we try to close it again (*before* the actual close has
+        # succeeded!) we should also still be waiting.
+        d1 = circuit.close()
+        self.assertTrue(not d1.called)
+        # ...and this Deferred should *not* be the same as the first
+        self.assertTrue(d0 is not d1)
+
         # simulate that Tor has really closed the circuit for us
         # this should cause our Deferred to callback
         circuit.update('123 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=FINISHED'.split())
 
+        # if we close *after* the close has succeeded, then we should
+        # immediately "succeed"
+        d2 = circuit.close()
+        self.assertTrue(d1.called)
+
         # confirm that our circuit callback has been triggered already
         self.assertRaises(
             defer.AlreadyCalledError,
-            d.callback,
+            d0.callback,
             "should have been called already"
         )
-        return d
+        return defer.DeferredList([d0, d1, d2])
 
     def test_is_built(self):
         tor = FakeTorController()
@@ -369,6 +463,32 @@ class CircuitTests(unittest.TestCase):
         self.assertTrue(built1.result == circuit)
         self.assertTrue(built2.result == circuit)
 
+    def test_when_closed(self):
+        tor = FakeTorController()
+        a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a')
+        b = FakeRouter('$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5', 'b')
+        c = FakeRouter('$253DFF1838A2B7782BE7735F74E50090D46CA1BC', 'c')
+        tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a
+        tor.routers['$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5'] = b
+        tor.routers['$253DFF1838A2B7782BE7735F74E50090D46CA1BC'] = c
+
+        circuit = Circuit(tor)
+        circuit.listen(tor)
+
+        circuit.update('123 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split())
+        d0 = circuit.when_closed()
+
+        self.assertFalse(d0.called)
+
+        circuit.update('123 BUILT $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split())
+        circuit.update('123 CLOSED'.split())
+
+        d1 = circuit.when_closed()
+
+        self.assertTrue(d0 is not d1)
+        self.assertTrue(d0.called)
+        self.assertTrue(d1.called)
+
     def test_is_built_errback(self):
         tor = FakeTorController()
         a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a')
@@ -382,7 +502,41 @@ class CircuitTests(unittest.TestCase):
         state.circuit_new(circuit)
         d = circuit.when_built()
 
-        state.circuit_closed(circuit)
+        called = []
+
+        def err(f):
+            called.append(f)
+            return None
+        d.addErrback(err)
+
+        state.circuit_closed(circuit, REASON='testing')
+
+        self.assertEqual(1, len(called))
+        self.assertTrue(isinstance(called[0], Failure))
+        self.assertTrue('testing' in str(called[0].value))
+        return d
+
+    def test_stream_success(self):
+        tor = FakeTorController()
+        a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a')
+        tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a
+
+        circuit = Circuit(tor)
+        reactor = Mock()
+
+        circuit.stream_via(
+            reactor, 'torproject.org', 443,
+            Mock(),
+            use_tls=True,
+        )
+
+    def test_circuit_web_agent(self):
+        tor = FakeTorController()
+        a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a')
+        tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a
+
+        circuit = Circuit(tor)
+        reactor = Mock()
 
-        self.assertTrue(d.called)
-        self.assertTrue(isinstance(d.result, Failure))
+        # just testing this doesn't cause an exception
+        circuit.web_agent(reactor, Mock())
diff --git a/test/test_controller.py b/test/test_controller.py
new file mode 100644
index 0000000..c8e1704
--- /dev/null
+++ b/test/test_controller.py
@@ -0,0 +1,1161 @@
+import os
+import functools
+from os.path import join
+from mock import Mock, patch
+from six.moves import StringIO
+
+from twisted.internet.interfaces import IReactorCore
+from twisted.internet.interfaces import IListeningPort
+from twisted.internet.interfaces import IStreamClientEndpoint
+from twisted.internet.address import IPv4Address
+from twisted.internet import defer, error, task
+from twisted.python.failure import Failure
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+from txtorcon import TorConfig
+from txtorcon import TorControlProtocol
+from txtorcon import TorProcessProtocol
+from txtorcon import launch
+from txtorcon import connect
+from txtorcon.controller import _is_non_public_numeric_address, Tor
+from txtorcon.interface import ITorControlProtocol
+from .util import TempDir
+
+from zope.interface import implementer, directlyProvides
+
+
+class FakeProcessTransport(proto_helpers.StringTransportWithDisconnection):
+    pid = -1
+    reactor = None
+
+    def signalProcess(self, signame):
+        assert self.reactor is not None
+        self.reactor.callLater(
+            0,
+            lambda: self.process_protocol.processEnded(
+                Failure(error.ProcessTerminated(signal=signame))
+            )
+        )
+        self.reactor.callLater(
+            0,
+            lambda: self.process_protocol.processExited(
+                Failure(error.ProcessTerminated(signal=signame))
+            )
+        )
+
+    def closeStdin(self):
+        self.process_protocol.outReceived(b"Bootstrap")
+        return
+
+
+class FakeProcessTransportNeverBootstraps(FakeProcessTransport):
+
+    pid = -1
+
+    def closeStdin(self):
+        return
+
+
+class FakeProcessTransportNoProtocol(FakeProcessTransport):
+    def closeStdin(self):
+        pass
+
+
+ at implementer(IListeningPort)
+class FakePort(object):
+    def __init__(self, port):
+        self._port = port
+
+    def startListening(self):
+        pass
+
+    def stopListening(self):
+        pass
+
+    def getHost(self):
+        return IPv4Address('TCP', "127.0.0.1", self._port)
+
+
+ at implementer(IReactorCore)
+class FakeReactor(task.Clock):
+
+    def __init__(self, test, trans, on_protocol, listen_ports=[]):
+        super(FakeReactor, self).__init__()
+        self.test = test
+        self.transport = trans
+        self.transport.reactor = self  # XXX FIXME this is a cycle now
+        self.on_protocol = on_protocol
+        self.listen_ports = listen_ports
+        # util.available_tcp_port ends up 'asking' for free ports via
+        # listenTCP, ultimately, and the answers we send back are from
+        # this list
+
+    def spawnProcess(self, processprotocol, bin, args, env, path,
+                     uid=None, gid=None, usePTY=None, childFDs=None):
+        self.protocol = processprotocol
+        self.protocol.makeConnection(self.transport)
+        self.transport.process_protocol = processprotocol
+        self.on_protocol(self.protocol)
+        return self.transport
+
+    def addSystemEventTrigger(self, *args):
+        self.test.assertEqual(args[0], 'before')
+        self.test.assertEqual(args[1], 'shutdown')
+        # we know this is just for the temporary file cleanup, so we
+        # nuke it right away to avoid polluting /tmp by calling the
+        # callback now.
+        args[2]()
+
+    def removeSystemEventTrigger(self, id):
+        pass
+
+    def listenTCP(self, *args, **kw):
+        port = self.listen_ports.pop()
+        return FakePort(port)
+
+    def connectTCP(self, host, port, factory, timeout=0, bindAddress=None):
+        return
+
+    def connectUNIX(self, *args, **kw):
+        return
+
+
+class LaunchTorTests(unittest.TestCase):
+
+    def setUp(self):
+        self.protocol = TorControlProtocol()
+        self.protocol.connectionMade = lambda: None
+        self.transport = proto_helpers.StringTransport()
+        self.protocol.makeConnection(self.transport)
+        self.clock = task.Clock()
+
+    def test_ctor_timeout_no_ireactortime(self):
+        with self.assertRaises(RuntimeError) as ctx:
+            TorProcessProtocol(lambda: None, timeout=42)
+        self.assertTrue("Must supply an IReactorTime" in str(ctx.exception))
+
+    def _fake_queue(self, cmd):
+        if cmd.split()[0] == 'PROTOCOLINFO':
+            return defer.succeed('AUTH METHODS=NULL')
+        elif cmd == 'GETINFO config/names':
+            return defer.succeed('config/names=')
+        elif cmd == 'GETINFO signal/names':
+            return defer.succeed('signal/names=')
+        elif cmd == 'GETINFO version':
+            return defer.succeed('version=0.1.2.3')
+        elif cmd == 'GETINFO events/names':
+            return defer.succeed('events/names=STATUS_CLIENT')
+        return defer.succeed(None)
+
+    def _fake_event_listener(self, what, cb):
+        if what == 'STATUS_CLIENT':
+            # should ignore non-BOOTSTRAP messages
+            cb('STATUS_CLIENT not-bootstrap')
+            cb('STATUS_CLIENT BOOTSTRAP PROGRESS=100 TAG=foo SUMMARY=bar')
+        return defer.succeed(None)
+
+    @defer.inlineCallbacks
+    def test_launch_tor_unix_controlport(self):
+        trans = FakeProcessTransport()
+        trans.protocol = self.protocol
+        self.protocol.post_bootstrap.callback(self.protocol)
+        self.protocol._set_valid_events("STATUS_CLIENT")
+        self.protocol.add_event_listener = self._fake_event_listener
+        self.protocol.queue_command = self._fake_queue
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 90%\n')
+
+        # launch() auto-discovers a SOCKS port
+        reactor = FakeReactor(self, trans, on_protocol, [9050])
+        reactor.connectUNIX = Mock()
+        # prepare a suitable directory for tor unix socket
+        with TempDir() as tmp:
+            tmpdir = str(tmp)
+            os.chmod(tmpdir, 0o0700)
+            socket_file = join(tmpdir, 'test_socket_file')
+            with patch('txtorcon.controller.UNIXClientEndpoint') as uce:
+                endpoint = Mock()
+                endpoint.connect = Mock(return_value=defer.succeed(self.protocol))
+                uce.return_value = endpoint
+
+                yield launch(
+                    reactor,
+                    control_port="unix:{}".format(socket_file),
+                    tor_binary="/bin/echo",
+                    stdout=Mock(),
+                    stderr=Mock(),
+                )
+
+        self.assertTrue(endpoint.connect.called)
+        self.assertTrue(uce.called)
+        self.assertEqual(
+            socket_file,
+            uce.mock_calls[0][1][1],
+        )
+
+    @defer.inlineCallbacks
+    def test_launch_tor_unix_controlport_wrong_perms(self):
+        reactor = FakeReactor(self, Mock(), None, [9050])
+        with self.assertRaises(ValueError) as ctx:
+            with TempDir() as tmp:
+                tmpdir = str(tmp)
+                os.chmod(tmpdir, 0o0777)
+                socket_file = join(tmpdir, 'socket_test')
+                yield launch(
+                    reactor,
+                    control_port="unix:{}".format(socket_file),
+                    tor_binary="/bin/echo",
+                    stdout=Mock(),
+                    stderr=Mock(),
+                )
+        self.assertTrue(
+            "must only be readable by the user" in str(ctx.exception)
+        )
+
+    @defer.inlineCallbacks
+    def test_launch_tor_unix_controlport_no_directory(self):
+        reactor = FakeReactor(self, Mock(), None, [9050])
+        with self.assertRaises(ValueError) as ctx:
+            socket_file = '/does/not/exist'
+            yield launch(
+                reactor,
+                control_port="unix:{}".format(socket_file),
+                tor_binary="/bin/echo",
+                stdout=Mock(),
+                stderr=Mock(),
+            )
+        self.assertTrue("must exist" in str(ctx.exception))
+
+    @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
+    @defer.inlineCallbacks
+    def test_launch_fails(self, ftb):
+        trans = FakeProcessTransport()
+
+        def on_proto(protocol):
+            protocol.processEnded(
+                Failure(error.ProcessTerminated(12, None, 'statusFIXME'))
+            )
+        reactor = FakeReactor(self, trans, on_proto, [1234, 9052])
+
+        try:
+            yield launch(reactor)
+            self.fail("Should fail")
+        except RuntimeError:
+            pass
+
+        errs = self.flushLoggedErrors(RuntimeError)
+        self.assertEqual(1, len(errs))
+        self.assertTrue(
+            "Tor exited with error-code 12" in str(errs[0])
+        )
+
+    @defer.inlineCallbacks
+    def test_launch_no_ireactorcore(self):
+        try:
+            yield launch(None)
+            self.fail("should get exception")
+        except ValueError as e:
+            self.assertTrue("provide IReactorCore" in str(e))
+
+    @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
+    @patch('txtorcon.controller.TorProcessProtocol')
+    @defer.inlineCallbacks
+    def test_successful_launch(self, tpp, ftb):
+        trans = FakeProcessTransport()
+        reactor = FakeReactor(self, trans, lambda p: None, [1, 2, 3])
+        config = TorConfig()
+
+        def boot(arg=None):
+            config.post_bootstrap.callback(config)
+        config.__dict__['bootstrap'] = Mock(side_effect=boot)
+        config.__dict__['attach_protocol'] = Mock(return_value=defer.succeed(None))
+
+        def foo(*args, **kw):
+            rtn = Mock()
+            rtn.post_bootstrap = defer.succeed(None)
+            rtn.when_connected = Mock(return_value=defer.succeed(rtn))
+            return rtn
+        tpp.side_effect = foo
+
+        tor = yield launch(reactor, _tor_config=config)
+        self.assertTrue(isinstance(tor, Tor))
+
+    @defer.inlineCallbacks
+    def test_quit(self):
+        tor = Tor(Mock(), Mock())
+        tor._protocol = Mock()
+        tor._process_protocol = Mock()
+        yield tor.quit()
+
+    @defer.inlineCallbacks
+    def test_quit_no_protocol(self):
+        tor = Tor(Mock(), Mock())
+        tor._protocol = None
+        tor._process_protocol = None
+        with self.assertRaises(RuntimeError) as ctx:
+            yield tor.quit()
+        self.assertTrue('no protocol instance' in str(ctx.exception))
+
+    @patch('txtorcon.controller.socks')
+    @defer.inlineCallbacks
+    def test_dns_resolve(self, fake_socks):
+        answer = object()
+        cfg = Mock()
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({"SocksPort": "9050"}))
+        tor = Tor(Mock(), proto, _tor_config=cfg)
+        fake_socks.resolve = Mock(return_value=defer.succeed(answer))
+        ans = yield tor.dns_resolve("meejah.ca")
+        self.assertEqual(ans, answer)
+
+    @patch('txtorcon.controller.socks')
+    @defer.inlineCallbacks
+    def test_dns_resolve_existing_socks(self, fake_socks):
+        answer = object()
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({"SocksPort": "9050"}))
+        tor = Tor(Mock(), proto)
+        fake_socks.resolve = Mock(return_value=defer.succeed(answer))
+        ans0 = yield tor.dns_resolve("meejah.ca")
+
+        # do it again to exercise the _default_socks_port() case when
+        # we already got the default
+        fake_socks.resolve = Mock(return_value=defer.succeed(answer))
+        ans1 = yield tor.dns_resolve("meejah.ca")
+        self.assertEqual(ans0, answer)
+        self.assertEqual(ans1, answer)
+
+    @patch('txtorcon.controller.socks')
+    @defer.inlineCallbacks
+    def test_dns_resolve_no_configured_socks(self, fake_socks):
+        answer = object()
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({"SocksPort": "9050"}))
+        cfg = Mock()
+        tor = Tor(Mock(), proto, _tor_config=cfg)
+
+        def boom(*args, **kw):
+            raise RuntimeError("no socks")
+        cfg.socks_endpoint = Mock(side_effect=boom)
+        fake_socks.resolve = Mock(return_value=defer.succeed(answer))
+        ans = yield tor.dns_resolve("meejah.ca")
+
+        self.assertEqual(ans, answer)
+
+    @patch('txtorcon.controller.socks')
+    @defer.inlineCallbacks
+    def test_dns_resolve_ptr(self, fake_socks):
+        answer = object()
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({"SocksPort": "9050"}))
+        tor = Tor(Mock(), proto)
+        fake_socks.resolve_ptr = Mock(return_value=defer.succeed(answer))
+        ans = yield tor.dns_resolve_ptr("4.3.2.1")
+        self.assertEqual(ans, answer)
+
+    @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
+    @defer.inlineCallbacks
+    def test_successful_launch_tcp_control(self, ftb):
+        """
+        full end-to-end test of a launch, faking things out at a "lower
+        level" than most of the other tests
+        """
+        trans = FakeProcessTransport()
+
+        def on_protocol(proto):
+            pass
+        reactor = FakeReactor(self, trans, on_protocol, [1, 2, 3])
+
+        def connect_tcp(host, port, factory, timeout=0, bindAddress=None):
+            addr = Mock()
+            factory.doStart()
+            proto = factory.buildProtocol(addr)
+            tpp = proto._wrappedProtocol
+            tpp.add_event_listener = self._fake_event_listener
+            tpp.queue_command = self._fake_queue
+            proto.makeConnection(Mock())
+            return proto
+        reactor.connectTCP = connect_tcp
+
+        config = TorConfig()
+
+        tor = yield launch(reactor, _tor_config=config, control_port='1234', timeout=30)
+        self.assertTrue(isinstance(tor, Tor))
+
+    @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
+    @patch('txtorcon.controller.sys')
+    @patch('txtorcon.controller.TorProcessProtocol')
+    @defer.inlineCallbacks
+    def test_successful_launch_tcp_control_non_unix(self, tpp, _sys, ftb):
+        _sys.platform = 'not darwin or linux2'
+        trans = FakeProcessTransport()
+        reactor = FakeReactor(self, trans, lambda p: None, [1, 2, 3])
+        config = TorConfig()
+
+        def boot(arg=None):
+            config.post_bootstrap.callback(config)
+        config.__dict__['bootstrap'] = Mock(side_effect=boot)
+        config.__dict__['attach_protocol'] = Mock(return_value=defer.succeed(None))
+
+        def foo(*args, **kw):
+            rtn = Mock()
+            rtn.post_bootstrap = defer.succeed(None)
+            rtn.when_connected = Mock(return_value=defer.succeed(rtn))
+            return rtn
+        tpp.side_effect = foo
+
+        tor = yield launch(reactor, _tor_config=config)
+        self.assertTrue(isinstance(tor, Tor))
+
+    @patch('txtorcon.controller.sys')
+    @patch('txtorcon.controller.pwd')
+    @patch('txtorcon.controller.os.geteuid')
+    @patch('txtorcon.controller.os.chown')
+    def test_launch_root_changes_tmp_ownership(self, chown, euid, _pwd, _sys):
+        _pwd.return_value = 1000
+        _sys.platform = 'linux2'
+        euid.return_value = 0
+        reactor = Mock()
+        directlyProvides(reactor, IReactorCore)
+
+        # note! we're providing enough options here that we react the
+        # "chown" before any 'yield' statements in launch, so we don't
+        # actually have to wait for it... a little rickety, though :/
+        launch(reactor, tor_binary='/bin/echo', user='chuffington', socks_port='1234')
+        self.assertEqual(1, chown.call_count)
+
+    @defer.inlineCallbacks
+    def test_launch_timeout_exception(self):
+        """
+        we provide a timeout, and it expires
+        """
+        trans = Mock()
+        trans.signalProcess = Mock(side_effect=error.ProcessExitedAlready)
+        trans.loseConnection = Mock()
+        on_proto = Mock()
+        react = FakeReactor(self, trans, on_proto, [1234])
+
+        def creator():
+            return defer.succeed(Mock())
+
+        d = launch(
+            reactor=react,
+            tor_binary='/bin/echo',
+            socks_port=1234,
+            timeout=10,
+            connection_creator=creator,
+        )
+        react.advance(12)
+        self.assertTrue(trans.loseConnection.called)
+        with self.assertRaises(RuntimeError) as ctx:
+            yield d
+        self.assertTrue("timeout while launching" in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_launch_timeout_process_exits(self):
+        # cover the "one more edge case" where we get a processEnded()
+        # but we've already "done" a timeout.
+        trans = Mock()
+        trans.signalProcess = Mock()
+        trans.loseConnection = Mock()
+
+        class MyFakeReactor(FakeReactor):
+            def spawnProcess(self, processprotocol, bin, args, env, path,
+                             uid=None, gid=None, usePTY=None, childFDs=None):
+                self.protocol = processprotocol
+                self.protocol.makeConnection(self.transport)
+                self.transport.process_protocol = processprotocol
+                self.on_protocol(self.protocol)
+
+                status = Mock()
+                status.value.exitCode = None
+                processprotocol.processEnded(status)
+                return self.transport
+
+        react = MyFakeReactor(self, trans, Mock(), [1234, 9052])
+
+        d = launch(
+            reactor=react,
+            tor_binary='/bin/echo',
+            timeout=10,
+            data_directory='/dev/null',
+        )
+        react.advance(20)
+
+        try:
+            yield d
+        except RuntimeError as e:
+            self.assertTrue("Tor was killed" in str(e))
+
+        errs = self.flushLoggedErrors(RuntimeError)
+        self.assertEqual(1, len(errs))
+        self.assertTrue("Tor was killed" in str(errs[0]))
+
+    @defer.inlineCallbacks
+    def test_launch_wrong_stdout(self):
+        try:
+            yield launch(
+                FakeReactor(self, Mock(), Mock()),
+                stdout=object(),
+                tor_binary='/bin/echo',
+            )
+            self.fail("Should have thrown an error")
+        except RuntimeError as e:
+            self.assertTrue("file-like object needed" in str(e).lower())
+
+    @defer.inlineCallbacks
+    def test_launch_with_timeout(self):
+        # XXX not entirely sure what this was/is supposed to be
+        # testing, but it covers an extra 7 lines of code??
+        timeout = 5
+
+        def connector(proto, trans):
+            proto._set_valid_events('STATUS_CLIENT')
+            proto.makeConnection(trans)
+            proto.post_bootstrap.callback(proto)
+            return proto.post_bootstrap
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 100%\n')
+
+        trans = FakeProcessTransportNeverBootstraps()
+        trans.protocol = self.protocol
+        creator = functools.partial(connector, Mock(), Mock())
+        react = FakeReactor(self, trans, on_protocol, [1234, 9052])
+
+        with self.assertRaises(RuntimeError) as ctx:
+            d = launch(react, connection_creator=creator,
+                       timeout=timeout, tor_binary='/bin/echo')
+            # FakeReactor is a task.Clock subclass and +1 just to be sure
+            react.advance(timeout + 1)
+            yield d
+        self.assertTrue(
+            'timeout while launching Tor' in str(ctx.exception)
+        )
+        # could/should just use return from this to do asserts?
+        self.flushLoggedErrors(RuntimeError)
+
+    @defer.inlineCallbacks
+    def test_tor_produces_stderr_output(self):
+        def connector(proto, trans):
+            proto._set_valid_events('STATUS_CLIENT')
+            proto.makeConnection(trans)
+            proto.post_bootstrap.callback(proto)
+            return proto.post_bootstrap
+
+        def on_protocol(proto):
+            proto.errReceived('Something went horribly wrong!\n')
+
+        trans = FakeProcessTransport()
+        trans.protocol = Mock()
+        fakeout = StringIO()
+        fakeerr = StringIO()
+        creator = functools.partial(connector, Mock(), Mock())
+        try:
+            yield launch(
+                FakeReactor(self, trans, on_protocol, [1234, 9052]),
+                connection_creator=creator,
+                tor_binary='/bin/echo',
+                stdout=fakeout,
+                stderr=fakeerr,
+            )
+            self.fail()  # should't get callback
+        except RuntimeError as e:
+            self.assertEqual('', fakeout.getvalue())
+            self.assertEqual('Something went horribly wrong!\n', fakeerr.getvalue())
+            self.assertTrue(
+                'Something went horribly wrong!' in str(e)
+            )
+
+    @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
+    @defer.inlineCallbacks
+    def test_tor_connection_fails(self, ftb):
+        trans = FakeProcessTransport()
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 100%\n')
+        reactor = FakeReactor(self, trans, on_protocol, [1, 2, 3])
+
+        fails = ['one']
+
+        def connect_tcp(host, port, factory, timeout=0, bindAddress=None):
+            if len(fails):
+                fails.pop()
+                raise error.CannotListenError('on-purpose-error', None, None)
+
+            addr = Mock()
+            factory.doStart()
+            proto = factory.buildProtocol(addr)
+            tpp = proto._wrappedProtocol
+
+            def fake_event_listener(what, cb):
+                if what == 'STATUS_CLIENT':
+                    # should ignore non-BOOTSTRAP messages
+                    cb('STATUS_CLIENT not-bootstrap')
+                    cb('STATUS_CLIENT BOOTSTRAP PROGRESS=100 TAG=foo SUMMARY=bar')
+                return defer.succeed(None)
+            tpp.add_event_listener = fake_event_listener
+
+            def fake_queue(cmd):
+                if cmd.split()[0] == 'PROTOCOLINFO':
+                    return defer.succeed('AUTH METHODS=NULL')
+                elif cmd == 'GETINFO config/names':
+                    return defer.succeed('config/names=')
+                elif cmd == 'GETINFO signal/names':
+                    return defer.succeed('signal/names=')
+                elif cmd == 'GETINFO version':
+                    return defer.succeed('version=0.1.2.3')
+                elif cmd == 'GETINFO events/names':
+                    return defer.succeed('events/names=STATUS_CLIENT')
+                return defer.succeed(None)
+            tpp.queue_command = fake_queue
+            proto.makeConnection(Mock())
+            return proto
+        reactor.connectTCP = connect_tcp
+        config = TorConfig()
+
+        tor = yield launch(reactor, _tor_config=config, control_port='1234', timeout=30)
+        errs = self.flushLoggedErrors()
+        self.assertTrue(isinstance(tor, Tor))
+        self.assertEqual(1, len(errs))
+
+    def test_tor_connection_user_data_dir(self):
+        """
+        Test that we don't delete a user-supplied data directory.
+        """
+
+        config = TorConfig()
+        config.OrPort = 1234
+
+        class Connector:
+            def __call__(self, proto, trans):
+                proto._set_valid_events('STATUS_CLIENT')
+                proto.makeConnection(trans)
+                proto.post_bootstrap.callback(proto)
+                return proto.post_bootstrap
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 90%\n')
+
+        with TempDir() as tmp:
+            my_dir = str(tmp)
+            config.DataDirectory = my_dir
+            trans = FakeProcessTransport()
+            trans.protocol = self.protocol
+            creator = functools.partial(Connector(), self.protocol, self.transport)
+            d = launch(
+                FakeReactor(self, trans, on_protocol, [1234, 9051]),
+                connection_creator=creator,
+                tor_binary='/bin/echo',
+                data_directory=my_dir,
+                control_port=0,
+            )
+
+            def still_have_data_dir(tor, tester):
+                tor._process_protocol.cleanup()  # FIXME? not really unit-testy as this is sort of internal function
+                tester.assertTrue(os.path.exists(my_dir))
+
+            d.addCallback(still_have_data_dir, self)
+            d.addErrback(self.fail)
+            return d
+
+    def _test_tor_connection_user_control_port(self):
+        """
+        Confirm we use a user-supplied control-port properly
+        """
+
+        config = TorConfig()
+        config.OrPort = 1234
+        config.ControlPort = 4321
+
+        class Connector:
+            def __call__(self, proto, trans):
+                proto._set_valid_events('STATUS_CLIENT')
+                proto.makeConnection(trans)
+                proto.post_bootstrap.callback(proto)
+                return proto.post_bootstrap
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 90%\n')
+            proto.outReceived(b'Bootstrapped 100%\n')
+
+        trans = FakeProcessTransport()
+        trans.protocol = self.protocol
+        creator = functools.partial(Connector(), self.protocol, self.transport)
+        d = launch(
+            FakeReactor(self, trans, on_protocol, [9052]),
+            connection_creator=creator,
+            tor_binary='/bin/echo',
+            socks_port=1234,
+        )
+
+        def check_control_port(proto, tester):
+            # we just want to ensure launch() didn't mess with
+            # the controlport we set
+            tester.assertEqual(config.ControlPort, 4321)
+
+        d.addCallback(check_control_port, self)
+        d.addErrback(self.fail)
+        return d
+
+    @defer.inlineCallbacks
+    def _test_tor_connection_default_control_port(self):
+        """
+        Confirm a default control-port is set if not user-supplied.
+        """
+
+        class Connector:
+            def __call__(self, proto, trans):
+                proto._set_valid_events('STATUS_CLIENT')
+                proto.makeConnection(trans)
+                proto.post_bootstrap.callback(proto)
+                return proto.post_bootstrap
+
+        def on_protocol(proto):
+            proto.outReceived(b'Bootstrapped 90%\n')
+            proto.outReceived(b'Bootstrapped 100%\n')
+
+        trans = FakeProcessTransport()
+        trans.protocol = self.protocol
+        creator = functools.partial(Connector(), self.protocol, self.transport)
+        tor = yield launch(
+            FakeReactor(self, trans, on_protocol, [9052]),
+            connection_creator=creator,
+            tor_binary='/bin/echo',
+            socks_port=1234,
+        )
+
+        cfg = yield tor.get_config()
+        self.assertEqual(cfg.ControlPort, 9052)
+
+    def test_progress_updates(self):
+        self.got_progress = False
+
+        def confirm_progress(p, t, s):
+            self.assertEqual(p, 10)
+            self.assertEqual(t, 'tag')
+            self.assertEqual(s, 'summary')
+            self.got_progress = True
+        process = TorProcessProtocol(None, confirm_progress)
+        process.progress(10, 'tag', 'summary')
+        self.assertTrue(self.got_progress)
+
+    def test_quit_process(self):
+        process = TorProcessProtocol(None)
+        process.transport = Mock()
+
+        d = process.quit()
+        self.assertFalse(d.called)
+
+        process.processExited(Failure(error.ProcessTerminated(exitCode=15)))
+        self.assertTrue(d.called)
+        process.processEnded(Failure(error.ProcessDone(None)))
+        self.assertTrue(d.called)
+        errs = self.flushLoggedErrors()
+        self.assertEqual(1, len(errs))
+        self.assertTrue("Tor exited with error-code" in str(errs[0]))
+
+    def test_quit_process_already(self):
+        process = TorProcessProtocol(None)
+        process.transport = Mock()
+
+        def boom(sig):
+            self.assertEqual(sig, 'TERM')
+            raise error.ProcessExitedAlready()
+        process.transport.signalProcess = Mock(side_effect=boom)
+
+        d = process.quit()
+        process.processEnded(Failure(error.ProcessDone(None)))
+        self.assertTrue(d.called)
+        errs = self.flushLoggedErrors()
+        self.assertEqual(1, len(errs))
+        self.assertTrue("Tor exited with error-code" in str(errs[0]))
+
+    @defer.inlineCallbacks
+    def test_quit_process_error(self):
+        process = TorProcessProtocol(None)
+        process.transport = Mock()
+
+        def boom(sig):
+            self.assertEqual(sig, 'TERM')
+            raise RuntimeError("Something bad")
+        process.transport.signalProcess = Mock(side_effect=boom)
+
+        try:
+            yield process.quit()
+        except RuntimeError as e:
+            self.assertEqual("Something bad", str(e))
+
+    def XXXtest_status_updates(self):
+        process = TorProcessProtocol(None)
+        process.status_client("NOTICE CONSENSUS_ARRIVED")
+
+    def XXXtest_tor_launch_success_then_shutdown(self):
+        """
+        There was an error where we double-callbacked a deferred,
+        i.e. success and then shutdown. This repeats it.
+        """
+        process = TorProcessProtocol(None)
+        process.status_client(
+            'STATUS_CLIENT BOOTSTRAP PROGRESS=100 TAG=foo SUMMARY=cabbage'
+        )
+        # XXX why this assert?
+        self.assertEqual(None, process._connected_cb)
+
+        class Value(object):
+            exitCode = 123
+
+        class Status(object):
+            value = Value()
+        process.processEnded(Status())
+        self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
+
+    @defer.inlineCallbacks
+    def test_launch_no_control_port(self):
+        '''
+        See Issue #80. This allows you to launch tor with a TorConfig
+        with ControlPort=0 in case you don't want a control connection
+        at all. In this case you get back a TorProcessProtocol and you
+        own both pieces. (i.e. you have to kill it yourself).
+        '''
+
+        trans = FakeProcessTransportNoProtocol()
+        trans.protocol = self.protocol
+
+        def creator(*args, **kw):
+            print("Bad: connection creator called")
+            self.fail()
+
+        def on_protocol(proto):
+            self.process_proto = proto
+            proto.outReceived(b'Bootstrapped 90%\n')
+            proto.outReceived(b'Bootstrapped 100%\n')
+
+        reactor = FakeReactor(self, trans, on_protocol, [9052, 9999])
+
+        tor = yield launch(
+            reactor=reactor,
+            connection_creator=creator,
+            tor_binary='/bin/echo',
+            socks_port=1234,
+            control_port=0,
+        )
+        self.assertEqual(tor._process_protocol, self.process_proto)
+        d = tor.quit()
+        reactor.advance(0)
+        yield d
+        errs = self.flushLoggedErrors()
+        self.assertEqual(1, len(errs))
+        self.assertTrue("Tor was killed" in str(errs[0]))
+
+
+def create_endpoint(*args, **kw):
+    ep = Mock()
+    directlyProvides(ep, IStreamClientEndpoint)
+    return ep
+
+
+def create_endpoint_fails(*args, **kw):
+    def go_boom(*args, **kw):
+        raise RuntimeError("boom")
+
+    ep = Mock(side_effect=go_boom)
+    directlyProvides(ep, IStreamClientEndpoint)
+    return ep
+
+
+class ConnectTorTests(unittest.TestCase):
+
+    @patch('txtorcon.controller.TorConfig')
+    @patch('txtorcon.controller.UNIXClientEndpoint', side_effect=create_endpoint)
+    @patch('txtorcon.controller.TCP4ClientEndpoint', side_effect=create_endpoint)
+    @defer.inlineCallbacks
+    def test_connect_defaults(self, fake_cfg, fake_unix, fake_tcp):
+        """
+        happy-path test, ensuring there are no exceptions
+        """
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        yield connect(reactor)
+
+    @patch('txtorcon.controller.TorConfig')
+    @defer.inlineCallbacks
+    def test_connect_provide_endpoint(self, fake_cfg):
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        ep = Mock()
+        with self.assertRaises(ValueError) as ctx:
+            yield connect(reactor, ep)
+        self.assertTrue('IStreamClientEndpoint' in str(ctx.exception))
+
+    @patch('txtorcon.controller.TorConfig')
+    @defer.inlineCallbacks
+    def test_connect_provide_multiple_endpoints(self, fake_cfg):
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        ep0 = Mock()
+        ep1 = Mock()
+        with self.assertRaises(ValueError) as ctx:
+            yield connect(reactor, [ep0, ep1])
+        self.assertTrue('IStreamClientEndpoint' in str(ctx.exception))
+
+    @patch('txtorcon.controller.TorConfig')
+    @defer.inlineCallbacks
+    def test_connect_multiple_endpoints_error(self, fake_cfg):
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        ep0 = Mock()
+
+        def boom(*args, **kw):
+            raise RuntimeError("the bad thing")
+        ep0.connect = boom
+        directlyProvides(ep0, IStreamClientEndpoint)
+        with self.assertRaises(RuntimeError) as ctx:
+            yield connect(reactor, ep0)
+        self.assertEqual("the bad thing", str(ctx.exception))
+
+    @patch('txtorcon.controller.TorConfig')
+    @defer.inlineCallbacks
+    def test_connect_multiple_endpoints_many_errors(self, fake_cfg):
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        ep0 = Mock()
+        ep1 = Mock()
+
+        def boom0(*args, **kw):
+            raise RuntimeError("the bad thing")
+
+        def boom1(*args, **kw):
+            raise RuntimeError("more sadness")
+
+        ep0.connect = boom0
+        ep1.connect = boom1
+        directlyProvides(ep0, IStreamClientEndpoint)
+        directlyProvides(ep1, IStreamClientEndpoint)
+
+        with self.assertRaises(RuntimeError) as ctx:
+            yield connect(reactor, [ep0, ep1])
+        self.assertTrue("the bad thing" in str(ctx.exception))
+        self.assertTrue("more sadness" in str(ctx.exception))
+
+    @patch('txtorcon.controller.TorConfig')
+    @defer.inlineCallbacks
+    def test_connect_success(self, fake_cfg):
+        transport = Mock()
+        reactor = FakeReactor(self, transport, lambda: None)
+        torcfg = Mock()
+        fake_cfg.from_protocol = Mock(return_value=torcfg)
+        ep0 = Mock()
+        proto = object()
+        torcfg.protocol = proto
+        ep0.connect = Mock(return_value=proto)
+        directlyProvides(ep0, IStreamClientEndpoint)
+
+        ans = yield connect(reactor, [ep0])
+        cfg = yield ans.get_config()
+        self.assertEqual(cfg, torcfg)
+        self.assertEqual(ans.protocol, proto)
+
+
+class WebAgentTests(unittest.TestCase):
+
+    def setUp(self):
+        proto = Mock()
+        self.pool = Mock()
+        self.expected_response = object()
+        proto.request = Mock(return_value=defer.succeed(self.expected_response))
+        self.pool.getConnection = Mock(return_value=defer.succeed(proto))
+
+    @defer.inlineCallbacks
+    def test_web_agent_defaults(self):
+        reactor = Mock()
+        # XXX is there a faster way to do this? better reactor fake?
+        fake_host = Mock()
+        fake_host.port = 1234
+        fake_port = Mock()
+        fake_port.getHost = Mock(return_value=fake_host)
+        reactor.listenTCP = Mock(return_value=fake_port)
+        cfg = Mock()
+        cfg.create_socks_endpoint = Mock(return_value=defer.succeed("9050"))
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({}))
+        directlyProvides(proto, ITorControlProtocol)
+
+        tor = Tor(reactor, proto, _tor_config=cfg)
+        try:
+            agent = tor.web_agent(pool=self.pool)
+        except ImportError as e:
+            if 'IAgentEndpointFactory' in str(e):
+                print("Skipping; appears we don't have web support")
+                return
+
+        resp = yield agent.request('GET', b'meejah.ca')
+        self.assertEqual(self.expected_response, resp)
+
+    @defer.inlineCallbacks
+    def test_web_agent_deferred(self):
+        socks_d = defer.succeed("9151")
+        reactor = Mock()
+        cfg = Mock()
+        proto = Mock()
+        directlyProvides(proto, ITorControlProtocol)
+
+        tor = Tor(reactor, proto, _tor_config=cfg)
+        agent = tor.web_agent(pool=self.pool, socks_endpoint=socks_d)
+
+        resp = yield agent.request('GET', b'meejah.ca')
+        self.assertEqual(self.expected_response, resp)
+
+    @defer.inlineCallbacks
+    def test_web_agent_endpoint(self):
+        socks = Mock()
+        directlyProvides(socks, IStreamClientEndpoint)
+        reactor = Mock()
+        cfg = Mock()
+        proto = Mock()
+        directlyProvides(proto, ITorControlProtocol)
+
+        tor = Tor(reactor, proto, _tor_config=cfg)
+        agent = tor.web_agent(pool=self.pool, socks_endpoint=socks)
+
+        resp = yield agent.request('GET', b'meejah.ca')
+        self.assertEqual(self.expected_response, resp)
+
+    @defer.inlineCallbacks
+    def test_web_agent_error(self):
+        reactor = Mock()
+        cfg = Mock()
+        proto = Mock()
+        directlyProvides(proto, ITorControlProtocol)
+
+        tor = Tor(reactor, proto, _tor_config=cfg)
+        with self.assertRaises(ValueError) as ctx:
+            agent = tor.web_agent(pool=self.pool, socks_endpoint=object())
+            yield agent.request('GET', b'meejah.ca')
+        self.assertTrue("'socks_endpoint' should be" in str(ctx.exception))
+
+
+class TorAttributeTests(unittest.TestCase):
+
+    def setUp(self):
+        reactor = Mock()
+        proto = Mock()
+        directlyProvides(proto, ITorControlProtocol)
+        self.cfg = Mock()
+        self.tor = Tor(reactor, proto, _tor_config=self.cfg)
+
+    def test_process(self):
+        with self.assertRaises(Exception) as ctx:
+            self.tor.process
+        self.assertTrue('not launched by us' in str(ctx.exception))
+
+    def test_when_connected_already(self):
+        tpp = TorProcessProtocol(lambda: None)
+        # hmmmmmph, delving into internal state "because way shorter
+        # test"
+        tpp._connected_listeners = None
+        d = tpp.when_connected()
+
+        self.assertTrue(d.called)
+        self.assertEqual(d.result, tpp)
+
+    def test_process_exists(self):
+        gold = object()
+        self.tor._process_protocol = gold
+        self.assertEqual(gold, self.tor.process)
+
+    def test_protocol_exists(self):
+        self.tor.protocol
+
+    def test_version_passthrough(self):
+        self.tor.version
+
+
+class TorAttributeTestsNoConfig(unittest.TestCase):
+
+    def setUp(self):
+        reactor = Mock()
+        proto = Mock()
+        directlyProvides(proto, ITorControlProtocol)
+        self.tor = Tor(reactor, proto)
+
+    @defer.inlineCallbacks
+    def test_get_config(self):
+        with patch('txtorcon.controller.TorConfig') as torcfg:
+            gold = object()
+            torcfg.from_protocol = Mock(return_value=defer.succeed(gold))
+            cfg = yield self.tor.get_config()
+            self.assertEqual(gold, cfg)
+
+
+class TorStreamTests(unittest.TestCase):
+
+    def setUp(self):
+        reactor = Mock()
+        proto = Mock()
+        proto.get_conf = Mock(return_value=defer.succeed({"SocksPort": "9050"}))
+        self.cfg = Mock()
+        self.tor = Tor(reactor, proto, _tor_config=self.cfg)
+
+    def test_sanity(self):
+        self.assertTrue(_is_non_public_numeric_address(u'10.0.0.0'))
+        self.assertTrue(_is_non_public_numeric_address(u'::1'))
+
+    def test_v6(self):
+        import ipaddress
+        ipaddress.ip_address(u'2603:3023:807:3d00:21e:52ff:fe71:a4ce')
+
+    def test_stream_private_ip(self):
+        with self.assertRaises(Exception) as ctx:
+            self.tor.stream_via('10.0.0.1', '1234')
+        self.assertTrue("isn't going to work over Tor", str(ctx.exception))
+
+    def test_stream_v6(self):
+        with self.assertRaises(Exception) as ctx:
+            self.tor.stream_via(u'::1', '1234')
+        self.assertTrue("isn't going to work over Tor", str(ctx.exception))
+
+    def test_public_v6(self):
+        # should not be an error
+        self.tor.stream_via(u'2603:3023:807:3d00:21e:52ff:fe71:a4ce', '4321')
+
+    def test_public_v4(self):
+        # should not be an error
+        self.tor.stream_via(u'8.8.8.8', '4321')
+
+    def test_stream_host(self):
+        self.tor.stream_via(b'meejah.ca', '1234')
+
+
+class IteratorTests(unittest.TestCase):
+    def XXXtest_iterate_torconfig(self):
+        cfg = TorConfig()
+        cfg.FooBar = 'quux'
+        cfg.save()
+        cfg.Quux = 'blimblam'
+
+        keys = sorted([k for k in cfg])
+
+        self.assertEqual(['FooBar', 'Quux'], keys)
+
+
+class FactoryFunctionTests(unittest.TestCase):
+    """
+    Mostly simple 'does not blow up' sanity checks of simple
+    factory-functions.
+    """
+
+    @defer.inlineCallbacks
+    def test_create_state(self):
+        tor = Tor(Mock(), Mock())
+        with patch('txtorcon.controller.TorState') as ts:
+            ts.post_boostrap = defer.succeed('boom')
+            yield tor.create_state()
+        # no assertions; we just testing this doesn't raise
+
+    def test_str(self):
+        tor = Tor(Mock(), Mock())
+        str(tor)
+        # just testing the __str__ method doesn't explode
diff --git a/test/test_endpoints.py b/test/test_endpoints.py
index 5ad0fae..8e2ca57 100644
--- a/test/test_endpoints.py
+++ b/test/test_endpoints.py
@@ -1,45 +1,56 @@
-import os
-import shutil
-import tempfile
+from __future__ import print_function
 
+import os
 from mock import patch
 from mock import Mock, MagicMock
 
-from zope.interface import implements
+from zope.interface import implementer, directlyProvides
 
 from twisted.trial import unittest
 from twisted.test import proto_helpers
-from twisted.internet import defer, error, task, tcp
-from twisted.internet.endpoints import TCP4ServerEndpoint
+from twisted.internet import defer, error, tcp, unix
 from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.endpoints import UNIXClientEndpoint
 from twisted.internet.endpoints import serverFromString
 from twisted.internet.endpoints import clientFromString
 from twisted.python.failure import Failure
 from twisted.internet.error import ConnectionRefusedError
+from twisted.internet.interfaces import IStreamClientEndpoint
 from twisted.internet.interfaces import IReactorCore
-from twisted.internet.interfaces import IProtocolFactory
 from twisted.internet.interfaces import IProtocol
 from twisted.internet.interfaces import IReactorTCP
 from twisted.internet.interfaces import IListeningPort
 from twisted.internet.interfaces import IAddress
 
 from txtorcon import TorControlProtocol
-from txtorcon import ITorControlProtocol
 from txtorcon import TorConfig
-from txtorcon import launch_tor
 from txtorcon import TCPHiddenServiceEndpoint
 from txtorcon import TorClientEndpoint
-from txtorcon import TorNotFound
-from txtorcon import TCPHiddenServiceEndpointParser
+# from txtorcon import TorClientEndpointStringParser
 from txtorcon import IProgressProvider
 from txtorcon import TorOnionAddress
 from txtorcon.util import NoOpProtocolFactory
+from txtorcon.util import SingleObserver
 from txtorcon.endpoints import get_global_tor                       # FIXME
-from txtorcon.endpoints import _HAVE_TLS
+from txtorcon.endpoints import _create_socks_endpoint
+from txtorcon.circuit import TorCircuitEndpoint, _get_circuit_attacher
+from txtorcon.controller import Tor
+from txtorcon.socks import _TorSocksFactory
+
+from . import util
+from .test_torconfig import FakeControlProtocol  # FIXME
+
 
-import util
+ at implementer(IReactorCore)
+class MockReactor(Mock):
+    """
+    Just so that our 'provides IReactorCore' assertions pass, but it's
+    still "just a Mock".
+    """
+    pass
 
 
+ at patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo')
 class EndpointTests(unittest.TestCase):
 
     def setUp(self):
@@ -57,11 +68,13 @@ class EndpointTests(unittest.TestCase):
         )
         self.config = TorConfig(self.protocol)
         self.protocol.answers.append(
-            'config/names=\nHiddenServiceOptions Virtual'
+            'config/names=\nHiddenServiceOptions Virtual\nControlPort LineList'
         )
         self.protocol.answers.append('HiddenServiceOptions')
+        # why do i have to pass a dict for this V but not this ^
+        self.protocol.answers.append({'ControlPort': '37337'})
         self.patcher = patch(
-            'txtorcon.torconfig.find_tor_binary',
+            'txtorcon.controller.find_tor_binary',
             return_value='/not/tor'
         )
         self.patcher.start()
@@ -74,7 +87,7 @@ class EndpointTests(unittest.TestCase):
         self.patcher.stop()
 
     @defer.inlineCallbacks
-    def test_global_tor(self):
+    def test_global_tor(self, ftb):
         config = yield get_global_tor(
             Mock(),
             _tor_launcher=lambda x, y, z: True
@@ -82,24 +95,27 @@ class EndpointTests(unittest.TestCase):
         self.assertEqual(0, config.SOCKSPort)
 
     @defer.inlineCallbacks
-    def test_global_tor_error(self):
-        config0 = yield get_global_tor(
-            Mock(),
+    def test_global_tor_error(self, ftb):
+        yield get_global_tor(
+            reactor=Mock(),
             _tor_launcher=lambda x, y, z: True
         )
         # now if we specify a control_port it should be an error since
         # the above should have launched one.
         try:
-            config1 = yield get_global_tor(Mock(), control_port=111,
-                                           _tor_launcher=lambda x, y, z: True)
+            yield get_global_tor(
+                reactor=Mock(),
+                control_port=111,
+                _tor_launcher=lambda x, y, z: True
+            )
             self.fail()
-        except RuntimeError as e:
+        except RuntimeError:
             # should be an error
             pass
 
     @defer.inlineCallbacks
-    def test_endpoint_properties(self):
-        ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80)
+    def test_endpoint_properties(self, ftb):
+        ep = yield TCPHiddenServiceEndpoint.private_tor(self.reactor, 80)
         self.assertEqual(None, ep.onion_private_key)
         self.assertEqual(None, ep.onion_uri)
         ep.hiddenservice = Mock()
@@ -107,35 +123,37 @@ class EndpointTests(unittest.TestCase):
         self.assertEqual('mumble', ep.onion_private_key)
 
     @defer.inlineCallbacks
-    def test_private_tor(self):
+    def test_private_tor(self, ftb):
         m = Mock()
         from txtorcon import endpoints
         endpoints.launch_tor = m
-        ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80,
-                                                        control_port=1234)
+        yield TCPHiddenServiceEndpoint.private_tor(
+            Mock(), 80,
+            control_port=1234,
+        )
         self.assertTrue(m.called)
 
     @defer.inlineCallbacks
-    def test_private_tor_no_control_port(self):
+    def test_private_tor_no_control_port(self, ftb):
         m = Mock()
         from txtorcon import endpoints
         endpoints.launch_tor = m
-        ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80)
+        yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80)
         self.assertTrue(m.called)
 
     @defer.inlineCallbacks
-    def test_system_tor(self):
-        from test_torconfig import FakeControlProtocol
+    def test_system_tor(self, ftb):
 
-        def boom(*args):
+        def boom():
             # why does the new_callable thing need a callable that
             # returns a callable? Feels like I must be doing something
             # wrong somewhere...
             def bam(*args, **kw):
-                return self.protocol
+                self.config.bootstrap()
+                return defer.succeed(Tor(Mock(), self.protocol, _tor_config=self.config))
             return bam
         with patch('txtorcon.endpoints.launch_tor') as launch_mock:
-            with patch('txtorcon.endpoints.build_tor_connection', new_callable=boom) as btc:
+            with patch('txtorcon.controller.connect', new_callable=boom):
                 client = clientFromString(
                     self.reactor,
                     "tcp:host=localhost:port=9050"
@@ -154,7 +172,7 @@ class EndpointTests(unittest.TestCase):
                 self.assertFalse(launch_mock.called)
 
     @defer.inlineCallbacks
-    def test_basic(self):
+    def test_basic(self, ftb):
         listen = RuntimeError("listen")
         connect = RuntimeError("connect")
         reactor = proto_helpers.RaisingMemoryReactor(listen, connect)
@@ -166,7 +184,7 @@ class EndpointTests(unittest.TestCase):
         self.assertTrue(IProgressProvider.providedBy(ep))
 
         try:
-            port = yield ep.listen(NoOpProtocolFactory())
+            yield ep.listen(NoOpProtocolFactory())
             self.fail("Should have been an exception")
         except RuntimeError as e:
             # make sure we called listenTCP not connectTCP
@@ -174,7 +192,7 @@ class EndpointTests(unittest.TestCase):
 
         repr(self.config.HiddenServices)
 
-    def test_progress_updates(self):
+    def test_progress_updates(self, ftb):
         config = TorConfig()
         ep = TCPHiddenServiceEndpoint(self.reactor, config, 123)
 
@@ -187,24 +205,28 @@ class EndpointTests(unittest.TestCase):
         ep._tor_progress_update(*args)
         self.assertTrue(ding.called_with(*args))
 
-    @patch('txtorcon.endpoints.launch_tor')
-    def test_progress_updates_private_tor(self, tor):
-        ep = TCPHiddenServiceEndpoint.private_tor(self.reactor, 1234)
-        tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar')
-        return ep
-
-    def __test_progress_updates_system_tor(self):
-        ep = TCPHiddenServiceEndpoint.system_tor(self.reactor, 1234)
+    def test_progress_updates_private_tor(self, ftb):
+        with patch('txtorcon.endpoints.launch_tor') as tor:
+            ep = TCPHiddenServiceEndpoint.private_tor(self.reactor, 1234)
+            self.assertEqual(len(tor.mock_calls), 1)
+            tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar')
+            return ep
+
+    def test_progress_updates_system_tor(self, ftb):
+        control_ep = Mock()
+        control_ep.connect = Mock(return_value=defer.succeed(None))
+        directlyProvides(control_ep, IStreamClientEndpoint)
+        ep = TCPHiddenServiceEndpoint.system_tor(self.reactor, control_ep, 1234)
         ep._tor_progress_update(40, "FOO", "foo to bar")
         return ep
 
-    @patch('txtorcon.endpoints.get_global_tor')
-    def test_progress_updates_global_tor(self, tor):
-        ep = TCPHiddenServiceEndpoint.global_tor(self.reactor, 1234)
-        tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar')
-        return ep
+    def test_progress_updates_global_tor(self, ftb):
+        with patch('txtorcon.endpoints.get_global_tor') as tor:
+            ep = TCPHiddenServiceEndpoint.global_tor(self.reactor, 1234)
+            tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar')
+            return ep
 
-    def test_hiddenservice_key_unfound(self):
+    def test_hiddenservice_key_unfound(self, ftb):
         ep = TCPHiddenServiceEndpoint.private_tor(
             self.reactor,
             1234,
@@ -221,7 +243,7 @@ class EndpointTests(unittest.TestCase):
         self.assertEqual(ep.onion_private_key, None)
         return ep
 
-    def test_multiple_listen(self):
+    def test_multiple_listen(self, ftb):
         ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123)
         d0 = ep.listen(NoOpProtocolFactory())
 
@@ -244,16 +266,16 @@ class EndpointTests(unittest.TestCase):
         d0.addCallback(check).addErrback(self.fail)
         return d0
 
-    def test_already_bootstrapped(self):
+    def test_already_bootstrapped(self, ftb):
         self.config.bootstrap()
         ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123)
         d = ep.listen(NoOpProtocolFactory())
         return d
 
     @defer.inlineCallbacks
-    def test_explicit_data_dir(self):
-        d = tempfile.mkdtemp()
-        try:
+    def test_explicit_data_dir(self, ftb):
+        with util.TempDir() as tmp:
+            d = str(tmp)
             with open(os.path.join(d, 'hostname'), 'w') as f:
                 f.write('public')
 
@@ -262,16 +284,13 @@ class EndpointTests(unittest.TestCase):
 
             # make sure listen() correctly configures our hidden-serivce
             # with the explicit directory we passed in above
-            port = yield ep.listen(NoOpProtocolFactory())
+            yield ep.listen(NoOpProtocolFactory())
 
             self.assertEqual(1, len(config.HiddenServices))
             self.assertEqual(config.HiddenServices[0].dir, d)
             self.assertEqual(config.HiddenServices[0].hostname, 'public')
 
-        finally:
-            shutil.rmtree(d, ignore_errors=True)
-
-    def test_failure(self):
+    def test_failure(self, ftb):
         self.reactor.failures = 1
         ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123)
         d = ep.listen(NoOpProtocolFactory())
@@ -283,7 +302,7 @@ class EndpointTests(unittest.TestCase):
         self.assertEqual(failure.type, error.CannotListenError)
         return None
 
-    def test_parse_via_plugin(self):
+    def test_parse_via_plugin(self, ftb):
         # make sure we have a valid thing from get_global_tor without
         # actually launching tor
         config = TorConfig()
@@ -302,7 +321,7 @@ class EndpointTests(unittest.TestCase):
         self.assertEqual(ep.local_port, 1234)
         self.assertEqual(ep.hidden_service_dir, '/foo/bar')
 
-    def test_parse_user_path(self):
+    def test_parse_user_path(self, ftb):
         # this makes sure we expand users and symlinks in
         # hiddenServiceDir args. see Issue #77
 
@@ -328,7 +347,7 @@ class EndpointTests(unittest.TestCase):
             ep.hidden_service_dir
         )
 
-    def test_parse_relative_path(self):
+    def test_parse_relative_path(self, ftb):
         # this makes sure we convert a relative path to absolute
         # hiddenServiceDir args. see Issue #77
 
@@ -365,7 +384,7 @@ class EndpointTests(unittest.TestCase):
             os.chdir(orig)
 
     @defer.inlineCallbacks
-    def test_stealth_auth(self):
+    def test_stealth_auth(self, ftb):
         '''
         make sure we produce a HiddenService instance with stealth-auth
         lines if we had authentication specified in the first place.
@@ -380,16 +399,33 @@ class EndpointTests(unittest.TestCase):
         d = ep.listen(NoOpProtocolFactory())
 
         def foo(fail):
-            print "ERROR", fail
+            print("ERROR", fail)
         d.addErrback(foo)
-        port = yield d
+        yield d  # returns 'port'
         self.assertEqual(1, len(config.HiddenServices))
         self.assertEqual(config.HiddenServices[0].dir, '/dev/null')
-        self.assertEqual(config.HiddenServices[0].authorize_client[0], 'stealth alice,bob')
+        self.assertEqual(
+            config.HiddenServices[0].authorize_client[0],
+            'stealth alice,bob'
+        )
         self.assertEqual(None, ep.onion_uri)
+        # XXX cheating; private API
         config.HiddenServices[0].hostname = 'oh my'
         self.assertEqual('oh my', ep.onion_uri)
 
+    @defer.inlineCallbacks
+    def test_factory(self, ftb):
+        reactor = Mock()
+        cp = Mock()
+        cp.get_conf = Mock(return_value=defer.succeed(dict()))
+
+        with patch(u'txtorcon.endpoints.available_tcp_port', return_value=9999):
+            ep = yield TorClientEndpoint.from_connection(reactor, cp, 'localhost', 1234)
+
+        self.assertTrue(isinstance(ep, TorClientEndpoint))
+        self.assertEqual(ep.host, 'localhost')
+        self.assertEqual(ep.port, 1234)
+
 
 class EndpointLaunchTests(unittest.TestCase):
 
@@ -405,8 +441,8 @@ class EndpointLaunchTests(unittest.TestCase):
         hash(addr)
 
     def test_onion_parse_unix_socket(self):
-        r = Mock()
-        ep = serverFromString(r, "onion:80:controlPort=/tmp/foo")
+        r = proto_helpers.MemoryReactor()
+        serverFromString(r, "onion:80:controlPort=/tmp/foo")
 
     @patch('txtorcon.TCPHiddenServiceEndpoint.system_tor')
     @patch('txtorcon.TCPHiddenServiceEndpoint.global_tor')
@@ -421,7 +457,7 @@ class EndpointLaunchTests(unittest.TestCase):
 
         reactor = proto_helpers.MemoryReactor()
         ep = serverFromString(reactor, 'onion:8888')
-        r = yield ep.listen(NoOpProtocolFactory())
+        yield ep.listen(NoOpProtocolFactory())
         self.assertEqual(global_tor.call_count, 1)
         self.assertEqual(private_tor.call_count, 0)
         self.assertEqual(system_tor.call_count, 0)
@@ -442,7 +478,7 @@ class EndpointLaunchTests(unittest.TestCase):
             reactor,
             'onion:8888:controlPort=9055:localPort=1234'
         )
-        r = yield ep.listen(NoOpProtocolFactory())
+        yield ep.listen(NoOpProtocolFactory())
         self.assertEqual(global_tor.call_count, 0)
         self.assertEqual(private_tor.call_count, 0)
         self.assertEqual(system_tor.call_count, 1)
@@ -456,25 +492,25 @@ class EndpointLaunchTests(unittest.TestCase):
 
 
 # FIXME should probably go somewhere else, so other tests can easily use these.
+ at implementer(IProtocol)
 class FakeProtocol(object):
-    implements(IProtocol)
 
     def dataReceived(self, data):
-        print "DATA", data
+        print("DATA", data)
 
     def connectionLost(self, reason):
-        print "LOST", reason
+        print("LOST", reason)
 
     def makeConnection(self, transport):
-        print "MAKE", transport
+        print("MAKE", transport)
         transport.protocol = self
 
     def connectionMade(self):
-        print "MADE!"
+        print("MADE!")
 
 
+ at implementer(IAddress)
 class FakeAddress(object):
-    implements(IAddress)
 
     compareAttributes = ('type', 'host', 'port')
     type = 'fakeTCP'
@@ -491,8 +527,8 @@ class FakeAddress(object):
         return hash((self.type, self.host, self.port))
 
 
+ at implementer(IListeningPort)
 class FakeListeningPort(object):
-    implements(IListeningPort)
 
     def __init__(self, port):
         self.port = port
@@ -508,17 +544,13 @@ class FakeListeningPort(object):
 
 
 def port_generator():
-    for x in xrange(65535, 0, -1):
+    # XXX six has xrange/range stuff?
+    for x in range(65535, 0, -1):
         yield x
 
 
-from test_torconfig import FakeReactor  # FIXME put in util or something?
-from test_torconfig import FakeProcessTransport  # FIXME importing from other test sucks
-from test_torconfig import FakeControlProtocol  # FIXME
-
-
-class FakeReactorTcp(FakeReactor):
-    implements(IReactorTCP)
+ at implementer(IReactorTCP, IReactorCore)
+class FakeReactorTcp(object):
 
     failures = 0
     _port_generator = port_generator()
@@ -527,14 +559,28 @@ class FakeReactorTcp(FakeReactor):
         self.protocol = TorControlProtocol()
         self.protocol.connectionMade = lambda: None
         self.transport = proto_helpers.StringTransport()
-        self.transport = FakeProcessTransport()
         self.transport.protocol = self.protocol
 
         def blam():
-            self.protocol.outReceived("Bootstrap")
+            self.protocol.outReceived(b"Bootstrap")
         self.transport.closeStdin = blam
         self.protocol.makeConnection(self.transport)
-        FakeReactor.__init__(self, test, self.transport, lambda x: None)
+        self.test = test
+
+    def spawnProcess(self, processprotocol, bin, args, env, path,
+                     uid=None, gid=None, usePTY=None, childFDs=None):
+        self.protocol = processprotocol
+        self.protocol.makeConnection(self.transport)
+        self.transport.process_protocol = processprotocol
+        return self.transport
+
+    def addSystemEventTrigger(self, *args):
+        self.test.assertEqual(args[0], 'before')
+        self.test.assertEqual(args[1], 'shutdown')
+        # we know this is just for the temporary file cleanup, so we
+        # nuke it right away to avoid polluting /tmp by calling the
+        # callback now.
+        args[2]()
 
     def listenTCP(self, port, factory, **kwargs):
         '''returns IListeningPort'''
@@ -543,7 +589,7 @@ class FakeReactorTcp(FakeReactor):
             raise error.CannotListenError(None, None, None)
 
         if port == 0:
-            port = self._port_generator.next()
+            port = next(self._port_generator)
         p = FakeListeningPort(port)
         p.factory = factory
         p.startListening()
@@ -557,15 +603,31 @@ class FakeReactorTcp(FakeReactor):
         )
 
         def blam(*args):
-            print "BLAAAAAM", args
+            print("BLAAAAAM", args)
+        r.connect = blam
+        return r
+
+    def connectUNIX(self, address, factory, timeout=30, checkPID=0):
+        '''should return IConnector'''
+        r = unix.Connector(
+            address, factory, timeout, self, checkPID,
+        )
+
+        def blam(*args):
+            print("BLAAAAAM", args)
         r.connect = blam
         return r
 
 
 class FakeTorSocksEndpoint(object):
-    def __init__(self, *args, **kw):
-        self.host = args[1]
-        self.port = args[2]
+    """
+    This ctor signature matches TorSocksEndpoint even though we don't
+    use it in the tests.
+    """
+
+    def __init__(self, socks_endpoint, host, port, tls=False, **kw):
+        self.host = host
+        self.port = port
         self.transport = None
 
         self.failure = kw.get('failure', None)
@@ -580,24 +642,143 @@ class FakeTorSocksEndpoint(object):
             if self.failure:
                 return defer.fail(self.failure)
         self.proto = fac.buildProtocol(None)
-        transport = proto_helpers.StringTransport()
+        transport = proto_helpers.StringTransportWithDisconnection()
         self.proto.makeConnection(transport)
         self.transport = transport
         return defer.succeed(self.proto)
 
 
+class FakeSocksProto(object):
+    def __init__(self, host, port, method, factory):
+        self.host = host
+        self.port = port
+        self.method = method
+        self.factory = factory
+        self._done = SingleObserver()
+
+    def when_done(self):
+        return self._done.when_fired()
+
+    def makeConnection(self, transport):
+        proto = self.factory.buildProtocol('socks5 addr')
+        self._done.fire(proto)
+
+
+class TestTorCircuitEndpoint(unittest.TestCase):
+
+    @defer.inlineCallbacks
+    def test_circuit_failure(self):
+        """
+        If the circuit fails the error propagates
+        """
+        reactor = Mock()
+        torstate = Mock()
+        target = Mock()
+        target.connect = Mock(return_value=defer.succeed(None))
+        circ = Mock()
+        circ.state = 'FAILED'
+        src_addr = Mock()
+        src_addr.host = 'host'
+        src_addr.port = 1234
+        target._get_address = Mock(return_value=defer.succeed(src_addr))
+        stream = Mock()
+        stream.source_port = 1234
+        stream.source_addr = 'host'
+
+        # okay, so we fire up our circuit-endpoint with mostly mocked
+        # things, and a circuit that's already in 'FAILED' state.
+        ep = TorCircuitEndpoint(reactor, torstate, circ, target)
+
+        # should get a Failure from the connect()
+        d = ep.connect(Mock())
+        attacher = yield _get_circuit_attacher(reactor, Mock())
+        attacher.attach_stream(stream, [circ])
+        try:
+            yield d
+            self.fail("Should get exception")
+        except RuntimeError as e:
+            assert "unusable" in str(e)
+
+    @defer.inlineCallbacks
+    def test_circuit_stream_failure(self):
+        """
+        If the stream-attach fails the error propagates
+        """
+        reactor = Mock()
+        torstate = Mock()
+        target = Mock()
+        target.connect = Mock(return_value=defer.succeed(None))
+        circ = Mock()
+        circ.state = 'FAILED'
+        src_addr = Mock()
+        src_addr.host = 'host'
+        src_addr.port = 1234
+        target._get_address = Mock(return_value=defer.succeed(src_addr))
+        stream = Mock()
+        stream.source_port = 1234
+        stream.source_addr = 'host'
+
+        # okay, so we fire up our circuit-endpoint with mostly mocked
+        # things, and a circuit that's already in 'FAILED' state.
+        ep = TorCircuitEndpoint(reactor, torstate, circ, target)
+
+        # should get a Failure from the connect()
+        d = ep.connect(Mock())
+        attacher = yield _get_circuit_attacher(reactor, Mock())
+        attacher.attach_stream_failure(stream, RuntimeError("a bad thing"))
+        try:
+            yield d
+            self.fail("Should get exception")
+        except RuntimeError as e:
+            self.assertEqual("a bad thing", str(e))
+
+    @defer.inlineCallbacks
+    def test_success(self):
+        """
+        Connect a stream via a circuit
+        """
+        reactor = Mock()
+        torstate = Mock()
+        target = Mock()
+        target.connect = Mock(return_value=defer.succeed('fake proto'))
+        circ = Mock()
+        circ.state = 'NEW'
+        src_addr = Mock()
+        src_addr.host = 'host'
+        src_addr.port = 1234
+        target._get_address = Mock(return_value=defer.succeed(src_addr))
+        stream = Mock()
+        stream.source_port = 1234
+        stream.source_addr = 'host'
+
+        # okay, so we fire up our circuit-endpoint with mostly mocked
+        # things, and a circuit that's already in 'FAILED' state.
+        ep = TorCircuitEndpoint(reactor, torstate, circ, target)
+
+        # should get a Failure from the connect()
+        d = ep.connect(Mock())
+        attacher = yield _get_circuit_attacher(reactor, torstate)
+        yield attacher.attach_stream(stream, [circ])
+        proto = yield d
+        self.assertEqual(proto, 'fake proto')
+
+
 class TestTorClientEndpoint(unittest.TestCase):
 
-    def test_client_connection_failed(self):
+    @patch('txtorcon.endpoints.get_global_tor')
+    def test_client_connection_failed(self, ggt):
         """
         This test is equivalent to txsocksx's
         TestSOCKS4ClientEndpoint.test_clientConnectionFailed
         """
-        args = "host123"
-        kw = dict()
-        kw['failure'] = Failure(ConnectionRefusedError())
-        tor_endpoint = FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, socks_endpoint=tor_endpoint)
+        tor_endpoint = FakeTorSocksEndpoint(
+            None, "host123", 9050,
+            failure=Failure(ConnectionRefusedError()),
+        )
+        endpoint = TorClientEndpoint(
+            '', 0,
+            socks_endpoint=tor_endpoint,
+        )
         d = endpoint.connect(None)
         return self.assertFailure(d, ConnectionRefusedError)
 
@@ -605,21 +786,23 @@ class TestTorClientEndpoint(unittest.TestCase):
         """
         Same as above, but with a username/password.
         """
-        args = "fakehost"
-        kw = dict()
-        kw['failure'] = Failure(ConnectionRefusedError())
-        tor_endpoint = FakeTorSocksEndpoint(*args, **kw)
+        tor_endpoint = FakeTorSocksEndpoint(
+            None, "fakehose", 9050,
+            failure=Failure(ConnectionRefusedError()),
+        )
         endpoint = TorClientEndpoint(
             'invalid host', 0,
             socks_username='billy', socks_password='s333cure',
-            socks_endpoint = tor_endpoint)
+            socks_endpoint=tor_endpoint)
         d = endpoint.connect(None)
+        # XXX we haven't fixed socks.py to support user/pw yet ...
+        return self.assertFailure(d, RuntimeError)
         return self.assertFailure(d, ConnectionRefusedError)
 
     def test_no_host(self):
         self.assertRaises(
             ValueError,
-            TorClientEndpoint, None, None
+            TorClientEndpoint, None, None, Mock(),
         )
 
     def test_parser_basic(self):
@@ -628,7 +811,7 @@ class TestTorClientEndpoint(unittest.TestCase):
         self.assertEqual(ep.host, 'timaq4ygg2iegci7.onion')
         self.assertEqual(ep.port, 80)
         # XXX what's "the Twisted way" to get the port out here?
-        self.assertEqual(ep.socks_endpoint._port, 9050)
+        self.assertEqual(ep._socks_endpoint._port, 9050)
 
     def test_parser_user_password(self):
         epstring = 'tor:host=torproject.org:port=443' + \
@@ -637,34 +820,36 @@ class TestTorClientEndpoint(unittest.TestCase):
 
         self.assertEqual(ep.host, 'torproject.org')
         self.assertEqual(ep.port, 443)
-        self.assertEqual(ep.socks_username, 'foo')
-        self.assertEqual(ep.socks_password, 'bar')
+        self.assertEqual(ep._socks_username, 'foo')
+        self.assertEqual(ep._socks_password, 'bar')
 
     def test_default_factory(self):
         """
-        This test is equivalent to txsocksx's TestSOCKS5ClientEndpoint.test_defaultFactory
+        This test is equivalent to txsocksx's
+        TestSOCKS5ClientEndpoint.test_defaultFactory
         """
 
-        args = "fakehost"
-        kw = dict()
-        tor_endpoint = FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, socks_endpoint=tor_endpoint)
+        tor_endpoint = FakeTorSocksEndpoint(None, "fakehost", 9050)
+        endpoint = TorClientEndpoint(
+            '', 0,
+            socks_endpoint=tor_endpoint,
+        )
         endpoint.connect(Mock)
-        self.assertEqual(tor_endpoint.transport.value(), '\x05\x01\x00')
+        self.assertEqual(tor_endpoint.transport.value(), b'\x05\x01\x00')
 
-    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
     @defer.inlineCallbacks
-    def test_success(self, socks5_factory):
-        ep = MagicMock()
-        gold_proto = object()
-        ep.connect = MagicMock(return_value=gold_proto)
-        socks5_factory.return_value = ep
-        args = "fakehost"
-        kw = dict()
-        tor_endpoint = FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, socks_endpoint = tor_endpoint)
-        other_proto = yield endpoint.connect(MagicMock())
-        self.assertEqual(other_proto, gold_proto)
+    def test_success(self):
+        with patch.object(_TorSocksFactory, "protocol", FakeSocksProto):
+            tor_endpoint = FakeTorSocksEndpoint(Mock(), "fakehost", 9050)
+            endpoint = TorClientEndpoint(
+                u'meejah.ca', 443,
+                socks_endpoint=tor_endpoint,
+            )
+            proto = yield endpoint.connect(MagicMock())
+            self.assertTrue(isinstance(proto, FakeSocksProto))
+            self.assertEqual(u"meejah.ca", proto.host)
+            self.assertEqual(443, proto.port)
+            self.assertEqual('CONNECT', proto.method)
 
     def test_good_port_retry(self):
         """
@@ -676,14 +861,17 @@ class TestTorClientEndpoint(unittest.TestCase):
         success_ports = TorClientEndpoint.socks_ports_to_try
         for port in success_ports:
             tor_endpoint = FakeTorSocksEndpoint(
-                "fakehost", "127.0.0.1", port,
+                u"fakehost", "127.0.0.1", port,
                 accept_port=port,
                 failure=Failure(ConnectionRefusedError()),
             )
 
-            endpoint = TorClientEndpoint('', 0, socks_endpoint=tor_endpoint)
-            endpoint.connect(None)
-            self.assertEqual(tor_endpoint.transport.value(), '\x05\x01\x00')
+            endpoint = TorClientEndpoint(
+                '', 0,
+                socks_endpoint=tor_endpoint,
+            )
+            endpoint.connect(Mock())
+            self.assertEqual(tor_endpoint.transport.value(), b'\x05\x01\x00')
 
     def test_bad_port_retry(self):
         """
@@ -700,7 +888,7 @@ class TestTorClientEndpoint(unittest.TestCase):
             d = endpoint.connect(None)
             return self.assertFailure(d, ConnectionRefusedError)
 
-    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
+    @patch('txtorcon.endpoints.TorSocksEndpoint')
     def test_default_socks_ports_fails(self, ep_mock):
         """
         Ensure we iterate over the default socks ports
@@ -714,12 +902,15 @@ class TestTorClientEndpoint(unittest.TestCase):
             def connect(self, *args, **kw):
                 raise ConnectionRefusedError()
 
+            def _get_address(self):
+                return defer.succeed(None)
+
         ep_mock.side_effect = FakeSocks5
-        endpoint = TorClientEndpoint('', 0)#, socks_endpoint=ep)
-        d = endpoint.connect(None)
+        endpoint = TorClientEndpoint('', 0)
+        d = endpoint.connect(Mock())
         self.assertFailure(d, ConnectionRefusedError)
 
-    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
+    @patch('txtorcon.endpoints.TorSocksEndpoint')
     @defer.inlineCallbacks
     def test_default_socks_ports_happy(self, ep_mock):
         """
@@ -727,6 +918,7 @@ class TestTorClientEndpoint(unittest.TestCase):
         """
 
         proto = object()
+
         class FakeSocks5(object):
 
             def __init__(self, *args, **kw):
@@ -735,24 +927,20 @@ class TestTorClientEndpoint(unittest.TestCase):
             def connect(self, *args, **kw):
                 return proto
 
+            def _get_address(self):
+                return defer.succeed(None)
+
         ep_mock.side_effect = FakeSocks5
         endpoint = TorClientEndpoint('', 0)
         p2 = yield endpoint.connect(None)
         self.assertTrue(proto is p2)
 
-    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
+    @patch('txtorcon.endpoints.TorSocksEndpoint')
     @defer.inlineCallbacks
     def test_tls_socks_no_endpoint(self, ep_mock):
+        the_proto = object()
+        proto = defer.succeed(the_proto)
 
-        if not _HAVE_TLS:
-            print("no TLS support")
-            return
-
-        class FakeWrappedProto(object):
-            wrappedProtocol = object()
-
-        wrap = FakeWrappedProto()
-        proto = defer.succeed(wrap)
         class FakeSocks5(object):
 
             def __init__(self, *args, **kw):
@@ -761,61 +949,122 @@ class TestTorClientEndpoint(unittest.TestCase):
             def connect(self, *args, **kw):
                 return proto
 
+            def _get_address(self):
+                return defer.succeed(None)
+
         ep_mock.side_effect = FakeSocks5
         endpoint = TorClientEndpoint('torproject.org', 0, tls=True)
         p2 = yield endpoint.connect(None)
-        self.assertTrue(wrap.wrappedProtocol is p2)
+        self.assertTrue(the_proto is p2)
 
-    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
+    @patch('txtorcon.endpoints.TorSocksEndpoint')
     @defer.inlineCallbacks
     def test_tls_socks_with_endpoint(self, ep_mock):
         """
         Same as above, except we provide an explicit endpoint
         """
+        the_proto = object()
+        proto_d = defer.succeed(the_proto)
 
-        if not _HAVE_TLS:
-            print("no TLS support")
-            return
-
-        class FakeWrappedProto(object):
-            wrappedProtocol = object()
-
-        wrap = FakeWrappedProto()
-        proto = defer.succeed(wrap)
         class FakeSocks5(object):
 
             def __init__(self, *args, **kw):
                 pass
 
             def connect(self, *args, **kw):
-                return proto
+                return proto_d
+
+            def _get_address(self):
+                return defer.succeed(None)
 
         ep_mock.side_effect = FakeSocks5
         endpoint = TorClientEndpoint(
-            'torproject.org', 0,
+            u'torproject.org', 0,
             socks_endpoint=clientFromString(Mock(), "tcp:localhost:9050"),
             tls=True,
         )
         p2 = yield endpoint.connect(None)
-        self.assertTrue(wrap.wrappedProtocol is p2)
+        self.assertTrue(p2 is the_proto)
 
-    @patch('txtorcon.endpoints.reactor')  # FIXME should be passing reactor to TorClientEndpoint :/
-    def test_client_endpoint_old_api(self, reactor):
+    def test_client_endpoint_old_api(self):
         """
         Test the old API of passing socks_host, socks_port
         """
 
+        reactor = Mock()
         endpoint = TorClientEndpoint(
             'torproject.org', 0,
             socks_hostname='localhost',
             socks_port=9050,
+            reactor=reactor,
+        )
+        self.assertTrue(
+            isinstance(endpoint._socks_endpoint, TCP4ClientEndpoint)
         )
-        self.assertTrue(isinstance(endpoint.socks_endpoint, TCP4ClientEndpoint))
 
-        d = endpoint.connect(Mock())
+        endpoint.connect(Mock())
         calls = reactor.mock_calls
         self.assertEqual(1, len(calls))
         name, args, kw = calls[0]
         self.assertEqual("connectTCP", name)
         self.assertEqual("localhost", args[0])
         self.assertEqual(9050, args[1])
+
+    def test_client_endpoint_get_address(self):
+        """
+        Test the old API of passing socks_host, socks_port
+        """
+
+        reactor = Mock()
+        endpoint = TorClientEndpoint(
+            'torproject.org', 0,
+            socks_endpoint=clientFromString(Mock(), "tcp:localhost:9050"),
+            reactor=reactor,
+        )
+        d = endpoint._get_address()
+        self.assertTrue(not d.called)
+
+
+class TestSocksFactory(unittest.TestCase):
+
+    @defer.inlineCallbacks
+    def test_explicit_socks(self):
+        reactor = Mock()
+        cp = Mock()
+        cp.get_conf = Mock(
+            return_value=defer.succeed({
+                'SocksPort': ['9050', '9150', 'unix:/tmp/boom']
+            })
+        )
+
+        ep = yield _create_socks_endpoint(reactor, cp, socks_config='unix:/tmp/boom')
+
+        self.assertTrue(isinstance(ep, UNIXClientEndpoint))
+
+    @defer.inlineCallbacks
+    def test_unix_socket_with_options(self):
+        reactor = Mock()
+        cp = Mock()
+        cp.get_conf = Mock(
+            return_value=defer.succeed({
+                'SocksPort': ['unix:/tmp/boom SomeOption']
+            })
+        )
+
+        ep = yield _create_socks_endpoint(reactor, cp)
+
+        self.assertTrue(isinstance(ep, UNIXClientEndpoint))
+        self.assertEqual("/tmp/boom", ep._path)
+
+    @defer.inlineCallbacks
+    def test_nothing_exists(self):
+        reactor = Mock()
+        cp = Mock()
+        cp.get_conf = Mock(return_value=defer.succeed(dict()))
+
+        with patch(u'txtorcon.endpoints.available_tcp_port', return_value=9999):
+            ep = yield _create_socks_endpoint(reactor, cp)
+
+        self.assertTrue(isinstance(ep, TCP4ClientEndpoint))
+        # internal details, but ...
+        self.assertEqual(ep._port, 9999)
diff --git a/test/test_fsm.py b/test/test_fsm.py
index 027f993..3bbc6a1 100644
--- a/test/test_fsm.py
+++ b/test/test_fsm.py
@@ -1,6 +1,9 @@
+from __future__ import print_function
 
 import txtorcon.spaghetti
-from txtorcon.spaghetti import *
+from txtorcon.spaghetti import State
+from txtorcon.spaghetti import Transition
+from txtorcon.spaghetti import FSM
 from twisted.trial import unittest
 
 import os
@@ -118,4 +121,4 @@ class FsmTests(unittest.TestCase):
         self.assertEqual(fsm.state, cmd)
 
     def doCommand(self, data):
-        print "transition:", data
+        print("transition:", data)
diff --git a/test/test_microdesc.py b/test/test_microdesc.py
new file mode 100644
index 0000000..35fc227
--- /dev/null
+++ b/test/test_microdesc.py
@@ -0,0 +1,96 @@
+
+from twisted.trial import unittest
+
+from txtorcon._microdesc_parser import MicrodescriptorParser
+
+
+class ParserTests(unittest.TestCase):
+
+    def test_two_no_w(self):
+        relays = []
+
+        def create_relay(**kw):
+            relays.append(kw)
+        m = MicrodescriptorParser(create_relay)
+
+        for line in [
+                'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80',
+                's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+                'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80',
+                's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+        ]:
+            m.feed_line(line)
+        m.done()
+
+        self.assertEqual(2, len(relays))
+        self.assertEqual('fake', relays[0]['nickname'])
+        self.assertEqual('ekaf', relays[1]['nickname'])
+        self.assertEqual('11.11.11.11', relays[0]['ip'])
+        self.assertEqual('22.22.22.22', relays[1]['ip'])
+
+        self.assertTrue('bandwidth' not in relays[0])
+        self.assertTrue('bandwidth' not in relays[1])
+        self.assertTrue('flags' in relays[0])
+        self.assertTrue('flags' in relays[1])
+        self.assertTrue('FutureProof' in relays[1]['flags'])
+
+    def test_two(self):
+        relays = []
+
+        def create_relay(**kw):
+            relays.append(kw)
+        m = MicrodescriptorParser(create_relay)
+
+        for line in [
+                'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80',
+                's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+                'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80',
+                's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+                'w Bandwidth=518000',
+                'p accept 43,53,79-81',
+        ]:
+            m.feed_line(line)
+        m.done()
+
+        self.assertEqual(2, len(relays))
+        self.assertEqual('fake', relays[0]['nickname'])
+        self.assertEqual('ekaf', relays[1]['nickname'])
+        self.assertEqual('11.11.11.11', relays[0]['ip'])
+        self.assertEqual('22.22.22.22', relays[1]['ip'])
+
+        self.assertTrue('bandwidth' not in relays[0])
+        self.assertTrue('bandwidth' in relays[1])
+        self.assertTrue('flags' in relays[0])
+        self.assertTrue('flags' in relays[1])
+        self.assertTrue('FutureProof' in relays[1]['flags'])
+
+    # re-enable when we switch back to Automat
+    def test_bad_line(self):
+        relays = []
+
+        def create_relay(**kw):
+            relays.append(kw)
+        m = MicrodescriptorParser(create_relay)
+
+        with self.assertRaises(Exception) as ctx:
+            m.feed_line('x blam')
+        # self.assertTrue('Unknown microdescriptor' in str(ctx.exception))
+        self.assertTrue('Expected "r " ' in str(ctx.exception))
+        self.assertEqual(0, len(relays))
+
+    def test_single_ipv6(self):
+        relays = []
+
+        def create_relay(**kw):
+            relays.append(kw)
+        m = MicrodescriptorParser(create_relay)
+
+        for line in [
+                'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80',
+                'a [2001:0:0:0::0]:4321'
+        ]:
+            m.feed_line(line)
+        m.done()
+
+        self.assertEqual(1, len(relays))
+        self.assertEqual(['[2001:0:0:0::0]:4321'], list(relays[0]['ip_v6']))
diff --git a/test/test_router.py b/test/test_router.py
index e140c4d..de8dd84 100644
--- a/test/test_router.py
+++ b/test/test_router.py
@@ -1,6 +1,11 @@
+import json
 from datetime import datetime
+from mock import Mock
+
 from twisted.trial import unittest
 from twisted.internet import defer
+from twisted.python.failure import Failure
+from twisted.web.client import ResponseDone
 
 from txtorcon.router import Router, hexIdFromHash, hashFromHexId
 
@@ -148,7 +153,7 @@ class RouterTests(unittest.TestCase):
         try:
             router.policy = 'foo 123'
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue("Don't understand" in str(e))
 
     def test_policy_not_set_error(self):
@@ -156,7 +161,7 @@ class RouterTests(unittest.TestCase):
         try:
             router.accepts_port(123)
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue("policy" in str(e))
 
     def test_repr(self):
@@ -173,3 +178,109 @@ class RouterTests(unittest.TestCase):
     def test_repr_no_update(self):
         router = Router(FakeController())
         repr(router)
+
+
+class OnionOOTests(unittest.TestCase):
+
+    def setUp(self):
+        self.router = Router(FakeController())
+        self.router.update(
+            "foo",
+            "AHhuQ8zFQJdT8l42Axxc6m6kNwI",
+            "MAANkj30tnFvmoh7FsjVFr+cmcs",
+            "2011-12-16 15:11:34",
+            "1.2.3.4",
+            "24051", "24052"
+        )
+
+    @defer.inlineCallbacks
+    def test_onionoo_get_fails(self):
+        agent = Mock()
+        resp = Mock()
+        resp.code = 500
+        agent.request = Mock(return_value=defer.succeed(resp))
+
+        with self.assertRaises(Exception) as ctx:
+            yield self.router.get_onionoo_details(agent)
+        self.assertTrue(
+            "Failed to lookup" in str(ctx.exception)
+        )
+
+    @defer.inlineCallbacks
+    def test_onionoo_success(self):
+        agent = Mock()
+        resp = Mock()
+        resp.code = 200
+
+        def feed_response(protocol):
+            config = {
+                "relays": [
+                    {
+                        "fingerprint": "00786E43CCC5409753F25E36031C5CEA6EA43702",
+                    },
+                ]
+            }
+            protocol.dataReceived(json.dumps(config).encode())
+            protocol.connectionLost(Failure(ResponseDone()))
+        resp.deliverBody = Mock(side_effect=feed_response)
+        agent.request = Mock(return_value=defer.succeed(resp))
+
+        data = yield self.router.get_onionoo_details(agent)
+
+        self.assertTrue('fingerprint' in data)
+        self.assertTrue(data['fingerprint'] == "00786E43CCC5409753F25E36031C5CEA6EA43702")
+
+    @defer.inlineCallbacks
+    def test_onionoo_too_many_answers(self):
+        agent = Mock()
+        resp = Mock()
+        resp.code = 200
+
+        def feed_response(protocol):
+            config = {
+                "relays": [
+                    {
+                        "fingerprint": "00786E43CCC5409753F25E36031C5CEA6EA43702",
+                    },
+                    {
+                        "fingerprint": "boom",
+                    }
+                ]
+            }
+            protocol.dataReceived(json.dumps(config).encode())
+            protocol.connectionLost(Failure(ResponseDone()))
+        resp.deliverBody = Mock(side_effect=feed_response)
+        agent.request = Mock(return_value=defer.succeed(resp))
+
+        with self.assertRaises(Exception) as ctx:
+            yield self.router.get_onionoo_details(agent)
+
+        self.assertTrue(
+            "multiple relays for" in str(ctx.exception)
+        )
+
+    @defer.inlineCallbacks
+    def test_onionoo_wrong_fingerprint(self):
+        agent = Mock()
+        resp = Mock()
+        resp.code = 200
+
+        def feed_response(protocol):
+            config = {
+                "relays": [
+                    {
+                        "fingerprint": "boom",
+                    },
+                ]
+            }
+            protocol.dataReceived(json.dumps(config).encode())
+            protocol.connectionLost(Failure(ResponseDone()))
+        resp.deliverBody = Mock(side_effect=feed_response)
+        agent.request = Mock(return_value=defer.succeed(resp))
+
+        with self.assertRaises(Exception) as ctx:
+            yield self.router.get_onionoo_details(agent)
+
+        self.assertTrue(
+            " but got data for " in str(ctx.exception)
+        )
diff --git a/test/test_socks.py b/test/test_socks.py
new file mode 100644
index 0000000..30fedf9
--- /dev/null
+++ b/test/test_socks.py
@@ -0,0 +1,780 @@
+from six import BytesIO, text_type
+from mock import Mock, patch
+
+from twisted.trial import unittest
+from twisted.internet import defer
+from twisted.internet.address import IPv4Address
+from twisted.internet.protocol import Protocol
+from twisted.test import proto_helpers
+from twisted.test.iosim import connect, FakeTransport
+
+from txtorcon import socks
+
+
+class SocksStateMachine(unittest.TestCase):
+
+    def test_illegal_request(self):
+        with self.assertRaises(ValueError) as ctx:
+            socks._SocksMachine('FOO_RESOLVE', u'meejah.ca', 443)
+        self.assertTrue(
+            'Unknown request type' in str(ctx.exception)
+        )
+
+    def test_illegal_host(self):
+        with self.assertRaises(ValueError) as ctx:
+            socks._SocksMachine('RESOLVE', 1234, 443)
+        self.assertTrue(
+            "'host' must be" in str(ctx.exception)
+        )
+
+    def test_illegal_ip_addr(self):
+        with self.assertRaises(ValueError) as ctx:
+            socks._create_ip_address(1234, 443)
+        self.assertTrue(
+            "'host' must be" in str(ctx.exception)
+        )
+
+    def test_connect_but_no_creator(self):
+        with self.assertRaises(ValueError) as ctx:
+            socks._SocksMachine(
+                'CONNECT', u'foo.bar',
+            )
+        self.assertTrue(
+            "create_connection function required" in str(ctx.exception)
+        )
+
+    @defer.inlineCallbacks
+    def test_connect_socks_illegal_packet(self):
+
+        class BadSocksServer(Protocol):
+            def __init__(self):
+                self._buffer = b''
+
+            def dataReceived(self, data):
+                self._buffer += data
+                if len(self._buffer) == 3:
+                    assert self._buffer == b'\x05\x01\x00'
+                    self._buffer = b''
+                    self.transport.write(b'\x05\x01\x01')
+
+        factory = socks._TorSocksFactory(u'meejah.ca', 1234, 'CONNECT', Mock())
+        server_proto = BadSocksServer()
+        server_transport = FakeTransport(server_proto, isServer=True)
+
+        client_proto = factory.buildProtocol('ignored')
+        client_transport = FakeTransport(client_proto, isServer=False)
+
+        pump = yield connect(
+            server_proto, server_transport,
+            client_proto, client_transport,
+        )
+
+        self.assertTrue(server_proto.transport.disconnected)
+        self.assertTrue(client_proto.transport.disconnected)
+        pump.flush()
+
+    @defer.inlineCallbacks
+    def test_connect_socks_unknown_version(self):
+
+        class BadSocksServer(Protocol):
+            def __init__(self):
+                self._buffer = b''
+                self._recv_stack = [
+                    (b'\x05\x01\x00', b'\x05\xff'),
+                ]
+
+            def dataReceived(self, data):
+                self._buffer += data
+                if len(self._recv_stack) == 0:
+                    assert "not expecting any more data, got {}".format(repr(self._buffer))
+                    return
+                expecting, to_send = self._recv_stack.pop(0)
+                got = self._buffer[:len(expecting)]
+                self._buffer = self._buffer[len(expecting):]
+                assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got))
+                self.transport.write(to_send)
+
+        factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock())
+        server_proto = BadSocksServer()
+        server_transport = FakeTransport(server_proto, isServer=True)
+
+        client_proto = factory.buildProtocol('ignored')
+        client_transport = FakeTransport(client_proto, isServer=False)
+
+        # returns IOPump
+        yield connect(
+            server_proto, server_transport,
+            client_proto, client_transport,
+        )
+
+        self.assertTrue(server_proto.transport.disconnected)
+        self.assertTrue(client_proto.transport.disconnected)
+
+    @defer.inlineCallbacks
+    def test_connect_socks_unknown_reply_code(self):
+
+        class BadSocksServer(Protocol):
+            def __init__(self):
+                self._buffer = b''
+                self._recv_stack = [
+                    (b'\x05\x01\x00', b'\x05\x00'),
+                    # the \xff is an invalid reply-code
+                    (b'\x05\x01\x00\x01\x01\x02\x03\x04\x04\xd2', b'\x05\xff\x00\x04\x01\x01\x01\x01'),
+                ]
+
+            def dataReceived(self, data):
+                self._buffer += data
+                if len(self._recv_stack) == 0:
+                    assert "not expecting any more data, got {}".format(repr(self._buffer))
+                    return
+                expecting, to_send = self._recv_stack.pop(0)
+                got = self._buffer[:len(expecting)]
+                self._buffer = self._buffer[len(expecting):]
+                assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got))
+                self.transport.write(to_send)
+
+        factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock())
+        server_proto = BadSocksServer()
+        server_transport = FakeTransport(server_proto, isServer=True)
+
+        client_proto = factory.buildProtocol('ignored')
+        client_transport = FakeTransport(client_proto, isServer=False)
+
+        d = client_proto._machine.when_done()
+
+        # returns IOPump
+        yield connect(
+            server_proto, server_transport,
+            client_proto, client_transport,
+        )
+        with self.assertRaises(Exception) as ctx:
+            yield d
+        self.assertIn('Unknown SOCKS error-code', str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_socks_relay_data(self):
+
+        class BadSocksServer(Protocol):
+            def __init__(self):
+                self._buffer = b''
+                self._recv_stack = [
+                    (b'\x05\x01\x00', b'\x05\x00'),
+                    (b'\x05\x01\x00\x01\x01\x02\x03\x04\x04\xd2', b'\x05\x00\x00\x01\x01\x02\x03\x04\x12\x34'),
+                ]
+
+            def dataReceived(self, data):
+                self._buffer += data
+                if len(self._recv_stack) == 0:
+                    assert "not expecting any more data, got {}".format(repr(self._buffer))
+                    return
+                expecting, to_send = self._recv_stack.pop(0)
+                got = self._buffer[:len(expecting)]
+                self._buffer = self._buffer[len(expecting):]
+                assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got))
+                self.transport.write(to_send)
+
+        factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock())
+        server_proto = BadSocksServer()
+        server_transport = FakeTransport(server_proto, isServer=True)
+
+        client_proto = factory.buildProtocol('ignored')
+        client_transport = FakeTransport(client_proto, isServer=False)
+
+        pump = yield connect(
+            server_proto, server_transport,
+            client_proto, client_transport,
+        )
+
+        # should be relaying now, try sending some datas
+
+        client_proto.transport.write(b'abcdef')
+        pump.flush()
+        self.assertEqual(b'abcdef', server_proto._buffer)
+
+    @defer.inlineCallbacks
+    def test_socks_ipv6(self):
+
+        class BadSocksServer(Protocol):
+            def __init__(self):
+                self._buffer = b''
+                self._recv_stack = [
+                    (b'\x05\x01\x00', b'\x05\x00'),
+                    (b'\x05\x01\x00\x04\x20\x02\x44\x93\x04\xd2',
+                     b'\x05\x00\x00\x04' + (b'\x00' * 16) + b'\xbe\xef'),
+                ]
+
+            def dataReceived(self, data):
+                self._buffer += data
+                if len(self._recv_stack) == 0:
+                    assert "not expecting any more data, got {}".format(repr(self._buffer))
+                    return
+                expecting, to_send = self._recv_stack.pop(0)
+                got = self._buffer[:len(expecting)]
+                self._buffer = self._buffer[len(expecting):]
+                assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got))
+                self.transport.write(to_send)
+
+        factory = socks._TorSocksFactory(u'2002:4493:5105::a299:9bff:fe0e:4471', 1234, 'CONNECT', Mock())
+        server_proto = BadSocksServer()
+        expected_address = object()
+        server_transport = FakeTransport(server_proto, isServer=True)
+
+        client_proto = factory.buildProtocol(u'ignored')
+        client_transport = FakeTransport(client_proto, isServer=False, hostAddress=expected_address)
+
+        pump = yield connect(
+            server_proto, server_transport,
+            client_proto, client_transport,
+        )
+
+        # should be relaying now, try sending some datas
+
+        client_proto.transport.write(b'abcdef')
+        addr = yield factory._get_address()
+
+        # FIXME how shall we test for IPv6-ness?
+        assert addr is expected_address
+        pump.flush()
+        self.assertEqual(b'abcdef', server_proto._buffer)
+
+    def test_end_to_end_wrong_method(self):
+        dis = []
+
+        def on_disconnect(error_message):
+            dis.append(error_message)
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443, on_disconnect=on_disconnect)
+        sm.connection()
+
+        sm.feed_data(b'\x05')
+        sm.feed_data(b'\x01')
+
+        # we should have sent the request to the server, and nothing
+        # else (because we disconnected)
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00',
+            data.getvalue(),
+        )
+        self.assertEqual(1, len(dis))
+        self.assertEqual("Wanted method 0 or 2, got 1", dis[0])
+
+    def test_end_to_end_wrong_version(self):
+        dis = []
+
+        def on_disconnect(error_message):
+            dis.append(error_message)
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443, on_disconnect=on_disconnect)
+        sm.connection()
+
+        sm.feed_data(b'\x06')
+        sm.feed_data(b'\x00')
+
+        # we should have sent the request to the server, and nothing
+        # else (because we disconnected)
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00',
+            data.getvalue(),
+        )
+        self.assertEqual(1, len(dis))
+        self.assertEqual("Expected version 5, got 6", dis[0])
+
+    def test_end_to_end_connection_refused(self):
+        dis = []
+
+        def on_disconnect(error_message):
+            dis.append(error_message)
+        sm = socks._SocksMachine(
+            'CONNECT', u'1.2.3.4', 443,
+            on_disconnect=on_disconnect,
+            create_connection=lambda a, p: None,
+        )
+        sm.connection()
+
+        sm.feed_data(b'\x05')
+        sm.feed_data(b'\x00')
+
+        # reply with 'connection refused'
+        sm.feed_data(b'\x05\x05\x00\x01\x00\x00\x00\x00\xff\xff')
+
+        self.assertEqual(1, len(dis))
+        self.assertEqual(socks.ConnectionRefusedError.message, dis[0])
+
+    def test_end_to_end_successful_relay(self):
+
+        class Proto(object):
+            data = b''
+            lost = []
+
+            def dataReceived(self, d):
+                self.data = self.data + d
+
+            def connectionLost(self, reason):
+                self.lost.append(reason)
+
+        the_proto = Proto()
+        dis = []
+
+        def on_disconnect(error_message):
+            dis.append(error_message)
+        sm = socks._SocksMachine(
+            'CONNECT', u'1.2.3.4', 443,
+            on_disconnect=on_disconnect,
+            create_connection=lambda a, p: the_proto,
+        )
+        sm.connection()
+
+        sm.feed_data(b'\x05')
+        sm.feed_data(b'\x00')
+
+        # reply with success, port 0x1234
+        sm.feed_data(b'\x05\x00\x00\x01\x00\x00\x00\x00\x12\x34')
+
+        # now some data that should get relayed
+        sm.feed_data(b'this is some relayed data')
+        # should *not* have disconnected
+        self.assertEqual(0, len(dis))
+        self.assertTrue(the_proto.data, b"this is some relayed data")
+        sm.disconnected(socks.SocksError("it's fine"))
+        self.assertEqual(1, len(Proto.lost))
+        self.assertTrue("it's fine" in str(Proto.lost[0]))
+
+    def test_end_to_end_success(self):
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443)
+        sm.connection()
+
+        sm.feed_data(b'\x05')
+        sm.feed_data(b'\x00')
+
+        # now we check we got the right bytes out the other side
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00',
+            data.getvalue(),
+        )
+
+    def test_end_to_end_connect_and_relay(self):
+        sm = socks._SocksMachine(
+            'CONNECT', u'1.2.3.4', 443,
+            create_connection=lambda a, p: None,
+        )
+        sm.connection()
+
+        sm.feed_data(b'\x05')
+        sm.feed_data(b'\x00')
+        sm.feed_data(b'some relayed data')
+
+        # now we check we got the right bytes out the other side
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb',
+            data.getvalue(),
+        )
+
+    def test_resolve(self):
+        # kurt: most things use (hsot, port) tuples, this probably
+        # should too
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443)
+        sm.connection()
+        sm.version_reply(0x00)
+
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00',
+            data.getvalue(),
+        )
+
+    @defer.inlineCallbacks
+    def test_resolve_with_reply(self):
+        # kurt: most things use (hsot, port) tuples, this probably
+        # should too
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443)
+        sm.connection()
+        sm.version_reply(0x00)
+
+        # make sure the state-machine wanted to send out the correct
+        # request.
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00',
+            data.getvalue(),
+        )
+
+        # now feed it a reply (but not enough to parse it yet!)
+        d = sm.when_done()
+        # ...we have to send at least 8 bytes, but NOT the entire hostname
+        sm.feed_data(b'\x05\x00\x00\x03')
+        sm.feed_data(b'\x06meeja')
+        self.assertTrue(not d.called)
+        # now send the rest, checking the buffering in _parse_domain_name_reply
+        sm.feed_data(b'h\x00\x00')
+        self.assertTrue(d.called)
+        answer = yield d
+        # XXX answer *should* be not-bytes, though I think
+        self.assertEqual(b'meejah', answer)
+
+    @defer.inlineCallbacks
+    def test_unknown_response_type(self):
+        # kurt: most things use (hsot, port) tuples, this probably
+        # should too
+        sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443)
+        sm.connection()
+        # don't actually support username/password (which is version 0x02) yet
+        # sm.version_reply(0x02)
+        sm.version_reply(0)
+
+        # make sure the state-machine wanted to send out the correct
+        # request.
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00',
+            data.getvalue(),
+        )
+
+        sm.feed_data(b'\x05\x00\x00\xaf\x00\x00\x00\x00')
+        with self.assertRaises(socks.SocksError) as ctx:
+            yield sm.when_done()
+        self.assertTrue('Unexpected response type 175' in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_resolve_ptr(self):
+        sm = socks._SocksMachine('RESOLVE_PTR', u'1.2.3.4', 443)
+        sm.connection()
+        sm.version_reply(0x00)
+
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\xf1\x00\x01\x01\x02\x03\x04\x00\x00',
+            data.getvalue(),
+        )
+        sm.feed_data(
+            b'\x05\x00\x00\x01\x00\x01\x02\xff\x12\x34'
+        )
+        addr = yield sm.when_done()
+        self.assertEqual('0.1.2.255', addr)
+
+    def test_connect(self):
+        sm = socks._SocksMachine(
+            'CONNECT', u'1.2.3.4', 443,
+            create_connection=lambda a, p: None,
+        )
+        sm.connection()
+        sm.version_reply(0x00)
+
+        data = BytesIO()
+        sm.send_data(data.write)
+        self.assertEqual(
+            b'\x05\x01\x00'
+            b'\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb',
+            data.getvalue(),
+        )
+
+
+# XXX should re-write (at LEAST) these to use Twisted's IOPump
+class SocksConnectTests(unittest.TestCase):
+
+    @defer.inlineCallbacks
+    def test_connect_no_tls(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443)
+        proto = yield ep.connect(factory)
+        self.assertEqual(proto, protocol)
+
+    @defer.inlineCallbacks
+    def test_connect_deferred_proxy(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(
+            socks_endpoint=defer.succeed(socks_ep),
+            host=u'meejah.ca',
+            port=443,
+        )
+        proto = yield ep.connect(factory)
+        self.assertEqual(proto, protocol)
+
+    @defer.inlineCallbacks
+    def test_connect_tls(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True)
+        proto = yield ep.connect(factory)
+        self.assertEqual(proto, protocol)
+
+    @defer.inlineCallbacks
+    def test_connect_socks_error(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x01\x00\x01\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True)
+        with self.assertRaises(Exception) as ctx:
+            yield ep.connect(factory)
+        self.assertTrue(isinstance(ctx.exception,
+                                   socks.GeneralServerFailureError))
+
+    @defer.inlineCallbacks
+    def test_connect_socks_error_unknown(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\xff\x00\x01\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True)
+        with self.assertRaises(Exception) as ctx:
+            yield ep.connect(factory)
+        self.assertTrue('Unknown SOCKS error-code' in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_connect_socks_illegal_byte(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x01\x00\x01\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True)
+        with self.assertRaises(Exception) as ctx:
+            yield ep.connect(factory)
+        self.assertTrue(isinstance(ctx.exception,
+                                   socks.GeneralServerFailureError))
+
+    @defer.inlineCallbacks
+    def test_get_address_endpoint(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+        delayed_addr = []
+
+        def connect(factory):
+            delayed_addr.append(factory._get_address())
+            delayed_addr.append(factory._get_address())
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            self.assertEqual(b'\x05\x01\x00', transport.value())
+            proto.dataReceived(b'\x05\x00')
+            proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
+            return proto
+        socks_ep.connect = connect
+        protocol = Mock()
+        factory = Mock()
+        factory.buildProtocol = Mock(return_value=protocol)
+        ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True)
+        yield ep.connect(factory)
+        addr = yield ep._get_address()
+
+        self.assertEqual(addr, IPv4Address('TCP', '10.0.0.1', 12345))
+        self.assertEqual(2, len(delayed_addr))
+        self.assertTrue(delayed_addr[0] is not delayed_addr[1])
+        self.assertTrue(all([d.called for d in delayed_addr]))
+
+    @defer.inlineCallbacks
+    def test_get_address(self):
+        # normally, ._get_address is only called via the
+        # attach_stream() method on Circuit
+        addr = object()
+        factory = socks._TorSocksFactory()
+        d = factory._get_address()
+        self.assertFalse(d.called)
+        factory._did_connect(addr)
+
+        maybe_addr = yield d
+
+        self.assertEqual(addr, maybe_addr)
+
+        # if we do it a second time, should be immediate
+        d = factory._get_address()
+        self.assertTrue(d.called)
+        self.assertEqual(d.result, addr)
+
+
+class SocksResolveTests(unittest.TestCase):
+
+    @defer.inlineCallbacks
+    def test_resolve(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            # XXX sadness: we probably "should" just feed the right
+            # bytes to the protocol to convince it a connection is
+            # made ... *or* we can cheat and just do the callback
+            # directly...
+            proto._machine._when_done.fire("the dns answer")
+            return proto
+        socks_ep.connect = connect
+        hn = yield socks.resolve(socks_ep, u'meejah.ca')
+        self.assertEqual(hn, "the dns answer")
+
+    @defer.inlineCallbacks
+    def test_resolve_ptr(self):
+        socks_ep = Mock()
+        transport = proto_helpers.StringTransport()
+
+        def connect(factory):
+            factory.startFactory()
+            proto = factory.buildProtocol("addr")
+            proto.makeConnection(transport)
+            # XXX sadness: we probably "should" just feed the right
+            # bytes to the protocol to convince it a connection is
+            # made ... *or* we can cheat and just do the callback
+            # directly...
+            proto._machine._when_done.fire(u"the dns answer")
+            return proto
+        socks_ep.connect = connect
+        hn = yield socks.resolve_ptr(socks_ep, u'meejah.ca')
+        self.assertEqual(hn, "the dns answer")
+
+    @patch('txtorcon.socks._TorSocksFactory')
+    def test_resolve_ptr_str(self, fac):
+        socks_ep = Mock()
+        d = socks.resolve_ptr(socks_ep, 'meejah.ca')
+        self.assertEqual(1, len(fac.mock_calls))
+        self.assertTrue(
+            isinstance(fac.mock_calls[0][1][0], text_type)
+        )
+        return d
+
+    @patch('txtorcon.socks._TorSocksFactory')
+    def test_resolve_str(self, fac):
+        socks_ep = Mock()
+        d = socks.resolve(socks_ep, 'meejah.ca')
+        self.assertEqual(1, len(fac.mock_calls))
+        self.assertTrue(
+            isinstance(fac.mock_calls[0][1][0], text_type)
+        )
+        return d
+
+    @patch('txtorcon.socks._TorSocksFactory')
+    def test_resolve_ptr_bytes(self, fac):
+        socks_ep = Mock()
+        d = socks.resolve_ptr(socks_ep, b'meejah.ca')
+        self.assertEqual(1, len(fac.mock_calls))
+        self.assertTrue(
+            isinstance(fac.mock_calls[0][1][0], text_type)
+        )
+        return d
+
+    @patch('txtorcon.socks._TorSocksFactory')
+    def test_resolve_bytes(self, fac):
+        socks_ep = Mock()
+        d = socks.resolve(socks_ep, b'meejah.ca')
+        self.assertEqual(1, len(fac.mock_calls))
+        self.assertTrue(
+            isinstance(fac.mock_calls[0][1][0], text_type)
+        )
+        return d
+
+
+class SocksErrorTests(unittest.TestCase):
+    def _check_error(self, error, cls_, code, message):
+        self.assertTrue(isinstance(error, cls_))
+        self.assertEqual(error.code, code)
+        self.assertEqual(error.message, message)
+        self.assertEqual(str(error), message)
+
+    def test_error_factory(self):
+        for cls in socks.SocksError.__subclasses__():
+            error = socks._create_socks_error(cls.code)
+            self._check_error(error, cls, cls.code, cls.message)
+
+    def test_custom_error(self):
+        code = 0xFF
+        message = 'Custom error message'
+
+        self._check_error(socks.SocksError(message),
+                          socks.SocksError, None, message)
+        self._check_error(socks.SocksError(message=message),
+                          socks.SocksError, None, message)
+        self._check_error(socks.SocksError(code=code),
+                          socks.SocksError, code, '')
+        self._check_error(socks.SocksError(message, code=code),
+                          socks.SocksError, code, message)
+        self._check_error(socks.SocksError(message=message, code=code),
+                          socks.SocksError, code, message)
diff --git a/test/test_stream.py b/test/test_stream.py
index b726ba2..c24c913 100644
--- a/test/test_stream.py
+++ b/test/test_stream.py
@@ -1,7 +1,9 @@
+from __future__ import print_function
+
 from txtorcon.util import maybe_ip_addr
 from twisted.trial import unittest
 from twisted.internet import defer
-from zope.interface import implements
+from zope.interface import implementer
 
 from txtorcon import Stream
 from txtorcon import IStreamListener
@@ -16,8 +18,8 @@ class FakeCircuit:
         self.id = id
 
 
+ at implementer(IStreamListener)
 class Listener(object):
-    implements(IStreamListener)
 
     def __init__(self, expected):
         "expect is a list of tuples: (event, {key:value, key1:value1, ..})"
@@ -37,7 +39,7 @@ class Listener(object):
             elif k == 'kwargs':
                 for (key, value) in v.items():
                     if key not in kw:
-                        print key, value, k, v, kw
+                        print(key, value, k, v, kw)
                         raise RuntimeError(
                             'Expected keyword argument for key "%s" but found nothing.' % key
                         )
@@ -78,10 +80,9 @@ class Listener(object):
         self.checker('failed', stream, **kw)
 
 
+ at implementer(ICircuitContainer)
 class StreamTests(unittest.TestCase):
 
-    implements(ICircuitContainer)
-
     def find_circuit(self, id):
         return self.circuits[id]
 
@@ -124,6 +125,7 @@ class StreamTests(unittest.TestCase):
         """A listener throws an exception during notify"""
 
         exc = Exception("the bad stuff happened")
+
         class Bad(StreamListenerMixin):
             def stream_new(*args, **kw):
                 raise exc
@@ -217,7 +219,7 @@ class StreamTests(unittest.TestCase):
         try:
             stream.update("999 SENTCONNECT 186 1.2.3.4:80 SOURCE=EXIT".split())
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue('wrong stream' in str(e))
 
     def test_update_illegal_state(self):
@@ -227,7 +229,7 @@ class StreamTests(unittest.TestCase):
         try:
             stream.update("316 FOO 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split())
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue('Unknown state' in str(e))
 
     def test_listen_unlisten(self):
diff --git a/test/test_torconfig.py b/test/test_torconfig.py
index a9668b6..7d47a2e 100644
--- a/test/test_torconfig.py
+++ b/test/test_torconfig.py
@@ -1,43 +1,34 @@
+from __future__ import print_function
+
 import os
 import shutil
 import tempfile
 import functools
-from getpass import getuser
-from mock import patch
-from StringIO import StringIO
-
+from six import StringIO
 from mock import Mock, patch
 
-from zope.interface import implements
+from zope.interface import implementer, directlyProvides
 from twisted.trial import unittest
 from twisted.test import proto_helpers
-from twisted.internet import defer, error, task, tcp
-from twisted.internet.endpoints import TCP4ServerEndpoint, serverFromString
-from twisted.python.failure import Failure
+from twisted.internet import defer
 from twisted.internet.interfaces import IReactorCore
-from twisted.internet.interfaces import IProtocolFactory
-from twisted.internet.interfaces import IProtocol
-from twisted.internet.interfaces import IReactorTCP
-from twisted.internet.interfaces import IListeningPort
-from twisted.internet.interfaces import IAddress
 
-from txtorcon import TorControlProtocol
+from txtorcon import TorProtocolError
 from txtorcon import ITorControlProtocol
+from txtorcon import TorProcessProtocol
 from txtorcon import TorConfig
 from txtorcon import DEFAULT_VALUE
 from txtorcon import HiddenService
-from txtorcon import launch_tor
-from txtorcon import TCPHiddenServiceEndpoint
+from txtorcon import launch
 from txtorcon import TorNotFound
-from txtorcon import TCPHiddenServiceEndpointParser
-from txtorcon import IProgressProvider
 from txtorcon import torconfig
-from txtorcon.torconfig import TorProcessProtocol
 
-from txtorcon.util import delete_file_or_tree
 from txtorcon.torconfig import parse_client_keys
+from txtorcon.torconfig import CommaList
+from txtorcon.torconfig import launch_tor
 
 
+ at implementer(ITorControlProtocol)     # actually, just get_info_raw
 class FakeControlProtocol:
     """
     This is a little weird, but in most tests the answer at the top of
@@ -53,8 +44,6 @@ class FakeControlProtocol:
     Chance (always?) that the callback just runs right away.
     """
 
-    implements(ITorControlProtocol)     # actually, just get_info_raw
-
     def __init__(self, answers):
         self.answers = answers
         self.pending = []
@@ -64,6 +53,13 @@ class FakeControlProtocol:
         self.events = {}  #: event type -> callback
         self.pending_events = {}  #: event type -> list
         self.is_owned = -1
+        self.commands = []
+        self.version = "0.2.8.0"
+
+    def queue_command(self, cmd):
+        d = defer.Deferred()
+        self.commands.append((cmd, d))
+        return d
 
     def event_happened(self, event_type, *args):
         '''
@@ -71,7 +67,9 @@ class FakeControlProtocol:
         is added.  XXX Also if we've *already* added one? Do that if
         there's a use-case for it
         '''
-        if event_type in self.pending_events:
+        if event_type in self.events:
+            self.events[event_type](*args)
+        elif event_type in self.pending_events:
             self.pending_events[event_type].append(args)
         else:
             self.pending_events[event_type] = [args]
@@ -136,10 +134,6 @@ class CheckAnswer:
 
 
 class ConfigTests(unittest.TestCase):
-    """
-    FIXME hmm, this all seems a little convoluted to test errors?
-    Maybe not that bad.
-    """
 
     def setUp(self):
         self.protocol = FakeControlProtocol([])
@@ -150,6 +144,19 @@ class ConfigTests(unittest.TestCase):
         cfg = TorConfig(self.protocol)
         return self.assertFailure(cfg.post_bootstrap, ValueError)
 
+    def test_create(self):
+
+        @implementer(ITorControlProtocol)
+        class FakeProtocol(object):
+            post_bootstrap = defer.succeed(None)
+
+            def add_event_listener(*args, **kw):
+                pass
+
+            def get_info_raw(*args, **kw):
+                return defer.succeed('config/names=')
+        TorConfig.from_protocol(FakeProtocol())
+
     def test_contains(self):
         cfg = TorConfig()
         cfg.ControlPort = 4455
@@ -393,7 +400,7 @@ class ConfigTests(unittest.TestCase):
         try:
             conf.foo
             self.assertTrue(False)
-        except KeyError, e:
+        except KeyError as e:
             self.assertTrue('foo' in str(e))
 
     def test_invalid_parser(self):
@@ -418,7 +425,7 @@ class ConfigTests(unittest.TestCase):
         self.protocol.answers.append({'SomethingExciting': 'a,b'})
         conf = TorConfig(self.protocol)
 
-        from txtorcon.torconfig import CommaList, HiddenService
+        from txtorcon.torconfig import HiddenService
         self.assertEqual(conf.get_type('SomethingExciting'), CommaList)
         self.assertEqual(conf.get_type('HiddenServices'), HiddenService)
 
@@ -431,7 +438,7 @@ class ConfigTests(unittest.TestCase):
         self.assertEqual(conf.HiddenServices[0], hs)
 
     def foo(self, *args):
-        print "FOOO", args
+        print("FOOO", args)
 
     def test_slutty_postbootstrap(self):
         # test that doPostbootstrap still works in "slutty" mode
@@ -703,7 +710,7 @@ class LogTests(unittest.TestCase):
         try:
             conf.log = ('this', 'is', 'a', 'tuple')
             self.fail()
-        except ValueError, e:
+        except ValueError as e:
             self.assertTrue('Not valid' in str(e))
 
 
@@ -745,7 +752,8 @@ class EventTests(unittest.TestCase):
         protocol.answers.append({'Foo': '0'})
         protocol.answers.append({'Bar': '1'})
 
-        config = TorConfig(protocol)
+        # Doing It For The Side Effects. Hoo boy.
+        TorConfig(protocol)
         # Initial value is not tested here
         try:
             protocol.events['CONF_CHANGED']('Foo=INVALID\nBar=VALUES')
@@ -779,6 +787,129 @@ Log 90 127.0.0.1:90
 SocksPort 1234''')
 
 
+class SocksEndpointTests(unittest.TestCase):
+
+    def setUp(self):
+        self.reactor = Mock()
+        self.config = TorConfig()
+        self.config.SocksPort = []
+
+    def test_nothing_configurd(self):
+        with self.assertRaises(Exception) as ctx:
+            self.config.socks_endpoint(self.reactor, '1234')
+        self.assertTrue('No SOCKS ports configured' in str(ctx.exception))
+
+    def test_default(self):
+        self.config.SocksPort = ['1234', '4321']
+        ep = self.config.socks_endpoint(self.reactor)
+
+        factory = Mock()
+        ep.connect(factory)
+        self.assertEqual(1, len(self.reactor.mock_calls))
+        call = self.reactor.mock_calls[0]
+        self.assertEqual('connectTCP', call[0])
+        self.assertEqual('127.0.0.1', call[1][0])
+        self.assertEqual(1234, call[1][1])
+
+    def test_explicit_host(self):
+        self.config.SocksPort = ['127.0.0.20:1234']
+        ep = self.config.socks_endpoint(self.reactor)
+
+        factory = Mock()
+        ep.connect(factory)
+        self.assertEqual(1, len(self.reactor.mock_calls))
+        call = self.reactor.mock_calls[0]
+        self.assertEqual('connectTCP', call[0])
+        self.assertEqual('127.0.0.20', call[1][0])
+        self.assertEqual(1234, call[1][1])
+
+    def test_something_not_configured(self):
+        self.config.SocksPort = ['1234', '4321']
+        with self.assertRaises(Exception) as ctx:
+            self.config.socks_endpoint(self.reactor, '1111')
+        self.assertTrue('No SOCKSPort configured' in str(ctx.exception))
+
+    def test_unix_socks(self):
+        self.config.SocksPort = ['unix:/foo']
+        self.config.socks_endpoint(self.reactor, 'unix:/foo')
+
+    def test_with_options(self):
+        self.config.SocksPort = ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth']
+        ep = self.config.socks_endpoint(self.reactor, 9150)
+
+        factory = Mock()
+        ep.connect(factory)
+        self.assertEqual(1, len(self.reactor.mock_calls))
+        call = self.reactor.mock_calls[0]
+        self.assertEqual('connectTCP', call[0])
+        self.assertEqual('127.0.0.1', call[1][0])
+        self.assertEqual(9150, call[1][1])
+
+    def test_with_options_in_ask(self):
+        self.config.SocksPort = ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth']
+
+        with self.assertRaises(Exception) as ctx:
+            self.config.socks_endpoint(self.reactor,
+                                       '9150 KeepAliveIsolateSOCKSAuth')
+        self.assertTrue("Can't specify options" in str(ctx.exception))
+
+
+class CreateSocksEndpointTests(unittest.TestCase):
+
+    def setUp(self):
+        self.reactor = Mock()
+        self.config = TorConfig()
+        self.config.SocksPort = []
+        self.config.bootstrap = defer.succeed(self.config)
+
+    @defer.inlineCallbacks
+    def test_create_default_no_ports(self):
+        with self.assertRaises(Exception) as ctx:
+            yield self.config.create_socks_endpoint(self.reactor, None)
+        self.assertTrue('no SocksPorts configured' in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_create_default(self):
+        self.config.SocksPort = ['9150']
+        ep = yield self.config.create_socks_endpoint(self.reactor, None)
+
+        factory = Mock()
+        ep.connect(factory)
+        self.assertEqual(1, len(self.reactor.mock_calls))
+        call = self.reactor.mock_calls[0]
+        self.assertEqual('connectTCP', call[0])
+        self.assertEqual('127.0.0.1', call[1][0])
+        self.assertEqual(9150, call[1][1])
+
+    @defer.inlineCallbacks
+    def test_create_tcp(self):
+        ep = yield self.config.create_socks_endpoint(
+            self.reactor, "9050",
+        )
+
+        factory = Mock()
+        ep.connect(factory)
+        self.assertEqual(1, len(self.reactor.mock_calls))
+        call = self.reactor.mock_calls[0]
+        self.assertEqual('connectTCP', call[0])
+        self.assertEqual('127.0.0.1', call[1][0])
+        self.assertEqual(9050, call[1][1])
+
+    @defer.inlineCallbacks
+    def test_create_error_on_save(self):
+        self.config.SocksPort = []
+
+        def boom(*args, **kw):
+            raise TorProtocolError(551, "Something bad happened")
+
+        with patch.object(TorConfig, 'save', boom):
+            with self.assertRaises(Exception) as ctx:
+                yield self.config.create_socks_endpoint(self.reactor, 'unix:/foo')
+        err = str(ctx.exception)
+        self.assertTrue('error from Tor' in err)
+        self.assertTrue('specific ownership/permissions requirements' in err)
+
+
 class HiddenServiceTests(unittest.TestCase):
 
     def setUp(self):
@@ -960,7 +1091,7 @@ HiddenServicePort=90 127.0.0.1:2345''')
         try:
             conf._setup_hidden_services('''FakeHiddenServiceKey=foo''')
             self.fail()
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('parse' in str(e))
 
     def test_hidden_service_directory_absolute_path(self):
@@ -1030,581 +1161,28 @@ HiddenServiceDir=/fake/path'''
         self.assertTrue(conf.needs_save())
 
 
-class FakeReactor(task.Clock):
-    implements(IReactorCore)
-
-    def __init__(self, test, trans, on_protocol):
-        super(FakeReactor, self).__init__()
-        self.test = test
-        self.transport = trans
-        self.on_protocol = on_protocol
-
-    def spawnProcess(self, processprotocol, bin, args, env, path,
-                     uid=None, gid=None, usePTY=None, childFDs=None):
-        self.protocol = processprotocol
-        self.protocol.makeConnection(self.transport)
-        self.transport.process_protocol = processprotocol
-        self.on_protocol(self.protocol)
-        return self.transport
-
-    def addSystemEventTrigger(self, *args):
-        self.test.assertEqual(args[0], 'before')
-        self.test.assertEqual(args[1], 'shutdown')
-        # we know this is just for the temporary file cleanup, so we
-        # nuke it right away to avoid polluting /tmp by calling the
-        # callback now.
-        args[2]()
-
-    def removeSystemEventTrigger(self, id):
-        pass
-
-
-class FakeProcessTransport(proto_helpers.StringTransportWithDisconnection):
-
-    pid = -1
-
-    def signalProcess(self, signame):
-        self.process_protocol.processEnded(
-            Failure(error.ProcessTerminated(signal=signame))
-        )
-
-    def closeStdin(self):
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived(
-            '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=90 '
-            'TAG=circuit_create SUMMARY="Establishing a Tor circuit"\r\n'
-        )
-        self.protocol.dataReceived(
-            '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=100 '
-            'TAG=done SUMMARY="Done"\r\n'
-        )
-
-
-class FakeProcessTransportNeverBootstraps(FakeProcessTransport):
-
-    pid = -1
-
-    def closeStdin(self):
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived('250 OK\r\n')
-        self.protocol.dataReceived(
-            '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=90 TAG=circuit_create '
-            'SUMMARY="Establishing a Tor circuit"\r\n')
-
-
-class FakeProcessTransportNoProtocol(FakeProcessTransport):
-    def closeStdin(self):
-        pass
-
-
-class LaunchTorTests(unittest.TestCase):
-
-    def setUp(self):
-        self.protocol = TorControlProtocol()
-        self.protocol.connectionMade = lambda: None
-        self.transport = proto_helpers.StringTransport()
-        self.protocol.makeConnection(self.transport)
-        self.clock = task.Clock()
-
-    def setup_complete_with_timer(self, proto):
-        proto._check_timeout.stop()
-        proto.checkTimeout()
-
-    def setup_complete_no_errors(self, proto, config, stdout, stderr):
-        self.assertEqual("Bootstrapped 100%\n", stdout.getvalue())
-        self.assertEqual("", stderr.getvalue())
-        todel = proto.to_delete
-        self.assertTrue(len(todel) > 0)
-        # ...because we know it's a TorProcessProtocol :/
-        proto.cleanup()
-        self.assertEqual(len(proto.to_delete), 0)
-        for f in todel:
-            self.assertTrue(not os.path.exists(f))
-        self.assertEqual(proto._timeout_delayed_call, None)
-
-        # make sure we set up the config to track the created tor
-        # protocol connection
-        self.assertEquals(config.protocol, proto.tor_protocol)
-
-    def setup_complete_fails(self, proto, stdout, stderr):
-        self.assertEqual("Bootstrapped 90%\n", stdout.getvalue())
-        self.assertEqual("", stderr.getvalue())
-        todel = proto.to_delete
-        self.assertTrue(len(todel) > 0)
-        # the "12" is just arbitrary, we check it later in the error-message
-        proto.processEnded(
-            Failure(error.ProcessTerminated(12, None, 'statusFIXME'))
-        )
-        self.assertEqual(1, len(self.flushLoggedErrors(RuntimeError)))
-        self.assertEqual(len(proto.to_delete), 0)
-        for f in todel:
-            self.assertTrue(not os.path.exists(f))
-        return None
-
-    @patch('txtorcon.torconfig.os.geteuid')
-    def test_basic_launch(self, geteuid):
-        # pretend we're root to exercise the "maybe chown data dir" codepath
-        geteuid.return_value = 0
-        config = TorConfig()
-        config.ORPort = 1234
-        config.SOCKSPort = 9999
-        config.User = getuser()
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        class OnProgress:
-            def __init__(self, test, expected):
-                self.test = test
-                self.expected = expected
-
-            def __call__(self, percent, tag, summary):
-                self.test.assertEqual(
-                    self.expected[0],
-                    (percent, tag, summary)
-                )
-                self.expected = self.expected[1:]
-                self.test.assertTrue('"' not in summary)
-                self.test.assertTrue(percent >= 0 and percent <= 100)
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 100%\n')
-            proto.progress = OnProgress(
-                self, [
-                    (90, 'circuit_create', 'Establishing a Tor circuit'),
-                    (100, 'done', 'Done'),
-                ]
-            )
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        fakeout = StringIO()
-        fakeerr = StringIO()
-        creator = functools.partial(connector, self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo',
-            stdout=fakeout,
-            stderr=fakeerr
-        )
-        d.addCallback(self.setup_complete_no_errors, config, fakeout, fakeerr)
-        return d
-
-    def check_setup_failure(self, fail):
-        self.assertTrue("with error-code 12" in fail.getErrorMessage())
-        # cancel the errback chain, we wanted this
-        return None
-
-    @defer.inlineCallbacks
-    def test_launch_tor_unix_controlport(self):
-        config = TorConfig()
-        config.ControlPort = "unix:/dev/null"
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        fakeout = StringIO()
-        fakeerr = StringIO()
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-
-        reactor = FakeReactor(self, trans, on_protocol)
-        reactor.connectUNIX = Mock()
-        try:
-            yield launch_tor(
-                config,
-                reactor,
-                tor_binary='/bin/echo',
-                stdout=fakeout,
-                stderr=fakeerr
-            )
-        except Exception:
-            pass
-        self.assertTrue(reactor.connectUNIX.called)
-        self.assertEqual(
-            '/dev/null',
-            reactor.connectUNIX.mock_calls[0][1][0],
-        )
-
-    def test_launch_tor_fails(self):
-        config = TorConfig()
-        config.OrPort = 1234
-        config.SocksPort = 9999
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        fakeout = StringIO()
-        fakeerr = StringIO()
-        creator = functools.partial(connector, self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo',
-            stdout=fakeout,
-            stderr=fakeerr
-        )
-        d.addCallback(self.setup_complete_fails, fakeout, fakeerr)
-        self.flushLoggedErrors(RuntimeError)
-        return d
-
-    def test_launch_with_timeout_no_ireactortime(self):
-        config = TorConfig()
-        return self.assertRaises(
-            RuntimeError,
-            launch_tor, config, None, timeout=5, tor_binary='/bin/echo'
-        )
-
-    @patch('txtorcon.torconfig.sys')
-    @patch('txtorcon.torconfig.pwd')
-    @patch('txtorcon.torconfig.os.geteuid')
-    @patch('txtorcon.torconfig.os.chown')
-    def test_launch_root_changes_tmp_ownership(self, chown, euid, _pwd, _sys):
-        _pwd.return_value = 1000
-        _sys.platform = 'linux2'
-        euid.return_value = 0
-        config = TorConfig()
-        config.User = 'chuffington'
-        d = launch_tor(config, Mock(), tor_binary='/bin/echo')
-        self.assertEqual(1, chown.call_count)
+class IteratorTests(unittest.TestCase):
+    def test_iterate_torconfig(self):
+        cfg = TorConfig()
+        cfg.FooBar = 'quux'
+        cfg.save()
+        cfg.Quux = 'blimblam'
 
-    @defer.inlineCallbacks
-    def test_launch_timeout_exception(self):
-        self.protocol = FakeControlProtocol([])
-        self.protocol.answers.append('''config/names=
-DataDirectory String
-ControlPort Port''')
-        self.protocol.answers.append({'DataDirectory': 'foo'})
-        self.protocol.answers.append({'ControlPort': 0})
-        config = TorConfig(self.protocol)
-        yield config.post_bootstrap
-        config.DataDirectory = '/dev/null'
+        keys = sorted([k for k in cfg])
 
-        trans = Mock()
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, Mock()),
-            tor_binary='/bin/echo'
-        )
-        tpp = yield d
-        tpp.transport = trans
-        trans.signalProcess = Mock(side_effect=error.ProcessExitedAlready)
-        trans.loseConnection = Mock()
+        self.assertEqual(['FooBar', 'Quux'], keys)
 
-        tpp.timeout_expired()
 
-        self.assertTrue(tpp.transport.loseConnection.called)
+class LegacyLaunchTorTests(unittest.TestCase):
+    """
+    Test backwards-compatibility on launch_tor()
+    """
 
+    @patch('txtorcon.controller.find_tor_binary', return_value=None)
+    @patch('twisted.python.deprecate.warn')
     @defer.inlineCallbacks
-    def test_launch_timeout_process_exits(self):
-        # cover the "one more edge case" where we get a processEnded()
-        # but we've already "done" a timeout.
-        self.protocol = FakeControlProtocol([])
-        self.protocol.answers.append('''config/names=
-DataDirectory String
-ControlPort Port''')
-        self.protocol.answers.append({'DataDirectory': 'foo'})
-        self.protocol.answers.append({'ControlPort': 0})
-        config = TorConfig(self.protocol)
-        yield config.post_bootstrap
-        config.DataDirectory = '/dev/null'
-
-        trans = Mock()
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, Mock()),
-            tor_binary='/bin/echo'
-        )
-        tpp = yield d
-        tpp.timeout_expired()
-        tpp.transport = trans
-        trans.signalProcess = Mock()
-        trans.loseConnection = Mock()
-        status = Mock()
-        status.value.exitCode = None
-        self.assertTrue(tpp._did_timeout)
-        tpp.processEnded(status)
-
-        errs = self.flushLoggedErrors(RuntimeError)
-        self.assertEqual(1, len(errs))
-
-    def test_launch_wrong_stdout(self):
-        config = TorConfig()
-        try:
-            launch_tor(config, None, stdout=object(), tor_binary='/bin/echo')
-            self.fail("Should have thrown an error")
-        except RuntimeError:
-            pass
-
-    def test_launch_with_timeout(self):
-        config = TorConfig()
-        config.OrPort = 1234
-        config.SocksPort = 9999
-        timeout = 5
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        class OnProgress:
-            def __init__(self, test, expected):
-                self.test = test
-                self.expected = expected
-
-            def __call__(self, percent, tag, summary):
-                self.test.assertEqual(
-                    self.expected[0],
-                    (percent, tag, summary)
-                )
-                self.expected = self.expected[1:]
-                self.test.assertTrue('"' not in summary)
-                self.test.assertTrue(percent >= 0 and percent <= 100)
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 100%\n')
-
-        trans = FakeProcessTransportNeverBootstraps()
-        trans.protocol = self.protocol
-        creator = functools.partial(connector, self.protocol, self.transport)
-        react = FakeReactor(self, trans, on_protocol)
-        d = launch_tor(config, react, connection_creator=creator,
-                       timeout=timeout, tor_binary='/bin/echo')
-        # FakeReactor is a task.Clock subclass and +1 just to be sure
-        react.advance(timeout + 1)
-
-        self.assertTrue(d.called)
-        self.assertTrue(
-            d.result.getErrorMessage().strip().endswith('Tor was killed (TERM).')
-        )
-        self.flushLoggedErrors(RuntimeError)
-        return self.assertFailure(d, RuntimeError)
-
-    def test_launch_with_timeout_that_doesnt_expire(self):
-        config = TorConfig()
-        config.OrPort = 1234
-        config.SocksPort = 9999
-        timeout = 5
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        class OnProgress:
-            def __init__(self, test, expected):
-                self.test = test
-                self.expected = expected
-
-            def __call__(self, percent, tag, summary):
-                self.test.assertEqual(
-                    self.expected[0],
-                    (percent, tag, summary)
-                )
-                self.expected = self.expected[1:]
-                self.test.assertTrue('"' not in summary)
-                self.test.assertTrue(percent >= 0 and percent <= 100)
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 100%\n')
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        creator = functools.partial(connector, self.protocol, self.transport)
-        react = FakeReactor(self, trans, on_protocol)
-        d = launch_tor(config, react, connection_creator=creator,
-                       timeout=timeout, tor_binary='/bin/echo')
-        # FakeReactor is a task.Clock subclass and +1 just to be sure
-        react.advance(timeout + 1)
-
-        self.assertTrue(d.called)
-        self.assertTrue(d.result.tor_protocol == self.protocol)
-
-    def setup_fails_stderr(self, fail, stdout, stderr):
-        self.assertEqual('', stdout.getvalue())
-        self.assertEqual('Something went horribly wrong!\n', stderr.getvalue())
-        self.assertTrue(
-            'Something went horribly wrong!' in fail.getErrorMessage()
-        )
-        # cancel the errback chain, we wanted this
-        return None
-
-    def test_tor_produces_stderr_output(self):
-        config = TorConfig()
-        config.OrPort = 1234
-        config.SocksPort = 9999
-
-        def connector(proto, trans):
-            proto._set_valid_events('STATUS_CLIENT')
-            proto.makeConnection(trans)
-            proto.post_bootstrap.callback(proto)
-            return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.errReceived('Something went horribly wrong!\n')
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        fakeout = StringIO()
-        fakeerr = StringIO()
-        creator = functools.partial(connector, self.protocol, self.transport)
-        d = launch_tor(config, FakeReactor(self, trans, on_protocol),
-                       connection_creator=creator, tor_binary='/bin/echo',
-                       stdout=fakeout, stderr=fakeerr)
-        d.addCallback(self.fail)        # should't get callback
-        d.addErrback(self.setup_fails_stderr, fakeout, fakeerr)
-        self.assertFalse(self.protocol.on_disconnect)
-        return d
-
-    def test_tor_connection_fails(self):
-        """
-        We fail to connect once, and then successfully connect --
-        testing whether we're retrying properly on each Bootstrapped
-        line from stdout.
-        """
-
-        config = TorConfig()
-        config.OrPort = 1234
-        config.SocksPort = 9999
-
-        class Connector:
-            count = 0
-
-            def __call__(self, proto, trans):
-                self.count += 1
-                if self.count < 2:
-                    return defer.fail(
-                        error.CannotListenError(None, None, None)
-                    )
-
-                proto._set_valid_events('STATUS_CLIENT')
-                proto.makeConnection(trans)
-                proto.post_bootstrap.callback(proto)
-                return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        creator = functools.partial(Connector(), self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo'
-        )
-        d.addCallback(self.setup_complete_fails)
-        return self.assertFailure(d, Exception)
-
-    def test_tor_connection_user_data_dir(self):
-        """
-        Test that we don't delete a user-supplied data directory.
-        """
-
-        config = TorConfig()
-        config.OrPort = 1234
-
-        class Connector:
-            def __call__(self, proto, trans):
-                proto._set_valid_events('STATUS_CLIENT')
-                proto.makeConnection(trans)
-                proto.post_bootstrap.callback(proto)
-                return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-
-        my_dir = tempfile.mkdtemp(prefix='tortmp')
-        config.DataDirectory = my_dir
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        creator = functools.partial(Connector(), self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo'
-        )
-
-        def still_have_data_dir(proto, tester):
-            proto.cleanup()  # FIXME? not really unit-testy as this is sort of internal function
-            tester.assertTrue(os.path.exists(my_dir))
-            delete_file_or_tree(my_dir)
-
-        d.addCallback(still_have_data_dir, self)
-        d.addErrback(self.fail)
-        return d
-
-    def test_tor_connection_user_control_port(self):
-        """
-        Confirm we use a user-supplied control-port properly
-        """
-
-        config = TorConfig()
-        config.OrPort = 1234
-        config.ControlPort = 4321
-
-        class Connector:
-            def __call__(self, proto, trans):
-                proto._set_valid_events('STATUS_CLIENT')
-                proto.makeConnection(trans)
-                proto.post_bootstrap.callback(proto)
-                return proto.post_bootstrap
-
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-            proto.outReceived('Bootstrapped 100%\n')
-
-        trans = FakeProcessTransport()
-        trans.protocol = self.protocol
-        creator = functools.partial(Connector(), self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo'
-        )
-
-        def check_control_port(proto, tester):
-            # we just want to ensure launch_tor() didn't mess with
-            # the controlport we set
-            tester.assertEquals(config.ControlPort, 4321)
-
-        d.addCallback(check_control_port, self)
-        d.addErrback(self.fail)
-        return d
-
-    def test_tor_connection_default_control_port(self):
-        """
-        Confirm a default control-port is set if not user-supplied.
-        """
-
-        config = TorConfig()
+    def test_happy_path(self, warn, ftb):
+        self.transport = proto_helpers.StringTransport()
 
         class Connector:
             def __call__(self, proto, trans):
@@ -1613,140 +1191,37 @@ ControlPort Port''')
                 proto.post_bootstrap.callback(proto)
                 return proto.post_bootstrap
 
-        def on_protocol(proto):
-            proto.outReceived('Bootstrapped 90%\n')
-            proto.outReceived('Bootstrapped 100%\n')
-
-        trans = FakeProcessTransport()
+        self.protocol = FakeControlProtocol([])
+        trans = Mock()
         trans.protocol = self.protocol
         creator = functools.partial(Connector(), self.protocol, self.transport)
-        d = launch_tor(
-            config,
-            FakeReactor(self, trans, on_protocol),
-            connection_creator=creator,
-            tor_binary='/bin/echo'
-        )
-
-        def check_control_port(proto, tester):
-            # ensure ControlPort was set to a default value
-            tester.assertEquals(config.ControlPort, 9052)
-
-        d.addCallback(check_control_port, self)
-        d.addErrback(self.fail)
-        return d
-
-    def test_progress_updates(self):
-        self.got_progress = False
-
-        def confirm_progress(p, t, s):
-            self.assertEqual(p, 10)
-            self.assertEqual(t, 'tag')
-            self.assertEqual(s, 'summary')
-            self.got_progress = True
-        process = TorProcessProtocol(None, confirm_progress)
-        process.progress(10, 'tag', 'summary')
-        self.assertTrue(self.got_progress)
-
-    def test_quit_process(self):
-        process = TorProcessProtocol(None)
-        process.transport = Mock()
-
-        d = process.quit()
-        self.assertFalse(d.called)
-
-        process.processExited(Failure(error.ProcessTerminated(exitCode=15)))
-        self.assertTrue(d.called)
-        process.processEnded(Failure(error.ProcessDone(None)))
-        self.assertTrue(d.called)
-        errs = self.flushLoggedErrors()
-        self.assertEqual(1, len(errs))
-        self.assertTrue("Tor exited with error-code" in str(errs[0]))
-
-    def test_quit_process_already(self):
-        process = TorProcessProtocol(None)
-        process.transport = Mock()
-
-        def boom(sig):
-            self.assertEqual(sig, 'TERM')
-            raise error.ProcessExitedAlready()
-        process.transport.signalProcess = Mock(side_effect=boom)
-
-        d = process.quit()
-        process.processEnded(Failure(error.ProcessDone(None)))
-        self.assertTrue(d.called)
-        errs = self.flushLoggedErrors()
-        self.assertEqual(1, len(errs))
-        self.assertTrue("Tor exited with error-code" in str(errs[0]))
-
-    def test_status_updates(self):
-        process = TorProcessProtocol(None)
-        process.status_client("NOTICE CONSENSUS_ARRIVED")
-
-    def test_tor_launch_success_then_shutdown(self):
-        """
-        There was an error where we double-callbacked a deferred,
-        i.e. success and then shutdown. This repeats it.
-        """
-        process = TorProcessProtocol(None)
-        process.status_client(
-            'STATUS_CLIENT BOOTSTRAP PROGRESS=100 TAG=foo SUMMARY=cabbage'
-        )
-        self.assertEqual(None, process.connected_cb)
-
-        class Value(object):
-            exitCode = 123
-
-        class Status(object):
-            value = Value()
-        process.processEnded(Status())
-        self.assertEquals(len(self.flushLoggedErrors(RuntimeError)), 1)
-
-    def test_launch_tor_no_control_port(self):
-        '''
-        See Issue #80. This allows you to launch tor with a TorConfig
-        with ControlPort=0 in case you don't want a control connection
-        at all. In this case you get back a TorProcessProtocol and you
-        own both pieces. (i.e. you have to kill it yourself).
-        '''
-
-        config = TorConfig()
-        config.ControlPort = 0
-        trans = FakeProcessTransportNoProtocol()
-        trans.protocol = self.protocol
-
-        def creator(*args, **kw):
-            print "Bad: connection creator called"
-            self.fail()
-
-        def on_protocol(proto):
-            self.process_proto = proto
-        pp = launch_tor(config,
-                        FakeReactor(self, trans, on_protocol),
-                        connection_creator=creator, tor_binary='/bin/echo')
-        self.assertTrue(pp.called)
-        self.assertEqual(pp.result, self.process_proto)
-        return pp
-
-
-class IteratorTests(unittest.TestCase):
-    def test_iterate_torconfig(self):
-        cfg = TorConfig()
-        cfg.FooBar = 'quux'
-        cfg.save()
-        cfg.Quux = 'blimblam'
-
-        keys = sorted([k for k in cfg])
-
-        self.assertEqual(['FooBar', 'Quux'], keys)
+        reactor = Mock()
+        config = Mock()
+        fake_tor = Mock()
+        fake_tor.process = TorProcessProtocol(creator)
+
+        with patch('txtorcon.controller.launch', return_value=fake_tor) as launch:
+            directlyProvides(reactor, IReactorCore)
+            tpp = yield launch_tor(
+                config,
+                reactor,
+                connection_creator=creator
+            )
+            self.assertEqual(1, len(launch.mock_calls))
+            self.assertTrue(
+                isinstance(tpp, TorProcessProtocol)
+            )
+            self.assertIs(tpp, fake_tor.process)
+        calls = warn.mock_calls
+        self.assertEqual(1, len(calls))
+        self.assertEqual(calls[0][1][1], DeprecationWarning)
 
 
 class ErrorTests(unittest.TestCase):
-    @patch('txtorcon.torconfig.find_tor_binary')
+    @patch('txtorcon.controller.find_tor_binary', return_value=None)
+    @defer.inlineCallbacks
     def test_no_tor_binary(self, ftb):
-        """FIXME: do I really need all this crap in here?"""
         self.transport = proto_helpers.StringTransport()
-        config = TorConfig()
-        d = None
 
         class Connector:
             def __call__(self, proto, trans):
@@ -1756,14 +1231,14 @@ class ErrorTests(unittest.TestCase):
                 return proto.post_bootstrap
 
         self.protocol = FakeControlProtocol([])
-        torconfig.find_tor_binary = lambda: None
-        trans = FakeProcessTransport()
+        trans = Mock()
         trans.protocol = self.protocol
         creator = functools.partial(Connector(), self.protocol, self.transport)
+        reactor = Mock()
+        directlyProvides(reactor, IReactorCore)
         try:
-            d = launch_tor(
-                config,
-                FakeReactor(self, trans, lambda x: None),
+            yield launch(
+                reactor,
                 connection_creator=creator
             )
             self.fail()
@@ -1771,8 +1246,6 @@ class ErrorTests(unittest.TestCase):
         except TorNotFound:
             pass  # success!
 
-        return d
-
 
 # the RSA keys have been shortened below for readability
 keydata = '''client-name bar
@@ -1830,11 +1303,13 @@ class EphemeralHiddenServiceTest(unittest.TestCase):
         self.assertEqual(eph._ports, ["80,localhost:80"])
 
     def test_wrong_blob(self):
-        try:
-            eph = torconfig.EphemeralHiddenService("80 localhost:80", "foo")
-            self.fail("should get exception")
-        except RuntimeError as e:
-            pass
+        wrong_blobs = ["", " ", "foo", ":", " : ", "foo:", ":foo", 0]
+        for b in wrong_blobs:
+            try:
+                torconfig.EphemeralHiddenService("80 localhost:80", b)
+                self.fail("should get exception")
+            except ValueError:
+                pass
 
     def test_add(self):
         eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
@@ -1845,6 +1320,15 @@ class EphemeralHiddenServiceTest(unittest.TestCase):
         self.assertEqual("blam", eph.private_key)
         self.assertEqual("ohai.onion", eph.hostname)
 
+    def test_add_keyblob(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80", "alg:blam")
+        proto = Mock()
+        proto.queue_command = Mock(return_value="ServiceID=ohai")
+        eph.add_to_tor(proto)
+
+        self.assertEqual("alg:blam", eph.private_key)
+        self.assertEqual("ohai.onion", eph.hostname)
+
     def test_descriptor_wait(self):
         eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
         proto = Mock()
@@ -1882,7 +1366,7 @@ class EphemeralHiddenServiceTest(unittest.TestCase):
         try:
             yield eph.remove_from_tor(proto)
             self.fail("should have gotten exception")
-        except RuntimeError as e:
+        except RuntimeError:
             pass
 
     def test_failed_upload(self):
diff --git a/test/test_torcontrolprotocol.py b/test/test_torcontrolprotocol.py
index 9da82a6..088c07b 100644
--- a/test/test_torcontrolprotocol.py
+++ b/test/test_torcontrolprotocol.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 from __future__ import with_statement
 
 from os.path import exists
@@ -12,10 +13,10 @@ from txtorcon import ITorControlProtocol
 from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE
 from txtorcon.util import hmac_sha256
 
-import types
 import functools
 import tempfile
 import base64
+from binascii import b2a_hex, a2b_hex
 
 
 class CallbackChecker:
@@ -26,7 +27,7 @@ class CallbackChecker:
     def __call__(self, *args, **kwargs):
         v = args[0]
         if v != self.expected_value:
-            print "WRONG"
+            print("WRONG")
             raise RuntimeError(
                 'Expected "%s" but got "%s"' % (self.expected_value, v)
             )
@@ -73,80 +74,99 @@ class AuthenticationTests(unittest.TestCase):
         self.transport = proto_helpers.StringTransport()
 
     def send(self, line):
-        self.protocol.dataReceived(line.strip() + "\r\n")
+        assert type(line) == bytes
+        self.protocol.dataReceived(line.strip() + b"\r\n")
 
     def test_authenticate_cookie(self):
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
         self.transport.clear()
-        cookie_data = 'cookiedata!cookiedata!cookiedata'
-        with open('authcookie', 'w') as f:
+        cookie_data = b'cookiedata!cookiedata!cookiedata'
+        with open('authcookie', 'wb') as f:
             f.write(cookie_data)
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="authcookie"')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="authcookie"')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
 
         self.assertEqual(
             self.transport.value(),
-            'AUTHENTICATE %s\r\n' % cookie_data.encode("hex")
+            b'AUTHENTICATE ' + b2a_hex(cookie_data) + b'\r\n',
         )
 
     def test_authenticate_password(self):
         self.protocol.password_function = lambda: 'foo'
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
         self.transport.clear()
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=HASHEDPASSWORD')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
 
-        self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
+        self.assertEqual(
+            self.transport.value(),
+            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n'
+        )
+
+    def test_authenticate_password_not_bytes(self):
+        self.protocol.password_function = lambda: u'foo'
+        self.protocol.makeConnection(self.transport)
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
+        self.transport.clear()
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
+
+        self.assertEqual(
+            self.transport.value(),
+            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n'
+        )
 
     def test_authenticate_null(self):
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
         self.transport.clear()
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=NULL')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=NULL')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
 
-        self.assertEqual(self.transport.value(), 'AUTHENTICATE\r\n')
+        self.assertEqual(self.transport.value(), b'AUTHENTICATE\r\n')
 
     def test_authenticate_password_deferred(self):
         d = defer.Deferred()
         self.protocol.password_function = lambda: d
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
         self.transport.clear()
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=HASHEDPASSWORD')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
 
         # make sure we haven't tried to authenticate before getting
         # the password callback
-        self.assertEqual(self.transport.value(), '')
+        self.assertEqual(self.transport.value(), b'')
         d.callback('foo')
 
         # now make sure we DID try to authenticate
         self.assertEqual(
             self.transport.value(),
-            'AUTHENTICATE %s\r\n' % "foo".encode("hex")
+            b'AUTHENTICATE ' + b2a_hex(b"foo") + b'\r\n'
         )
 
     def test_authenticate_password_deferred_but_no_password(self):
         d = defer.Deferred()
         self.protocol.password_function = lambda: d
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
         self.transport.clear()
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=HASHEDPASSWORD')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
         d.callback(None)
         return self.assertFailure(self.protocol.post_bootstrap, RuntimeError)
 
@@ -158,12 +178,12 @@ class AuthenticationTests(unittest.TestCase):
         self.auth_failed = False
 
         self.protocol.makeConnection(self.transport)
-        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n')
 
-        self.send('250-PROTOCOLINFO 1')
-        self.send('250-AUTH METHODS=HASHEDPASSWORD')
-        self.send('250-VERSION Tor="0.2.2.34"')
-        self.send('250 OK')
+        self.send(b'250-PROTOCOLINFO 1')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250-VERSION Tor="0.2.2.34"')
+        self.send(b'250 OK')
 
         self.assertTrue(self.auth_failed)
 
@@ -223,13 +243,14 @@ class ProtocolTests(unittest.TestCase):
         self.protocol = None
 
     def send(self, line):
-        self.protocol.dataReceived(line.strip() + "\r\n")
+        assert type(line) == bytes
+        self.protocol.dataReceived(line.strip() + b"\r\n")
 
     def test_statemachine_broadcast_no_code(self):
         try:
             self.protocol._broadcast_response("foo")
             self.fail()
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('No code set yet' in str(e))
 
     def test_statemachine_broadcast_unknown_code(self):
@@ -237,7 +258,7 @@ class ProtocolTests(unittest.TestCase):
             self.protocol.code = 999
             self.protocol._broadcast_response("foo")
             self.fail()
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('Unknown code' in str(e))
 
     def test_statemachine_is_finish(self):
@@ -254,7 +275,7 @@ class ProtocolTests(unittest.TestCase):
             self.protocol.code = 250
             self.protocol._is_continuation_line("123 ")
             self.fail()
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('Unexpected code' in str(e))
 
     def test_statemachine_multiline(self):
@@ -262,9 +283,17 @@ class ProtocolTests(unittest.TestCase):
             self.protocol.code = 250
             self.protocol._is_multi_line("123 ")
             self.fail()
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('Unexpected code' in str(e))
 
+    def test_response_with_no_request(self):
+        with self.assertRaises(RuntimeError) as ctx:
+            self.protocol.code = 200
+            self.protocol._broadcast_response('200 OK')
+        self.assertTrue(
+            "didn't issue a command" in str(ctx.exception)
+        )
+
     def auth_failed(self, msg):
         self.assertEqual(str(msg.value), '551 go away')
         self.got_auth_failed = True
@@ -278,7 +307,7 @@ class ProtocolTests(unittest.TestCase):
 AUTH METHODS=HASHEDPASSWORD
 VERSION Tor="0.2.2.35"
 OK''')
-        self.send('551 go away\r\n')
+        self.send(b'551 go away\r\n')
         self.assertTrue(self.got_auth_failed)
 
     def test_authenticate_no_auth_line(self):
@@ -288,12 +317,12 @@ FOOAUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/dev/null"
 VERSION Tor="0.2.2.35"
 OK''')
             self.assertTrue(False)
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('find AUTH line' in str(e))
 
     def test_authenticate_not_enough_cookie_data(self):
         with tempfile.NamedTemporaryFile() as cookietmp:
-            cookietmp.write('x' * 35)  # too much data
+            cookietmp.write(b'x' * 35)  # too much data
             cookietmp.flush()
 
             try:
@@ -302,12 +331,12 @@ AUTH METHODS=COOKIE COOKIEFILE="%s"
 VERSION Tor="0.2.2.35"
 OK''' % cookietmp.name)
                 self.assertTrue(False)
-            except RuntimeError, e:
+            except RuntimeError as e:
                 self.assertTrue('cookie to be 32' in str(e))
 
     def test_authenticate_not_enough_safecookie_data(self):
         with tempfile.NamedTemporaryFile() as cookietmp:
-            cookietmp.write('x' * 35)  # too much data
+            cookietmp.write(b'x' * 35)  # too much data
             cookietmp.flush()
 
             try:
@@ -316,46 +345,48 @@ AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
 VERSION Tor="0.2.2.35"
 OK''' % cookietmp.name)
                 self.assertTrue(False)
-            except RuntimeError, e:
+            except RuntimeError as e:
                 self.assertTrue('cookie to be 32' in str(e))
 
     def test_authenticate_safecookie(self):
         with tempfile.NamedTemporaryFile() as cookietmp:
-            cookiedata = str(bytearray([0] * 32))
+            cookiedata = bytes(bytearray([0] * 32))
             cookietmp.write(cookiedata)
             cookietmp.flush()
 
             self.protocol._do_authenticate('''PROTOCOLINFO 1
-AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
+AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
 VERSION Tor="0.2.2.35"
-OK''' % cookietmp.name)
+OK'''.format(cookietmp.name))
             self.assertTrue(
-                'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value()
+                b'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value()
             )
-            client_nonce = base64.b16decode(self.transport.value().split()[-1])
+            x = self.transport.value().split()[-1]
+            client_nonce = a2b_hex(x)
             self.transport.clear()
-            server_nonce = str(bytearray([0] * 32))
+            server_nonce = bytes(bytearray([0] * 32))
             server_hash = hmac_sha256(
-                "Tor safe cookie authentication server-to-controller hash",
-                cookiedata + client_nonce + server_nonce
+                b"Tor safe cookie authentication server-to-controller hash",
+                cookiedata + client_nonce + server_nonce,
             )
 
             self.send(
-                '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
-                (base64.b16encode(server_hash), base64.b16encode(server_nonce))
+                b'250 AUTHCHALLENGE SERVERHASH=' +
+                base64.b16encode(server_hash) + b' SERVERNONCE=' +
+                base64.b16encode(server_nonce) + b'\r\n'
             )
-            self.assertTrue('AUTHENTICATE ' in self.transport.value())
+            self.assertTrue(b'AUTHENTICATE ' in self.transport.value())
 
     def test_authenticate_cookie_without_reading(self):
-        server_nonce = str(bytearray([0] * 32))
-        server_hash = str(bytearray([0] * 32))
+        server_nonce = bytes(bytearray([0] * 32))
+        server_hash = bytes(bytearray([0] * 32))
         try:
             self.protocol._safecookie_authchallenge(
                 '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
                 (base64.b16encode(server_hash), base64.b16encode(server_nonce))
             )
             self.assertTrue(False)
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('not read' in str(e))
 
     def test_authenticate_unexisting_cookie_file(self):
@@ -373,9 +404,9 @@ OK''' % unexisting_file)
         unexisting_file = __file__ + "-unexisting"
         try:
             self.protocol._do_authenticate('''PROTOCOLINFO 1
-AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
+AUTH METHODS=SAFECOOKIE COOKIEFILE="{}"
 VERSION Tor="0.2.2.35"
-OK''' % unexisting_file)
+OK'''.format(unexisting_file))
             self.assertTrue(False)
         except RuntimeError:
             pass
@@ -394,24 +425,30 @@ OK''')
         unexisting_file = __file__ + "-unexisting"
         self.protocol.password_function = lambda: 'foo'
         self.protocol._do_authenticate('''PROTOCOLINFO 1
-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="%s"
+AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="{}"
 VERSION Tor="0.2.2.35"
-OK''' % unexisting_file)
-        self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
+OK'''.format(unexisting_file))
+        self.assertEqual(
+            self.transport.value(),
+            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
+        )
 
     def test_authenticate_password_when_safecookie_unavailable(self):
         unexisting_file = __file__ + "-unexisting"
         self.protocol.password_function = lambda: 'foo'
         self.protocol._do_authenticate('''PROTOCOLINFO 1
-AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="%s"
+AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="{}"
 VERSION Tor="0.2.2.35"
-OK''' % unexisting_file)
-        self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
+OK'''.format(unexisting_file))
+        self.assertEqual(
+            self.transport.value(),
+            b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n',
+        )
 
     def test_authenticate_safecookie_wrong_hash(self):
-        cookiedata = str(bytearray([0] * 32))
-        server_nonce = str(bytearray([0] * 32))
-        server_hash = str(bytearray([0] * 32))
+        cookiedata = bytes(bytearray([0] * 32))
+        server_nonce = bytes(bytearray([0] * 32))
+        server_hash = bytes(bytearray([0] * 32))
 
         # pretend we already did PROTOCOLINFO and read the cookie
         # file
@@ -419,11 +456,13 @@ OK''' % unexisting_file)
         self.protocol.client_nonce = server_nonce  # all 0's anyway
         try:
             self.protocol._safecookie_authchallenge(
-                '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
-                (base64.b16encode(server_hash), base64.b16encode(server_nonce))
+                '250 AUTHCHALLENGE SERVERHASH={} SERVERNONCE={}'.format(
+                    b2a_hex(server_hash).decode('ascii'),
+                    b2a_hex(server_nonce).decode('ascii'),
+                )
             )
             self.assertTrue(False)
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('hash not expected' in str(e))
 
     def confirm_version_events(self, arg):
@@ -437,26 +476,26 @@ OK''' % unexisting_file)
         d.addCallback(CallbackChecker(self.protocol))
         d.addCallback(self.confirm_version_events)
 
-        events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'
+        events = b'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'
         self.protocol._bootstrap()
 
         # answer all the requests generated by boostrapping etc.
-        self.send("250-signal/names=")
-        self.send("250 OK")
+        self.send(b"250-signal/names=")
+        self.send(b"250 OK")
 
-        self.send("250-version=foo")
-        self.send("250 OK")
+        self.send(b"250-version=foo")
+        self.send(b"250 OK")
 
-        self.send("250-events/names=" + events)
-        self.send("250 OK")
+        self.send(b"250-events/names=" + events)
+        self.send(b"250 OK")
 
-        self.send("250 OK")  # for USEFEATURE
+        self.send(b"250 OK")  # for USEFEATURE
 
         return d
 
     def test_bootstrap_tor_does_not_support_signal_names(self):
         self.protocol._bootstrap()
-        self.send('552 Unrecognized key "signal/names"')
+        self.send(b'552 Unrecognized key "signal/names"')
         valid_signals = ["RELOAD", "DUMP", "DEBUG", "NEWNYM", "CLEARDNSCACHE"]
         self.assertEqual(self.protocol.valid_signals, valid_signals)
 
@@ -467,12 +506,12 @@ OK''' % unexisting_file)
         """
         self.protocol._set_valid_events('CIRC')
         self.protocol.add_event_listener('CIRC', lambda _: None)
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         d = self.protocol.get_conf("SOCKSPORT ORPORT")
-        self.send("650 CIRC 1000 EXTENDED moria1,moria2")
-        self.send("250-SOCKSPORT=9050")
-        self.send("250 ORPORT=0")
+        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
+        self.send(b"250-SOCKSPORT=9050")
+        self.send(b"250 ORPORT=0")
         return d
 
     def test_async_multiline(self):
@@ -489,15 +528,15 @@ OK''' % unexisting_file)
                 "1000 EXTENDED moria1,moria2\nEXTRAMAGIC=99\nANONYMITY=high"
             )
         )
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         d = self.protocol.get_conf("SOCKSPORT ORPORT")
         d.addCallback(CallbackChecker({"ORPORT": "0", "SOCKSPORT": "9050"}))
-        self.send("650-CIRC 1000 EXTENDED moria1,moria2")
-        self.send("650-EXTRAMAGIC=99")
-        self.send("650 ANONYMITY=high")
-        self.send("250-SOCKSPORT=9050")
-        self.send("250 ORPORT=0")
+        self.send(b"650-CIRC 1000 EXTENDED moria1,moria2")
+        self.send(b"650-EXTRAMAGIC=99")
+        self.send(b"650 ANONYMITY=high")
+        self.send(b"250-SOCKSPORT=9050")
+        self.send(b"250 ORPORT=0")
         return d
 
     def test_multiline_plus(self):
@@ -506,12 +545,12 @@ OK''' % unexisting_file)
 
         d = self.protocol.get_info("FOO")
         d.addCallback(CallbackChecker({"FOO": "\na\nb\nc"}))
-        self.send("250+FOO=")
-        self.send("a")
-        self.send("b")
-        self.send("c")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+FOO=")
+        self.send(b"a")
+        self.send(b"b")
+        self.send(b"c")
+        self.send(b".")
+        self.send(b"250 OK")
         return d
 
     def test_multiline_plus_embedded_equals(self):
@@ -520,10 +559,10 @@ OK''' % unexisting_file)
 
         d = self.protocol.get_info("FOO")
         d.addCallback(CallbackChecker({"FOO": "\na="}))
-        self.send("250+FOO=")
-        self.send("a=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+FOO=")
+        self.send(b"a=")
+        self.send(b".")
+        self.send(b"250 OK")
         return d
 
     def incremental_check(self, expected, actual):
@@ -536,11 +575,11 @@ OK''' % unexisting_file)
             "FOO",
             functools.partial(self.incremental_check, "bar")
         )
-        self.send("250+FOO=")
-        self.send("bar")
-        self.send("bar")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+FOO=")
+        self.send(b"bar")
+        self.send(b"bar")
+        self.send(b".")
+        self.send(b"250 OK")
         return d
 
     def test_getinfo_incremental_continuation(self):
@@ -548,32 +587,32 @@ OK''' % unexisting_file)
             "FOO",
             functools.partial(self.incremental_check, "bar")
         )
-        self.send("250-FOO=")
-        self.send("250-bar")
-        self.send("250-bar")
-        self.send("250 OK")
+        self.send(b"250-FOO=")
+        self.send(b"250-bar")
+        self.send(b"250-bar")
+        self.send(b"250 OK")
         return d
 
     def test_getinfo_one_line(self):
         d = self.protocol.get_info(
             "foo",
-            functools.partial(self.incremental_check, "bar")
         )
-        self.send('250 foo=bar')
+        self.send(b'250 foo=bar')
+        d.addCallback(lambda _: functools.partial(self.incremental_check, "bar"))
         return d
 
     def test_getconf(self):
         d = self.protocol.get_conf("SOCKSPORT ORPORT")
         d.addCallback(CallbackChecker({'SocksPort': '9050', 'ORPort': '0'}))
-        self.send("250-SocksPort=9050")
-        self.send("250 ORPort=0")
+        self.send(b"250-SocksPort=9050")
+        self.send(b"250 ORPort=0")
         return d
 
     def test_getconf_raw(self):
         d = self.protocol.get_conf_raw("SOCKSPORT ORPORT")
         d.addCallback(CallbackChecker('SocksPort=9050\nORPort=0'))
-        self.send("250-SocksPort=9050")
-        self.send("250 ORPort=0")
+        self.send(b"250-SocksPort=9050")
+        self.send(b"250 ORPort=0")
         return d
 
     def response_ok(self, v):
@@ -583,31 +622,37 @@ OK''' % unexisting_file)
         d = self.protocol.set_conf("foo", "bar").addCallback(
             functools.partial(self.response_ok)
         )
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
-        self.assertEqual(self.transport.value(), "SETCONF foo=bar\r\n")
+        self.assertEqual(self.transport.value(), b"SETCONF foo=bar\r\n")
 
     def test_setconf_with_space(self):
         d = self.protocol.set_conf("foo", "a value with a space")
         d.addCallback(functools.partial(self.response_ok))
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
         self.assertEqual(
             self.transport.value(),
-            'SETCONF foo="a value with a space"\r\n'
+            b'SETCONF foo="a value with a space"\r\n'
         )
 
     def test_setconf_multi(self):
         d = self.protocol.set_conf("foo", "bar", "baz", 1)
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
-        self.assertEqual(self.transport.value(), "SETCONF foo=bar baz=1\r\n")
+        self.assertEqual(
+            self.transport.value(),
+            b"SETCONF foo=bar baz=1\r\n",
+        )
 
     def test_quit(self):
         d = self.protocol.quit()
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
-        self.assertEqual(self.transport.value(), "QUIT\r\n")
+        self.assertEqual(
+            self.transport.value(),
+            b"QUIT\r\n",
+        )
 
     def test_dot(self):
         # just checking we don't expode
@@ -618,7 +663,7 @@ OK''' % unexisting_file)
         self.assertTrue(exists('txtorcon-debug.log'))
 
     def error(self, failure):
-        print "ERROR", failure
+        print("ERROR", failure)
         self.assertTrue(False)
 
     def test_twocommands(self):
@@ -630,10 +675,10 @@ OK''' % unexisting_file)
         d2 = self.protocol.get_info_raw("BAR")
         d2.addCallback(CallbackChecker("bar")).addErrback(log.err)
 
-        self.send("250-a=one")
-        self.send("250-b=two")
-        self.send("250 OK")
-        self.send("250 bar")
+        self.send(b"250-a=one")
+        self.send(b"250-b=two")
+        self.send(b"250 OK")
+        self.send(b"250 bar")
 
         return d2
 
@@ -641,13 +686,16 @@ OK''' % unexisting_file)
         try:
             self.protocol.signal('FOO')
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue('Invalid signal' in str(e))
 
     def test_signal(self):
         self.protocol.valid_signals = ['NEWNYM']
         self.protocol.signal('NEWNYM')
-        self.assertEqual(self.transport.value(), 'SIGNAL NEWNYM\r\n')
+        self.assertEqual(
+            self.transport.value(),
+            b'SIGNAL NEWNYM\r\n',
+        )
 
     def test_650_after_authenticate(self):
         self.protocol._set_valid_events('CONF_CHANGED')
@@ -655,10 +703,10 @@ OK''' % unexisting_file)
             'CONF_CHANGED',
             CallbackChecker("Foo=bar")
         )
-        self.send("250 OK")
+        self.send(b"250 OK")
 
-        self.send("650-CONF_CHANGED")
-        self.send("650-Foo=bar")
+        self.send(b"650-CONF_CHANGED")
+        self.send(b"650-Foo=bar")
 
     def test_notify_after_getinfo(self):
         self.protocol._set_valid_events('CIRC')
@@ -666,32 +714,35 @@ OK''' % unexisting_file)
             'CIRC',
             CallbackChecker("1000 EXTENDED moria1,moria2")
         )
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         d = self.protocol.get_info("a")
         d.addCallback(CallbackChecker({'a': 'one'})).addErrback(self.fail)
-        self.send("250-a=one")
-        self.send("250 OK")
-        self.send("650 CIRC 1000 EXTENDED moria1,moria2")
+        self.send(b"250-a=one")
+        self.send(b"250 OK")
+        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
         return d
 
     def test_notify_error(self):
         self.protocol._set_valid_events('CIRC')
-        self.send("650 CIRC 1000 EXTENDED moria1,moria2")
+        self.send(b"650 CIRC 1000 EXTENDED moria1,moria2")
 
     def test_getinfo(self):
         d = self.protocol.get_info("version")
         d.addCallback(CallbackChecker({'version': '0.2.2.34'}))
         d.addErrback(self.fail)
 
-        self.send("250-version=0.2.2.34")
-        self.send("250 OK")
+        self.send(b"250-version=0.2.2.34")
+        self.send(b"250 OK")
 
-        self.assertEqual(self.transport.value(), "GETINFO version\r\n")
+        self.assertEqual(
+            self.transport.value(),
+            b"GETINFO version\r\n",
+        )
         return d
 
     def test_getinfo_for_descriptor(self):
-        descriptor_info = """250+desc/name/moria1=
+        descriptor_info = b"""250+desc/name/moria1=
 router moria1 128.31.0.34 9101 0 9131
 platform Tor 0.2.5.0-alpha-dev on Linux
 protocols Link 1 2 Circuit 1
@@ -726,15 +777,15 @@ iO3EUE0AEYah2W9gdz8t+i3Dtr0zgqLS841GC/TyDKCm+MKmN8d098qnwK0NGF9q
 .
 250 OK"""
         d = self.protocol.get_info("desc/name/moria1")
-        d.addCallback(CallbackChecker({'desc/name/moria1': '\n' + '\n'.join(descriptor_info.split('\n')[1:-2])}))
+        d.addCallback(CallbackChecker({'desc/name/moria1': '\n' + '\n'.join(descriptor_info.decode('ascii').split('\n')[1:-2])}))
         d.addErrback(self.fail)
 
-        for line in descriptor_info.split('\n'):
+        for line in descriptor_info.split(b'\n'):
             self.send(line)
         return d
 
     def test_getinfo_multiline(self):
-        descriptor_info = """250+desc/name/moria1=
+        descriptor_info = b"""250+desc/name/moria1=
 router moria1 128.31.0.34 9101 0 9131
 platform Tor 0.2.5.0-alpha-dev on Linux
 .
@@ -744,7 +795,7 @@ platform Tor 0.2.5.0-alpha-dev on Linux
         d.addCallback(CallbackChecker({'desc/name/moria1': gold}))
         d.addErrback(self.fail)
 
-        for line in descriptor_info.split('\n'):
+        for line in descriptor_info.split(b'\n'):
             self.send(line)
         return d
 
@@ -755,19 +806,21 @@ platform Tor 0.2.5.0-alpha-dev on Linux
         # is it dangerous/ill-advised to depend on internal state of
         # class under test?
         d = self.protocol.defer
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
         self.assertEqual(
-            self.transport.value().split('\r\n')[-2],
-            "SETEVENTS FOO"
+            self.transport.value().split(b'\r\n')[-2],
+            b"SETEVENTS FOO"
         )
         self.transport.clear()
 
         self.protocol.add_event_listener('BAR', lambda _: None)
         d = self.protocol.defer
-        self.send("250 OK")
-        self.assertTrue(self.transport.value() == "SETEVENTS FOO BAR\r\n" or
-                        self.transport.value() == "SETEVENTS BAR FOO\r\n")
+        self.send(b"250 OK")
+        self.assertTrue(
+            self.transport.value() == b"SETEVENTS FOO BAR\r\n" or
+            self.transport.value() == b"SETEVENTS BAR FOO\r\n"
+        )
         self._wait(d)
 
         try:
@@ -791,12 +844,46 @@ platform Tor 0.2.5.0-alpha-dev on Linux
         self.protocol.add_event_listener('STREAM', listener)
 
         d = self.protocol.defer
-        self.send("250 OK")
+        self.send(b"250 OK")
         self._wait(d)
-        self.send("650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
-        self.send("650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
+        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
+        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
         self.assertEqual(listener.stream_events, 2)
 
+    def test_eventlistener_error(self):
+        self.protocol._set_valid_events('STREAM')
+
+        class EventListener(object):
+            stream_events = 0
+            do_error = False
+
+            def __call__(self, data):
+                self.stream_events += 1
+                if self.do_error:
+                    raise Exception("the bad thing happened")
+
+        # we make sure the first listener has the errors to prove the
+        # second one still gets called.
+        listener0 = EventListener()
+        listener0.do_error = True
+        listener1 = EventListener()
+        self.protocol.add_event_listener('STREAM', listener0)
+        self.protocol.add_event_listener('STREAM', listener1)
+
+        d = self.protocol.defer
+        self.send(b"250 OK")
+        self._wait(d)
+        self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC")
+        self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC")
+        self.assertEqual(listener0.stream_events, 2)
+        self.assertEqual(listener1.stream_events, 2)
+
+        # should have logged the two errors
+        logged = self.flushLoggedErrors()
+        self.assertEqual(2, len(logged))
+        self.assertTrue("the bad thing happened" in str(logged[0]))
+        self.assertTrue("the bad thing happened" in str(logged[1]))
+
     def test_remove_eventlistener(self):
         self.protocol._set_valid_events('STREAM')
 
@@ -808,11 +895,11 @@ platform Tor 0.2.5.0-alpha-dev on Linux
 
         listener = EventListener()
         self.protocol.add_event_listener('STREAM', listener)
-        self.assertEqual(self.transport.value(), 'SETEVENTS STREAM\r\n')
-        self.protocol.lineReceived("250 OK")
+        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
+        self.protocol.lineReceived(b"250 OK")
         self.transport.clear()
         self.protocol.remove_event_listener('STREAM', listener)
-        self.assertEqual(self.transport.value(), 'SETEVENTS \r\n')
+        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')
 
     def test_remove_eventlistener_multiple(self):
         self.protocol._set_valid_events('STREAM')
@@ -826,41 +913,40 @@ platform Tor 0.2.5.0-alpha-dev on Linux
         listener0 = EventListener()
         listener1 = EventListener()
         self.protocol.add_event_listener('STREAM', listener0)
-        self.assertEqual(self.transport.value(), 'SETEVENTS STREAM\r\n')
-        self.protocol.lineReceived("250 OK")
+        self.assertEqual(self.transport.value(), b'SETEVENTS STREAM\r\n')
+        self.protocol.lineReceived(b"250 OK")
         self.transport.clear()
         # add another one, shouldn't issue a tor command
         self.protocol.add_event_listener('STREAM', listener1)
-        self.assertEqual(self.transport.value(), '')
+        self.assertEqual(self.transport.value(), b'')
 
         # remove one, should still not issue a tor command
         self.protocol.remove_event_listener('STREAM', listener0)
-        self.assertEqual(self.transport.value(), '')
+        self.assertEqual(self.transport.value(), b'')
 
         # remove the other one, NOW should issue a command
         self.protocol.remove_event_listener('STREAM', listener1)
-        self.assertEqual(self.transport.value(), 'SETEVENTS \r\n')
+        self.assertEqual(self.transport.value(), b'SETEVENTS \r\n')
 
         # try removing invalid event
         try:
             self.protocol.remove_event_listener('FOO', listener0)
             self.fail()
-        except Exception, e:
+        except Exception as e:
             self.assertTrue('FOO' in str(e))
 
-    def checkContinuation(self, v):
-        self.assertEqual(v, "key=\nvalue0\nvalue1")
-
-    def test_continuationLine(self):
+    def test_continuation_line(self):
         d = self.protocol.get_info_raw("key")
 
-        d.addCallback(self.checkContinuation)
+        def check_continuation(v):
+            self.assertEqual(v, "key=\nvalue0\nvalue1")
+        d.addCallback(check_continuation)
 
-        self.send("250+key=")
-        self.send("value0")
-        self.send("value1")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+key=")
+        self.send(b"value0")
+        self.send(b"value1")
+        self.send(b".")
+        self.send(b"250 OK")
 
         return d
 
@@ -879,19 +965,19 @@ s Exit Fast Guard HSDir Named Running Stable V2Dir Valid
 w Bandwidth=518000
 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888"""))
 
-        self.send("250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=")
-        self.send("r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80")
-        self.send("s Exit Fast Guard HSDir Named Running Stable V2Dir Valid")
-        self.send("w Bandwidth=518000")
-        self.send("p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=")
+        self.send(b"r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80")
+        self.send(b"s Exit Fast Guard HSDir Named Running Stable V2Dir Valid")
+        self.send(b"w Bandwidth=518000")
+        self.send(b"p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888")
+        self.send(b".")
+        self.send(b"250 OK")
 
         return d
 
     def test_plus_line_no_command(self):
-        self.protocol.lineReceived("650+NS\r\n")
-        self.protocol.lineReceived("r Gabor gFpAHsFOHGATy12ZUswRf0ZrqAU GG6GDp40cQfR3ODvkBT0r+Q09kw 2012-05-12 16:54:56 91.219.238.71 443 80\r\n")
+        self.protocol.lineReceived(b"650+NS\r\n")
+        self.protocol.lineReceived(b"r Gabor gFpAHsFOHGATy12ZUswRf0ZrqAU GG6GDp40cQfR3ODvkBT0r+Q09kw 2012-05-12 16:54:56 91.219.238.71 443 80\r\n")
 
     def test_minus_line_no_command(self):
         """
@@ -900,8 +986,8 @@ p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863
         """
         self.protocol._set_valid_events('NS')
         self.protocol.add_event_listener('NS', lambda _: None)
-        self.protocol.lineReceived("650-NS\r\n")
-        self.protocol.lineReceived("650 OK\r\n")
+        self.protocol.lineReceived(b"650-NS\r\n")
+        self.protocol.lineReceived(b"650 OK\r\n")
 
 
 class ParseTests(unittest.TestCase):
@@ -931,7 +1017,7 @@ class ParseTests(unittest.TestCase):
     def test_multientry_keywords_2(self):
         x = parse_keywords('foo=bar\nfoo=zarimba')
         self.assertEqual(len(x), 1)
-        self.assertTrue(isinstance(x['foo'], types.ListType))
+        self.assertTrue(isinstance(x['foo'], list))
         self.assertEqual(len(x['foo']), 2)
         self.assertEqual(x['foo'][0], 'bar')
         self.assertEqual(x['foo'][1], 'zarimba')
@@ -939,7 +1025,7 @@ class ParseTests(unittest.TestCase):
     def test_multientry_keywords_3(self):
         x = parse_keywords('foo=bar\nfoo=baz\nfoo=zarimba')
         self.assertEqual(len(x), 1)
-        self.assertTrue(isinstance(x['foo'], types.ListType))
+        self.assertTrue(isinstance(x['foo'], list))
         self.assertEqual(len(x['foo']), 3)
         self.assertEqual(x['foo'][0], 'bar')
         self.assertEqual(x['foo'][1], 'baz')
@@ -948,7 +1034,7 @@ class ParseTests(unittest.TestCase):
     def test_multientry_keywords_4(self):
         x = parse_keywords('foo=bar\nfoo=baz\nfoo=zarimba\nfoo=foo')
         self.assertEqual(len(x), 1)
-        self.assertTrue(isinstance(x['foo'], types.ListType))
+        self.assertTrue(isinstance(x['foo'], list))
         self.assertEqual(len(x['foo']), 4)
         self.assertEqual(x['foo'][0], 'bar')
         self.assertEqual(x['foo'][1], 'baz')
@@ -967,8 +1053,7 @@ w Bandwidth=1234
 OK
 ''')
         self.assertEqual(2, len(x))
-        keys = x.keys()
-        keys.sort()
+        keys = sorted(x.keys())
         self.assertEqual(keys, ['ns/name/bar', 'ns/name/foo'])
 
     def test_multiline_keywords(self):
diff --git a/test/test_torinfo.py b/test/test_torinfo.py
index 8060444..606b85e 100644
--- a/test/test_torinfo.py
+++ b/test/test_torinfo.py
@@ -1,4 +1,4 @@
-from zope.interface import implements
+from zope.interface import implementer
 from twisted.trial import unittest
 from twisted.test import proto_helpers
 from twisted.internet import defer
@@ -6,11 +6,8 @@ from twisted.internet import defer
 from txtorcon import ITorControlProtocol, TorInfo, TorControlProtocol
 
 
+ at implementer(ITorControlProtocol)
 class FakeControlProtocol:
-    """
-    """
-
-    implements(ITorControlProtocol)
 
     def __init__(self, answers):
         self.answers = answers
@@ -61,7 +58,7 @@ class ProtocolIntegrationTests(unittest.TestCase):
         self.transport = proto_helpers.StringTransport()
 
     def send(self, line):
-        self.protocol.dataReceived(line.strip() + "\r\n")
+        self.protocol.dataReceived(line.strip() + b"\r\n")
 
     @defer.inlineCallbacks
     def test_with_arg(self):
@@ -73,28 +70,28 @@ class ProtocolIntegrationTests(unittest.TestCase):
 
         # answer all the requests generated by TorControlProtocol
         # boostrapping etc.
-        self.send('250-AUTH METHODS=HASHEDPASSWORD')
-        self.send('250 OK')
+        self.send(b'250-AUTH METHODS=HASHEDPASSWORD')
+        self.send(b'250 OK')
 
         # response to AUTHENTICATE
-        self.send('250 OK')
+        self.send(b'250 OK')
 
         # now we're in _bootstrap() in TorControlProtocol()
-        self.send("250-signal/names=")
-        self.send("250 OK")
+        self.send(b"250-signal/names=")
+        self.send(b"250 OK")
 
-        self.send("250-version=foo")
-        self.send("250 OK")
+        self.send(b"250-version=foo")
+        self.send(b"250 OK")
 
-        self.send("250-events/names=")
-        self.send("250 OK")
+        self.send(b"250-events/names=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")  # for USEFEATURE
+        self.send(b"250 OK")  # for USEFEATURE
 
         # do the TorInfo magic
-        self.send('250-info/names=')
-        self.send('250-multi/path/arg/* a documentation string')
-        self.send('250 OK')
+        self.send(b'250-info/names=')
+        self.send(b'250-multi/path/arg/* a documentation string')
+        self.send(b'250 OK')
 
         # we had to save this up above due to the "interesting" way
         # TorInfo switches to become a possible-nice magic thingy
@@ -114,8 +111,8 @@ class ProtocolIntegrationTests(unittest.TestCase):
 
         d = info.multi.path.arg('quux')
         d.addCallback(CheckAnswer(self, 'foo'))
-        self.send("250-multi/path/arg/quux=foo")
-        self.send("250 OK")
+        self.send(b"250-multi/path/arg/quux=foo")
+        self.send(b"250 OK")
         yield d
 
 
diff --git a/test/test_torstate.py b/test/test_torstate.py
index 002e5d0..d0d5cbf 100644
--- a/test/test_torstate.py
+++ b/test/test_torstate.py
@@ -1,4 +1,6 @@
-from zope.interface import implements
+from __future__ import print_function
+
+from zope.interface import implementer, directlyProvides
 from zope.interface.verify import verifyClass
 from twisted.trial import unittest
 from twisted.test import proto_helpers
@@ -6,10 +8,12 @@ from twisted.python.failure import Failure
 from twisted.internet import task, defer
 from twisted.internet.interfaces import IStreamClientEndpoint, IReactorCore
 
-import os
 import tempfile
+import six
+
+from ipaddress import IPv4Address
 
-from mock import patch
+from mock import patch, Mock
 
 from txtorcon import TorControlProtocol
 from txtorcon import TorProtocolError
@@ -20,16 +24,20 @@ from txtorcon import build_tor_connection
 from txtorcon import build_local_tor_connection
 from txtorcon import build_timeout_circuit
 from txtorcon import CircuitBuildTimedOutError
-from txtorcon.interface import ITorControlProtocol
 from txtorcon.interface import IStreamAttacher
 from txtorcon.interface import ICircuitListener
 from txtorcon.interface import IStreamListener
 from txtorcon.interface import StreamListenerMixin
 from txtorcon.interface import CircuitListenerMixin
+from txtorcon.torstate import _extract_reason
+from txtorcon.circuit import _get_circuit_attacher
 
+if six.PY3:
+    from .py3_torstate import TorStatePy3Tests  # noqa
 
+
+ at implementer(ICircuitListener)
 class CircuitListener(object):
-    implements(ICircuitListener)
 
     def __init__(self, expected):
         "expect is a list of tuples: (event, {key:value, key1:value1, ..})"
@@ -73,8 +81,8 @@ class CircuitListener(object):
         self.checker('failed', circuit, **kw)
 
 
+ at implementer(IStreamListener)
 class StreamListener(object):
-    implements(IStreamListener)
 
     def __init__(self, expected):
         "expect is a list of tuples: (event, {key:value, key1:value1, ..})"
@@ -115,8 +123,8 @@ class StreamListener(object):
         self.checker('failed', stream, reason)
 
 
+ at implementer(IReactorCore)
 class FakeReactor:
-    implements(IReactorCore)
 
     def __init__(self, test):
         self.test = test
@@ -147,8 +155,8 @@ class FakeCircuit(Circuit):
         self.state = 'BOGUS'
 
 
+ at implementer(IStreamClientEndpoint)
 class FakeEndpoint:
-    implements(IStreamClientEndpoint)
 
     def get_info_raw(self, keys):
         ans = '\r\n'.join(map(lambda k: '%s=' % k, keys.split()))
@@ -170,8 +178,8 @@ class FakeEndpoint:
         return defer.succeed(self.proto)
 
 
+ at implementer(IStreamClientEndpoint)
 class FakeEndpointAnswers:
-    implements(IStreamClientEndpoint)
 
     def __init__(self, answers):
         self.answers = answers
@@ -187,7 +195,13 @@ class FakeEndpointAnswers:
         return ans[:-2]                 # don't want trailing \r\n
 
     def get_info_incremental(self, key, linecb):
-        linecb('%s=%s' % (key, self.answers.pop()))
+        data = self.answers.pop().split('\n')
+        if len(data) == 1:
+            linecb('{}={}'.format(key, data[0]))
+        else:
+            linecb('{}='.format(key))
+            for line in data:
+                linecb(line)
         return defer.succeed('')
 
     def connect(self, protocol_factory):
@@ -207,12 +221,18 @@ class BootstrapTests(unittest.TestCase):
     def confirm_proto(self, x):
         self.assertTrue(isinstance(x, TorControlProtocol))
         self.assertTrue(x.post_bootstrap.called)
+        return x
 
     def confirm_state(self, x):
-        self.assertTrue(isinstance(x, TorState))
+        self.assertIsInstance(x, TorState)
         self.assertTrue(x.post_bootstrap.called)
         return x
 
+    def confirm_consensus(self, x):
+        self.assertEqual(1, len(x.all_routers))
+        self.assertEqual('fake', list(x.routers.values())[0].name)
+        return x
+
     def test_build(self):
         p = FakeEndpoint()
         d = build_tor_connection(p, build_state=False)
@@ -252,6 +272,7 @@ class BootstrapTests(unittest.TestCase):
 
     def confirm_pid(self, state):
         self.assertEqual(state.tor_pid, 1234)
+        return state
 
     def confirm_no_pid(self, state):
         self.assertEqual(state.tor_pid, 0)
@@ -271,6 +292,29 @@ class BootstrapTests(unittest.TestCase):
         p.proto.post_bootstrap.callback(p.proto)
         return d
 
+    def test_build_with_answers_ns(self):
+        fake_consensus = '\n'.join([
+            'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80',
+            's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+            'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80',
+            's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof',
+            '',
+        ])
+        p = FakeEndpointAnswers([fake_consensus,     # ns/all
+                                 '',     # circuit-status
+                                 '',     # stream-status
+                                 '',     # address-mappings/all
+                                 '',     # entry-guards
+                                 '1234'  # PID
+                                 ])
+
+        d = build_tor_connection(p, build_state=True)
+        d.addCallback(self.confirm_state).addErrback(self.fail)
+        d.addCallback(self.confirm_pid).addErrback(self.fail)
+        d.addCallback(self.confirm_consensus).addErrback(self.fail)
+        p.proto.post_bootstrap.callback(p.proto)
+        return d
+
     def test_build_with_answers_no_pid(self):
         p = FakeEndpointAnswers(['',    # ns/all
                                  '',    # circuit-status
@@ -312,6 +356,13 @@ class BootstrapTests(unittest.TestCase):
         return d
 
 
+class UtilTests(unittest.TestCase):
+
+    def test_extract_reason_no_reason(self):
+        reason = _extract_reason(dict())
+        self.assertEqual("unknown", reason)
+
+
 class StateTests(unittest.TestCase):
 
     def setUp(self):
@@ -324,8 +375,8 @@ class StateTests(unittest.TestCase):
         self.protocol.makeConnection(self.transport)
 
     def test_close_stream_with_attacher(self):
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def __init__(self):
                 self.streams = []
@@ -340,7 +391,7 @@ class StateTests(unittest.TestCase):
 
     def test_attacher_error_handler(self):
         # make sure error-handling "does something" that isn't blowing up
-        with patch('sys.stdout') as fake_stdout:
+        with patch('sys.stdout'):
             TorState(self.protocol)._attacher_error(Failure(RuntimeError("quote")))
 
     def test_stream_update(self):
@@ -369,7 +420,7 @@ class StateTests(unittest.TestCase):
         self.assertEqual(len(self.state.streams), 2)
 
     def send(self, line):
-        self.protocol.dataReceived(line.strip() + "\r\n")
+        self.protocol.dataReceived(line.strip() + b"\r\n")
 
     @defer.inlineCallbacks
     def test_bootstrap_callback(self):
@@ -378,7 +429,7 @@ class StateTests(unittest.TestCase):
         exception from TorState.bootstrap and we'll just hang...
         '''
 
-        from test_torconfig import FakeControlProtocol
+        from .test_torconfig import FakeControlProtocol
         protocol = FakeControlProtocol(
             [
                 "ns/all=",  # ns/all
@@ -413,39 +464,39 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(' '.join(self.state.event_map.keys()))
         self.state._bootstrap()
 
-        self.send("250+ns/all=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250+circuit-status=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+circuit-status=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250-stream-status=")
-        self.send("250 OK")
+        self.send(b"250-stream-status=")
+        self.send(b"250 OK")
 
-        self.send("250+address-mappings/all=")
-        self.send('www.example.com 127.0.0.1 "2012-01-01 00:00:00"')
-        self.send('subdomain.example.com 10.0.0.0 "2012-01-01 00:01:02"')
-        self.send('.')
-        self.send('250 OK')
+        self.send(b"250+address-mappings/all=")
+        self.send(b'www.example.com 127.0.0.1 "2012-01-01 00:00:00"')
+        self.send(b'subdomain.example.com 10.0.0.0 "2012-01-01 00:01:02"')
+        self.send(b".")
+        self.send(b"250 OK")
 
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("250-entry-guards=")
-        self.send("250 OK")
+        self.send(b"250-entry-guards=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         self.assertEqual(len(self.state.addrmap.addr), 4)
         self.assertTrue('www.example.com' in self.state.addrmap.addr)
         self.assertTrue('subdomain.example.com' in self.state.addrmap.addr)
         self.assertTrue('10.0.0.0' in self.state.addrmap.addr)
         self.assertTrue('127.0.0.1' in self.state.addrmap.addr)
-        self.assertEqual('127.0.0.1', self.state.addrmap.find('www.example.com').ip)
+        self.assertEqual(IPv4Address(u'127.0.0.1'), self.state.addrmap.find('www.example.com').ip)
         self.assertEqual('www.example.com', self.state.addrmap.find('127.0.0.1').name)
-        self.assertEqual('10.0.0.0', self.state.addrmap.find('subdomain.example.com').ip)
+        self.assertEqual(IPv4Address(u'10.0.0.0'), self.state.addrmap.find('subdomain.example.com').ip)
         self.assertEqual('subdomain.example.com', self.state.addrmap.find('10.0.0.0').name)
 
         return d
@@ -464,55 +515,119 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(' '.join(self.state.event_map.keys()))
         self.state._bootstrap()
 
-        self.send("250+ns/all=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250-circuit-status=123 BUILT PURPOSE=GENERAL")
-        self.send("250 OK")
+        self.send(b"250-circuit-status=123 BUILT PURPOSE=GENERAL")
+        self.send(b"250 OK")
 
-        self.send("250-stream-status=")
-        self.send("250 OK")
+        self.send(b"250-stream-status=")
+        self.send(b"250 OK")
 
-        self.send("250+address-mappings/all=")
-        self.send('.')
-        self.send('250 OK')
+        self.send(b"250+address-mappings/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("250-entry-guards=")
-        self.send("250 OK")
+        self.send(b"250-entry-guards=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         self.assertTrue(self.state.find_circuit(123))
-        self.assertEquals(len(self.state.circuits), 1)
+        self.assertEqual(len(self.state.circuits), 1)
 
         return d
 
     def test_unset_attacher(self):
 
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def attach_stream(self, stream, circuits):
                 return None
 
         fr = FakeReactor(self)
-        self.state.set_attacher(MyAttacher(), fr)
-        self.send("250 OK")
+        attacher = MyAttacher()
+        self.state.set_attacher(attacher, fr)
+        self.send(b"250 OK")
         self.state.set_attacher(None, fr)
-        self.send("250 OK")
+        self.send(b"250 OK")
         self.assertEqual(
             self.transport.value(),
-            'SETCONF __LeaveStreamsUnattached=1\r\nSETCONF'
-            ' __LeaveStreamsUnattached=0\r\n'
+            b'SETCONF __LeaveStreamsUnattached=1\r\nSETCONF'
+            b' __LeaveStreamsUnattached=0\r\n'
+        )
+
+    def test_attacher_twice(self):
+        """
+        It should be an error to set an attacher twice
+        """
+        @implementer(IStreamAttacher)
+        class MyAttacher(object):
+            pass
+
+        attacher = MyAttacher()
+        self.state.set_attacher(attacher, FakeReactor(self))
+        # attach the *same* instance twice; not an error
+        self.state.set_attacher(attacher, FakeReactor(self))
+        with self.assertRaises(RuntimeError) as ctx:
+            self.state.set_attacher(MyAttacher(), FakeReactor(self))
+        self.assertTrue(
+            "already have an attacher" in str(ctx.exception)
+        )
+
+    @defer.inlineCallbacks
+    def _test_attacher_both_apis(self):
+        """
+        similar to above, but first set_attacher is implicit via
+        Circuit.stream_via
+        """
+        reactor = Mock()
+        directlyProvides(reactor, IReactorCore)
+
+        @implementer(IStreamAttacher)
+        class MyAttacher(object):
+            pass
+
+        circ = Circuit(self.state)
+        circ.state = 'BUILT'
+
+        # use the "preferred" API, which will set an attacher
+        factory = Mock()
+        proto = Mock()
+        proto.when_done = Mock(return_value=defer.succeed(None))
+        factory.connect = Mock(return_value=defer.succeed(proto))
+        ep = circ.stream_via(reactor, 'meejah.ca', 443, factory)
+        addr = Mock()
+        addr.host = '10.0.0.1'
+        addr.port = 1011
+        ep._target_endpoint._get_address = Mock(return_value=defer.succeed(addr))
+        print("EP", ep)
+        attacher = yield _get_circuit_attacher(reactor, self.state)
+        print("attacher", attacher)
+        d = ep.connect('foo')
+        print("doin' it")
+        stream = Mock()
+        import ipaddress
+        stream.source_addr = ipaddress.IPv4Address(u'10.0.0.1')
+        stream.source_port = 1011
+        attacher.attach_stream(stream, [])
+        yield d
+
+        # ...now use the low-level API (should be an error)
+        with self.assertRaises(RuntimeError) as ctx:
+            self.state.set_attacher(MyAttacher(), FakeReactor(self))
+        self.assertTrue(
+            "already have an attacher" in str(ctx.exception)
         )
 
     def test_attacher(self):
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def __init__(self):
                 self.streams = []
@@ -528,41 +643,41 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(events)
         self.state._add_events()
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
-        self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
+        self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
+        self.send(b"650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
         self.assertEqual(len(attacher.streams), 1)
         self.assertEqual(attacher.streams[0].id, 1)
         self.assertEqual(len(self.protocol.commands), 1)
-        self.assertEqual(self.protocol.commands[0][1], 'ATTACHSTREAM 1 0')
+        self.assertEqual(self.protocol.commands[0][1], b'ATTACHSTREAM 1 0')
 
         # we should totally ignore .exit URIs
         attacher.streams = []
-        self.send("650 STREAM 2 NEW 0 10.0.0.0.$E11D2B2269CC25E67CA6C9FB5843497539A74FD0.exit:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
+        self.send(b"650 STREAM 2 NEW 0 10.0.0.0.$E11D2B2269CC25E67CA6C9FB5843497539A74FD0.exit:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
         self.assertEqual(len(attacher.streams), 0)
         self.assertEqual(len(self.protocol.commands), 1)
 
         # we should NOT ignore .onion URIs
         attacher.streams = []
-        self.send("650 STREAM 3 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
+        self.send(b"650 STREAM 3 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
         self.assertEqual(len(attacher.streams), 1)
         self.assertEqual(len(self.protocol.commands), 2)
-        self.assertEqual(self.protocol.commands[1][1], 'ATTACHSTREAM 3 0')
+        self.assertEqual(self.protocol.commands[1][1], b'ATTACHSTREAM 3 0')
 
         # normal attach
         circ = FakeCircuit(1)
         circ.state = 'BUILT'
         self.state.circuits[1] = circ
         attacher.answer = circ
-        self.send("650 STREAM 4 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
+        self.send(b"650 STREAM 4 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME")
         self.assertEqual(len(attacher.streams), 2)
         self.assertEqual(len(self.protocol.commands), 3)
-        self.assertEqual(self.protocol.commands[2][1], 'ATTACHSTREAM 4 1')
+        self.assertEqual(self.protocol.commands[2][1], b'ATTACHSTREAM 4 1')
 
     def test_attacher_defer(self):
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def __init__(self, answer):
                 self.streams = []
@@ -583,19 +698,19 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(events)
         self.state._add_events()
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
-        self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
+        self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
+        self.send(b"650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
         self.assertEqual(len(attacher.streams), 1)
         self.assertEqual(attacher.streams[0].id, 1)
         self.assertEqual(len(self.protocol.commands), 1)
-        self.assertEqual(self.protocol.commands[0][1], 'ATTACHSTREAM 1 1')
+        self.assertEqual(self.protocol.commands[0][1], b'ATTACHSTREAM 1 1')
 
     @defer.inlineCallbacks
     def test_attacher_errors(self):
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def __init__(self, answer):
                 self.streams = []
@@ -613,7 +728,7 @@ class StateTests(unittest.TestCase):
         msg = ''
         try:
             yield self.state._maybe_attach(stream)
-        except Exception, e:
+        except Exception as e:
             msg = str(e)
         self.assertTrue('circuit unknown' in msg)
 
@@ -621,7 +736,7 @@ class StateTests(unittest.TestCase):
         msg = ''
         try:
             yield self.state._maybe_attach(stream)
-        except Exception, e:
+        except Exception as e:
             msg = str(e)
         self.assertTrue('only attach to BUILT' in msg)
 
@@ -629,13 +744,13 @@ class StateTests(unittest.TestCase):
         msg = ''
         try:
             yield self.state._maybe_attach(stream)
-        except Exception, e:
+        except Exception as e:
             msg = str(e)
         self.assertTrue('Circuit instance' in msg)
 
     def test_attacher_no_attach(self):
+        @implementer(IStreamAttacher)
         class MyAttacher(object):
-            implements(IStreamAttacher)
 
             def __init__(self):
                 self.streams = []
@@ -650,15 +765,14 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(events)
         self.state._add_events()
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
         self.transport.clear()
-        self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
-        self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
+        self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER")
+        self.send(b"650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE")
         self.assertEqual(len(attacher.streams), 1)
         self.assertEqual(attacher.streams[0].id, 1)
-        print self.transport.value()
-        self.assertEqual(self.transport.value(), '')
+        self.assertEqual(self.transport.value(), b'')
 
     def test_close_stream_with_id(self):
         stream = Stream(self.state)
@@ -666,7 +780,7 @@ class StateTests(unittest.TestCase):
 
         self.state.streams[1] = stream
         self.state.close_stream(stream)
-        self.assertEqual(self.transport.value(), 'CLOSESTREAM 1 1\r\n')
+        self.assertEqual(self.transport.value(), b'CLOSESTREAM 1 1\r\n')
 
     def test_close_stream_with_stream(self):
         stream = Stream(self.state)
@@ -674,7 +788,7 @@ class StateTests(unittest.TestCase):
 
         self.state.streams[1] = stream
         self.state.close_stream(stream.id)
-        self.assertEqual(self.transport.value(), 'CLOSESTREAM 1 1\r\n')
+        self.assertEqual(self.transport.value(), b'CLOSESTREAM 1 1\r\n')
 
     def test_close_stream_invalid_reason(self):
         stream = Stream(self.state)
@@ -693,7 +807,7 @@ class StateTests(unittest.TestCase):
 
         self.state.circuits[1] = circuit
         self.state.close_circuit(circuit.id)
-        self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1\r\n')
+        self.assertEqual(self.transport.value(), b'CLOSECIRCUIT 1\r\n')
 
     def test_close_circuit_with_circuit(self):
         circuit = Circuit(self.state)
@@ -701,7 +815,7 @@ class StateTests(unittest.TestCase):
 
         self.state.circuits[1] = circuit
         self.state.close_circuit(circuit)
-        self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1\r\n')
+        self.assertEqual(self.transport.value(), b'CLOSECIRCUIT 1\r\n')
 
     def test_close_circuit_with_flags(self):
         circuit = Circuit(self.state)
@@ -714,7 +828,7 @@ class StateTests(unittest.TestCase):
 
         self.state.circuits[1] = circuit
         self.state.close_circuit(circuit.id, IfUnused=True)
-        self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1 IfUnused\r\n')
+        self.assertEqual(self.transport.value(), b'CLOSECIRCUIT 1 IfUnused\r\n')
 
     def test_circuit_destroy(self):
         self.state._circuit_update('365 LAUNCHED PURPOSE=GENERAL')
@@ -735,7 +849,7 @@ class StateTests(unittest.TestCase):
         self.protocol._set_valid_events(events)
         self.state._add_events()
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
         # we use this router later on in an EXTEND
         self.state._update_network_status("""ns/all=
@@ -750,22 +864,23 @@ p reject 1-65535""")
                     ]
         listen = CircuitListener(expected)
         # first add a Circuit before we listen
-        self.protocol.dataReceived("650 CIRC 123 LAUNCHED PURPOSE=GENERAL\r\n")
+        self.protocol.dataReceived(b"650 CIRC 123 LAUNCHED PURPOSE=GENERAL\r\n")
         self.assertEqual(len(self.state.circuits), 1)
 
         # make sure we get added to existing circuits
         self.state.add_circuit_listener(listen)
-        self.assertTrue(listen in self.state.circuits.values()[0].listeners)
+        first_circuit = list(self.state.circuits.values())[0]
+        self.assertTrue(listen in first_circuit.listeners)
 
         # now add a Circuit after we started listening
-        self.protocol.dataReceived("650 CIRC 456 LAUNCHED PURPOSE=GENERAL\r\n")
+        self.protocol.dataReceived(b"650 CIRC 456 LAUNCHED PURPOSE=GENERAL\r\n")
         self.assertEqual(len(self.state.circuits), 2)
-        self.assertTrue(listen in self.state.circuits.values()[0].listeners)
-        self.assertTrue(listen in self.state.circuits.values()[1].listeners)
+        self.assertTrue(listen in list(self.state.circuits.values())[0].listeners)
+        self.assertTrue(listen in list(self.state.circuits.values())[1].listeners)
 
         # now update the first Circuit to ensure we're really, really
         # listening
-        self.protocol.dataReceived("650 CIRC 123 EXTENDED $D82183B1C09E1D7795FF2D7116BAB5106AA3E60E~PPrivCom012 PURPOSE=GENERAL\r\n")
+        self.protocol.dataReceived(b"650 CIRC 123 EXTENDED $D82183B1C09E1D7795FF2D7116BAB5106AA3E60E~PPrivCom012 PURPOSE=GENERAL\r\n")
         self.assertEqual(len(listen.expected), 0)
 
     def test_router_from_id_invalid_key(self):
@@ -819,8 +934,10 @@ p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863
 .''')
             self.fail()
 
-        except RuntimeError, e:
-            self.assertTrue('"s "' in str(e))
+        except RuntimeError as e:
+            # self.assertTrue('Illegal state' in str(e))
+            # flip back when we go back to Automat
+            self.assertTrue('Expected "s "' in str(e))
 
     def test_routers_no_policy(self):
         """
@@ -899,48 +1016,49 @@ p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863
         self.protocol._set_valid_events(' '.join(self.state.event_map.keys()))
         self.state._bootstrap()
 
-        self.send("250+ns/all=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250+circuit-status=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+circuit-status=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250-stream-status=")
-        self.send("250 OK")
+        self.send(b"250-stream-status=")
+        self.send(b"250 OK")
 
-        self.send("250-address-mappings/all=")
-        self.send('250 OK')
+        self.send(b"250-address-mappings/all=")
+        self.send(b'250 OK')
 
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("250-entry-guards=")
-        self.send("250 OK")
+        self.send(b"250-entry-guards=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         # state is now bootstrapped, we can send our NEWCONSENSUS update
 
-        self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS
+        self.protocol.dataReceived(b'\r\n'.join(b'''650+NEWCONSENSUS
 r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0
 s Fast Guard Running Stable Valid
 w Bandwidth=166
 p reject 1-65535
 .
 650 OK
-'''.split('\n')))
+'''.split(b'\n')))
 
-        self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS
+        self.protocol.dataReceived(b'\r\n'.join(b'''650+NEWCONSENSUS
 r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0
 s Fast Guard Running Stable Valid
 w Bandwidth=166
 p reject 1-65535
 .
 650 OK
-'''.split('\n')))
+'''.split(b'\n')))
 
+        self.assertEqual(1, len(self.state.all_routers))
         self.assertTrue('Unnamed' in self.state.routers)
         self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers)
 
@@ -956,37 +1074,37 @@ p reject 1-65535
         self.protocol._set_valid_events(' '.join(self.state.event_map.keys()))
         self.state._bootstrap()
 
-        self.send("250+ns/all=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250+circuit-status=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+circuit-status=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250-stream-status=")
-        self.send("250 OK")
+        self.send(b"250-stream-status=")
+        self.send(b"250 OK")
 
-        self.send("250-address-mappings/all=")
-        self.send('250 OK')
+        self.send(b"250-address-mappings/all=")
+        self.send(b"250 OK")
 
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("250-entry-guards=")
-        self.send("250 OK")
+        self.send(b"250-entry-guards=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         # state is now bootstrapped, we can send our NEWCONSENSUS update
 
-        self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS
+        self.protocol.dataReceived(b'\r\n'.join(b'''650+NEWCONSENSUS
 r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0
 s Fast Guard Running Stable Valid
 w Bandwidth=166
 .
 650 OK
-'''.split('\n')))
+'''.split(b'\n')))
 
         self.assertTrue('Unnamed' in self.state.routers)
         self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers)
@@ -1003,36 +1121,36 @@ w Bandwidth=166
         self.protocol._set_valid_events(' '.join(self.state.event_map.keys()))
         self.state._bootstrap()
 
-        self.send("250+ns/all=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+ns/all=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250+circuit-status=")
-        self.send(".")
-        self.send("250 OK")
+        self.send(b"250+circuit-status=")
+        self.send(b".")
+        self.send(b"250 OK")
 
-        self.send("250-stream-status=")
-        self.send("250 OK")
+        self.send(b"250-stream-status=")
+        self.send(b"250 OK")
 
-        self.send("250-address-mappings/all=")
-        self.send('250 OK')
+        self.send(b"250-address-mappings/all=")
+        self.send(b"250 OK")
 
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
-        self.send("250-entry-guards=")
-        self.send("250 OK")
+        self.send(b"250-entry-guards=")
+        self.send(b"250 OK")
 
-        self.send("250 OK")
+        self.send(b"250 OK")
 
         # state is now bootstrapped, we can send our NEWCONSENSUS update
 
-        self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS
+        self.protocol.dataReceived(b'\r\n'.join(b'''650+NEWCONSENSUS
 r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0
 s Fast Guard Running Stable Valid
 .
 650 OK
-'''.split('\n')))
+'''.split(b'\n')))
 
         self.assertTrue('Unnamed' in self.state.routers)
         self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers)
@@ -1064,19 +1182,20 @@ s Fast Guard Running Stable Valid
         self.protocol._set_valid_events('CIRC STREAM ORCONN BW DEBUG INFO NOTICE WARN ERR NEWDESC ADDRMAP AUTHDIR_NEWDESCS DESCCHANGED NS STATUS_GENERAL STATUS_CLIENT STATUS_SERVER GUARD STREAM_BW CLIENTS_SEEN NEWCONSENSUS BUILDTIMEOUT_SET')
         self.state._add_events()
         for ignored in self.state.event_map.items():
-            self.send("250 OK")
+            self.send(b"250 OK")
 
         expected = [('new', {}),
                     ]
         listen = StreamListener(expected)
-        self.send("650 STREAM 77 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER")
+        self.send(b"650 STREAM 77 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER")
         self.state.add_stream_listener(listen)
 
-        self.assertTrue(listen in self.state.streams.values()[0].listeners)
+        self.assertEqual(1, len(self.state.streams.values()))
+        self.assertTrue(listen in list(self.state.streams.values())[0].listeners)
         self.assertEqual(len(self.state.streams), 1)
         self.assertEqual(len(listen.expected), 1)
 
-        self.send("650 STREAM 78 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER")
+        self.send(b"650 STREAM 78 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER")
         self.assertEqual(len(self.state.streams), 2)
         self.assertEqual(len(listen.expected), 0)
 
@@ -1094,18 +1213,18 @@ s Fast Guard Running Stable Valid
         path[0].flags = ['guard']
 
         self.state.build_circuit(path, using_guards=True)
-        self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
+        self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
         # should have gotten a warning about this not being an entry
         # guard
         self.assertEqual(len(self.flushWarnings()), 1)
 
     def test_build_circuit_no_routers(self):
         self.state.build_circuit()
-        self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0\r\n')
+        self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0\r\n')
 
     def test_build_circuit_unfound_router(self):
-        self.state.build_circuit(routers=['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'], using_guards=False)
-        self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n')
+        self.state.build_circuit(routers=[b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'], using_guards=False)
+        self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n')
 
     def circuit_callback(self, circ):
         self.assertTrue(isinstance(circ, Circuit))
@@ -1129,8 +1248,8 @@ s Fast Guard Running Stable Valid
 
         d = self.state.build_circuit(path, using_guards=True)
         d.addCallback(self.circuit_callback)
-        self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
-        self.send('250 EXTENDED 1234')
+        self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
+        self.send(b"250 EXTENDED 1234")
         # should have gotten a warning about this not being an entry
         # guard
         self.assertEqual(len(self.flushWarnings()), 1)
@@ -1144,7 +1263,7 @@ s Fast Guard Running Stable Valid
         try:
             self.state._find_circuit_after_extend("FOO 1234")
             self.assertTrue(False)
-        except RuntimeError, e:
+        except RuntimeError as e:
             self.assertTrue('Expected EXTENDED' in str(e))
 
     def test_listener_mixins(self):
@@ -1169,7 +1288,7 @@ s Fast Guard Running Stable Valid
         timeout = 10
         clock = task.Clock()
 
-        d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=True)
+        d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False)
         clock.advance(10)
 
         def check_for_timeout_error(f):
@@ -1177,6 +1296,42 @@ s Fast Guard Running Stable Valid
         d.addErrback(check_for_timeout_error)
         return d
 
+    def test_build_circuit_timeout_after_progress(self):
+        """
+        Similar to above but we timeout after Tor has ack'd our
+        circuit-creation attempt, but before reaching BUILT.
+        """
+        class FakeRouter:
+            def __init__(self, i):
+                self.id_hex = i
+                self.flags = []
+
+        class FakeCircuit(Circuit):
+            def close(self):
+                return defer.succeed(None)
+
+        path = []
+        for x in range(3):
+            path.append(FakeRouter("$%040d" % x))
+
+        def fake_queue(cmd):
+            self.assertTrue(cmd.startswith('EXTENDCIRCUIT 0'))
+            return defer.succeed("EXTENDED 1234")
+
+        queue_command = patch.object(self.protocol, 'queue_command', fake_queue)
+        circuit_factory = patch.object(self.state, 'circuit_factory', FakeCircuit)
+        with queue_command, circuit_factory:
+            timeout = 10
+            clock = task.Clock()
+
+            d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False)
+            clock.advance(timeout + 1)
+
+            def check_for_timeout_error(f):
+                self.assertTrue(isinstance(f.type(), CircuitBuildTimedOutError))
+            d.addErrback(check_for_timeout_error)
+        return d
+
     def test_build_circuit_not_timedout(self):
         class FakeRouter:
             def __init__(self, i):
@@ -1193,9 +1348,9 @@ s Fast Guard Running Stable Valid
         d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=True)
         d.addCallback(self.circuit_callback)
 
-        self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
-        self.send('250 EXTENDED 1234')
-        # we can't just .send('650 CIRC 1234 BUILT') this because we
+        self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
+        self.send(b"250 EXTENDED 1234")
+        # we can't just .send(b'650 CIRC 1234 BUILT') this because we
         # didn't fully hook up the protocol to the state, e.g. via
         # post_bootstrap etc.
         self.state.circuits[1234].update(['1234', 'BUILT'])
diff --git a/test/test_util.py b/test/test_util.py
index 4d74b15..d1f9ecb 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -1,9 +1,15 @@
+import os
+import sys
+import tempfile
 from mock import patch
+from unittest import skipIf
+import ipaddress
+
 from twisted.trial import unittest
 from twisted.internet import defer
 from twisted.internet.endpoints import TCP4ServerEndpoint
 from twisted.internet.interfaces import IProtocolFactory
-from zope.interface import implements
+from zope.interface import implementer
 
 from txtorcon.util import process_from_address
 from txtorcon.util import delete_file_or_tree
@@ -12,17 +18,19 @@ from txtorcon.util import ip_from_int
 from txtorcon.util import find_tor_binary
 from txtorcon.util import maybe_ip_addr
 from txtorcon.util import unescape_quoted_string
-
-import os
-import tempfile
+from txtorcon.util import available_tcp_port
+from txtorcon.util import version_at_least
+from txtorcon.util import default_control_port
+from txtorcon.util import _Listener, _ListenerCollection
+from txtorcon.util import create_tbb_web_headers
 
 
 class FakeState:
     tor_pid = 0
 
 
+ at implementer(IProtocolFactory)
 class FakeProtocolFactory:
-    implements(IProtocolFactory)
 
     def doStart(self):
         "IProtocolFactory API"
@@ -57,14 +65,15 @@ class TestGeoIpDatabaseLoading(unittest.TestCase):
         ret_val = util.create_geoip(f)
         delete_file_or_tree(f)
         util.GeoIP = _GeoIP
-        self.assertEquals(ret_val, None)
+        self.assertEqual(ret_val, None)
 
+    @skipIf('pypy' in sys.version.lower(), "No GeoIP in PyPy")
     def test_return_geoip_object(self):
         from txtorcon import util
         (fd, f) = tempfile.mkstemp()
         ret_val = util.create_geoip(f)
         delete_file_or_tree(f)
-        self.assertEquals(type(ret_val).__name__, 'GeoIP')
+        self.assertEqual(type(ret_val).__name__, 'GeoIP')
 
 
 class TestFindKeywords(unittest.TestCase):
@@ -102,8 +111,8 @@ class TestNetLocation(unittest.TestCase):
             util.city = FakeGeoIP(version=2)
             nl = util.NetLocation('127.0.0.1')
             self.assertTrue(nl.city)
-            self.assertEquals(nl.city[0], 'City')
-            self.assertEquals(nl.city[1], 'Region')
+            self.assertEqual(nl.city[0], 'City')
+            self.assertEqual(nl.city[1], 'Region')
         finally:
             util.ity = orig
 
@@ -114,8 +123,8 @@ class TestNetLocation(unittest.TestCase):
             util.city = FakeGeoIP(version=3)
             nl = util.NetLocation('127.0.0.1')
             self.assertTrue(nl.city)
-            self.assertEquals(nl.city[0], 'City')
-            self.assertEquals(nl.city[1], 'Region')
+            self.assertEqual(nl.city[0], 'City')
+            self.assertEqual(nl.city[1], 'Region')
         finally:
             util.ity = orig
 
@@ -208,11 +217,12 @@ class TestProcessFromUtil(unittest.TestCase):
         # tests, but I was using "nc" before, and I think this is
         # preferable.
         from twisted.internet import reactor
-        ep = TCP4ServerEndpoint(reactor, 9887)
+        port = yield available_tcp_port(reactor)
+        ep = TCP4ServerEndpoint(reactor, port)
         listener = yield ep.listen(FakeProtocolFactory())
 
         try:
-            pid = process_from_address('0.0.0.0', 9887, self.fakestate)
+            pid = process_from_address('0.0.0.0', port, self.fakestate)
         finally:
             listener.stopListening()
 
@@ -223,7 +233,7 @@ class TestDelete(unittest.TestCase):
 
     def test_delete_file(self):
         (fd, f) = tempfile.mkstemp()
-        os.write(fd, 'some\ndata\n')
+        os.write(fd, b'some\ndata\n')
         os.close(fd)
         self.assertTrue(os.path.exists(f))
         delete_file_or_tree(f)
@@ -231,8 +241,8 @@ class TestDelete(unittest.TestCase):
 
     def test_delete_tree(self):
         d = tempfile.mkdtemp()
-        f = open(os.path.join(d, 'foo'), 'w')
-        f.write('foo\n')
+        f = open(os.path.join(d, 'foo'), 'wb')
+        f.write(b'foo\n')
         f.close()
 
         self.assertTrue(os.path.exists(d))
@@ -268,16 +278,17 @@ class TestFindTor(unittest.TestCase):
 
 class TestIpAddr(unittest.TestCase):
 
-    @patch('txtorcon.util.ipaddress')
-    def test_create_ipaddr(self, ipaddr):
+    def test_create_ipaddr(self):
         ip = maybe_ip_addr('1.2.3.4')
+        self.assertTrue(isinstance(ip, ipaddress.IPv4Address))
 
     @patch('txtorcon.util.ipaddress')
-    def test_create_ipaddr(self, ipaddr):
+    def test_create_ipaddr_fail(self, ipaddr):
         def foo(blam):
             raise ValueError('testing')
         ipaddr.ip_address.side_effect = foo
         ip = maybe_ip_addr('1.2.3.4')
+        self.assertTrue(isinstance(ip, type('1.2.3.4')))
 
 
 class TestUnescapeQuotedString(unittest.TestCase):
@@ -315,17 +326,14 @@ class TestUnescapeQuotedString(unittest.TestCase):
         Octal numbers can be escaped by a backslash:
         \0 is interpreted as a byte with the value 0
         '''
-        for number in range(1000):
-            escaped = '\\{}'.format(number)
+        for number in range(0x7f):
+            escaped = '\\%o' % number
             result = unescape_quoted_string('"{}"'.format(escaped))
-
-            expected = escaped.decode('string-escape')
-            if expected[0] == '\\' and len(expected) > 1:
-                expected = expected[1:]
+            expected = chr(number)
 
             msg = "Number not decoded correctly: {escaped} -> {result} instead of {expected}"
             msg = msg.format(escaped=escaped, result=repr(result), expected=repr(expected))
-            self.assertEquals(result, expected, msg=msg)
+            self.assertEqual(result, expected, msg=msg)
 
     def test_invalid_string_unescaping(self):
         invalid_escaped = [
@@ -337,3 +345,118 @@ class TestUnescapeQuotedString(unittest.TestCase):
 
         for invalid_string in invalid_escaped:
             self.assertRaises(ValueError, unescape_quoted_string, invalid_string)
+
+
+class TestVersions(unittest.TestCase):
+    def test_version_1(self):
+        self.assertTrue(
+            version_at_least("1.2.3.4", 1, 2, 3, 4)
+        )
+
+    def test_version_2(self):
+        self.assertFalse(
+            version_at_least("1.2.3.4", 1, 2, 3, 5)
+        )
+
+    def test_version_3(self):
+        self.assertTrue(
+            version_at_least("1.2.3.4", 1, 2, 3, 2)
+        )
+
+    def test_version_4(self):
+        self.assertTrue(
+            version_at_least("2.1.1.1", 2, 0, 0, 0)
+        )
+
+
+class TestHeaders(unittest.TestCase):
+
+    def test_simple(self):
+        create_tbb_web_headers()
+
+
+class TestDefaultPort(unittest.TestCase):
+
+    def test_no_env_var(self):
+        p = default_control_port()
+        self.assertEqual(p, 9151)
+
+    @patch('txtorcon.util.os')
+    def test_env_var(self, fake_os):
+        fake_os.environ = dict(TX_CONTROL_PORT=1234)
+        p = default_control_port()
+        self.assertEqual(p, 1234)
+
+
+class TestListeners(unittest.TestCase):
+
+    def test_add_remove(self):
+        listener = _Listener()
+        calls = []
+
+        def cb(*args, **kw):
+            calls.append((args, kw))
+
+        listener.add(cb)
+        listener.notify('foo', 'bar', quux='zing')
+        listener.remove(cb)
+        listener.notify('foo', 'bar', quux='zing')
+
+        self.assertEqual(1, len(calls))
+        self.assertEqual(('foo', 'bar'), calls[0][0])
+        self.assertEqual(dict(quux='zing'), calls[0][1])
+
+    def test_notify_with_exception(self):
+        listener = _Listener()
+        calls = []
+
+        def cb(*args, **kw):
+            calls.append((args, kw))
+
+        def bad_cb(*args, **kw):
+            raise Exception("sadness")
+
+        listener.add(bad_cb)
+        listener.add(cb)
+        listener.notify('foo', 'bar', quux='zing')
+
+        self.assertEqual(1, len(calls))
+        self.assertEqual(('foo', 'bar'), calls[0][0])
+        self.assertEqual(dict(quux='zing'), calls[0][1])
+
+    def test_collection_invalid_event(self):
+        collection = _ListenerCollection(['event0', 'event1'])
+
+        with self.assertRaises(Exception) as ctx:
+            collection('bad', lambda: None)
+        self.assertTrue('Invalid event' in str(ctx.exception))
+
+    def test_collection_invalid_event_notify(self):
+        collection = _ListenerCollection(['event0', 'event1'])
+
+        with self.assertRaises(Exception) as ctx:
+            collection.notify('bad', lambda: None)
+        self.assertTrue('Invalid event' in str(ctx.exception))
+
+    def test_collection_invalid_event_remove(self):
+        collection = _ListenerCollection(['event0', 'event1'])
+
+        with self.assertRaises(Exception) as ctx:
+            collection.remove('bad', lambda: None)
+        self.assertTrue('Invalid event' in str(ctx.exception))
+
+    def test_collection(self):
+        collection = _ListenerCollection(['event0', 'event1'])
+        calls = []
+
+        def cb(*args, **kw):
+            calls.append((args, kw))
+
+        collection('event0', cb)
+        collection.notify('event0', 'foo', 'bar', quux='zing')
+        collection.remove('event0', cb)
+        collection.notify('event0', 'foo', 'bar', quux='zing')
+
+        self.assertEqual(1, len(calls))
+        self.assertEqual(calls[0][0], ('foo', 'bar'))
+        self.assertEqual(calls[0][1], dict(quux='zing'))
diff --git a/test/test_util_imports.py b/test/test_util_imports.py
index ab64c78..b2285c9 100644
--- a/test/test_util_imports.py
+++ b/test/test_util_imports.py
@@ -1,9 +1,9 @@
 from twisted.trial import unittest
 
 import sys
-import types
 import functools
-from unittest import skipIf
+from unittest import skipUnless
+import six
 
 
 def fake_import(orig, name, *args, **kw):
@@ -14,7 +14,7 @@ def fake_import(orig, name, *args, **kw):
 
 class TestImports(unittest.TestCase):
 
-    @skipIf('pypy' in sys.version.lower(), "Doesn't work in PYPY")
+    @skipUnless(six.PY2 and 'pypy' not in sys.version.lower(), "Doesn't work in PYPY, Py3")
     def test_no_GeoIP(self):
         """
         Make sure we don't explode if there's no GeoIP module
@@ -39,8 +39,10 @@ class TestImports(unittest.TestCase):
             # now ensure we set up all the databases as "None" when we
             # import w/o the GeoIP thing available.
             import txtorcon.util
-            ipa = txtorcon.util.maybe_ip_addr('127.0.0.1')
-            self.assertTrue(isinstance(ipa, types.StringType))
+            loc = txtorcon.util.NetLocation('127.0.0.1')
+            self.assertEqual(loc.city, None)
+            self.assertEqual(loc.asn, None)
+            self.assertEqual(loc.countrycode, '')
 
         finally:
             __import__ = orig
diff --git a/test/test_web.py b/test/test_web.py
new file mode 100644
index 0000000..fe891a0
--- /dev/null
+++ b/test/test_web.py
@@ -0,0 +1,108 @@
+
+from mock import Mock
+
+from twisted.trial import unittest
+from twisted.internet import defer
+
+try:
+    from txtorcon.web import agent_for_socks_port
+    from txtorcon.web import tor_agent
+    _HAVE_WEB = True
+except ImportError:
+    _HAVE_WEB = False
+from txtorcon.socks import TorSocksEndpoint
+from txtorcon.circuit import TorCircuitEndpoint
+
+
+class WebAgentTests(unittest.TestCase):
+    skip = not _HAVE_WEB
+
+    def test_socks_agent_tcp_port(self):
+        reactor = Mock()
+        config = Mock()
+        config.SocksPort = ['1234']
+        agent_for_socks_port(reactor, config, '1234')
+
+    @defer.inlineCallbacks
+    def test_socks_agent_error_saving(self):
+        reactor = Mock()
+        config = Mock()
+        config.SocksPort = []
+
+        def boom(*args, **kw):
+            raise RuntimeError("sad times at ridgemont high")
+        config.save = boom
+        try:
+            yield agent_for_socks_port(reactor, config, '1234')
+            self.fail("Should get an error")
+        except RuntimeError as e:
+            self.assertTrue("sad times at ridgemont high" in str(e))
+
+    def test_socks_agent_unix(self):
+        reactor = Mock()
+        config = Mock()
+        config.SocksPort = []
+        agent_for_socks_port(reactor, config, 'unix:/foo')
+
+    @defer.inlineCallbacks
+    def test_socks_agent_tcp_host_port(self):
+        reactor = Mock()
+        config = Mock()
+        config.SocksPort = []
+        proto = Mock()
+        gold = object()
+        proto.request = Mock(return_value=defer.succeed(gold))
+
+        def getConnection(key, endpoint):
+            self.assertTrue(isinstance(endpoint, TorSocksEndpoint))
+            self.assertTrue(endpoint._tls)
+            self.assertEqual(endpoint._host, u'meejah.ca')
+            self.assertEqual(endpoint._port, 443)
+            return defer.succeed(proto)
+        pool = Mock()
+        pool.getConnection = getConnection
+
+        # do the test
+        agent = yield agent_for_socks_port(reactor, config, '127.0.0.50:1234', pool=pool)
+
+        # apart from the getConnection asserts...
+        res = yield agent.request(b'GET', b'https://meejah.ca')
+        self.assertIs(res, gold)
+
+    @defer.inlineCallbacks
+    def test_agent(self):
+        reactor = Mock()
+        socks_ep = Mock()
+        yield tor_agent(reactor, socks_ep)
+
+    @defer.inlineCallbacks
+    def test_agent_no_socks(self):
+        reactor = Mock()
+        with self.assertRaises(Exception) as ctx:
+            yield tor_agent(reactor, None)
+        self.assertTrue('Must provide socks_endpoint' in str(ctx.exception))
+
+    @defer.inlineCallbacks
+    def test_agent_with_circuit(self):
+        reactor = Mock()
+        circuit = Mock()
+        socks_ep = Mock()
+        proto = Mock()
+        gold = object()
+        proto.request = Mock(return_value=defer.succeed(gold))
+
+        def getConnection(key, endpoint):
+            self.assertTrue(isinstance(endpoint, TorCircuitEndpoint))
+            target = endpoint._target_endpoint
+            self.assertTrue(target._tls)
+            self.assertEqual(target._host, u'meejah.ca')
+            self.assertEqual(target._port, 443)
+            return defer.succeed(proto)
+        pool = Mock()
+        pool.getConnection = getConnection
+
+        agent = yield tor_agent(reactor, socks_ep, circuit=circuit, pool=pool)
+
+        # apart from the getConnection asserts...
+        res = yield agent.request(b'GET', b'https://meejah.ca')
+        self.assertIs(res, gold)
diff --git a/test/verify-release.py b/test/verify-release.py
new file mode 100644
index 0000000..c039ec8
--- /dev/null
+++ b/test/verify-release.py
@@ -0,0 +1,58 @@
+# this does the "download and verify release from web + hidden-service
+# machine" for txtorcon release-checklist.
+
+import sys
+import hashlib
+from os.path import join, split, exists
+
+import txtorcon
+
+from twisted.internet import defer, task
+from twisted.web.client import readBody
+from twisted.python.failure import Failure
+
+
+ at task.react
+ at defer.inlineCallbacks
+def main(reactor):
+    if len(sys.argv) != 2:
+        print('usage: {} <version>'.format(__file__))
+        raise SystemExit(1)
+    version = sys.argv[1]
+    announce = join(split(__file__)[0], '..', 'release-announce-{}'.format(version))
+    if not exists(announce):
+        print('no announcement file: {}'.format(announce))
+        raise SystemExit(2)
+
+    sums = None
+    with open(announce, 'r') as f:
+        for line in f.readlines():
+            if line.strip() == 'cat <<EOF | sha256sum --check':
+                sums = []
+            elif line.strip() == 'EOF':
+                break
+            elif sums is not None:
+                checksum, fname = line.split()
+                sums.append((checksum, split(fname)[1]))
+
+    tor = yield txtorcon.connect(reactor)
+    agent = tor.web_agent()
+
+    for sha256, fname in sums:
+        print("Verifying '{}'".format(fname))
+        uri = b'http://timaq4ygg2iegci7.onion/' + fname.encode('ascii')
+        try:
+            resp = yield agent.request(b'GET', uri)
+        except Exception:
+            print(Failure())
+            raise
+        data = yield readBody(resp)
+        print('data: {} {}'.format(type(data), len(data)))
+        hasher = hashlib.new('sha256')
+        hasher.update(data)
+        alleged_sum = hasher.hexdigest()
+        if alleged_sum != sha256:
+            print("Checksum mismatch:")
+            print("wanted: {}".format(sha256))
+            print("   got: {}".format(alleged_sum))
+            raise SystemExit(45)
diff --git a/txtorcon.egg-info/PKG-INFO b/txtorcon.egg-info/PKG-INFO
index f537578..2a92510 100644
--- a/txtorcon.egg-info/PKG-INFO
+++ b/txtorcon.egg-info/PKG-INFO
@@ -1,18 +1,28 @@
 Metadata-Version: 1.1
 Name: txtorcon
-Version: 0.17.0
+Version: 0.19.3
 Summary: 
     Twisted-based Tor controller client, with state-tracking and
     configuration abstractions.
+    https://txtorcon.readthedocs.org
+    https://github.com/meejah/txtorcon
 
 Home-page: https://github.com/meejah/txtorcon
 Author: meejah
 Author-email: meejah at meejah.ca
 License: MIT
-Description: README
-        ======
+Description: 
+        
+        
+        
+        
+        .. _NOTE: see docs/index.rst for the starting-point
+        .. _ALSO: https://txtorcon.readthedocs.org for rendered docs
+        
+        
+        
+        
         
-        Documentation at https://txtorcon.readthedocs.org
         
         .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master
             :target: https://www.travis-ci.org/meejah/txtorcon
@@ -26,6 +36,14 @@ Description: README
             :target: http://codecov.io/github/meejah/txtorcon?branch=master
             :alt: codecov
         
+        .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
+            :target: https://txtorcon.readthedocs.io/en/stable
+            :alt: ReadTheDocs
+        
+        .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest
+            :target: https://txtorcon.readthedocs.io/en/latest
+            :alt: ReadTheDocs
+        
         .. image:: http://api.flattr.com/button/flattr-badge-large.png
             :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
             :alt: flattr
@@ -34,213 +52,118 @@ Description: README
             :target: https://landscape.io/github/meejah/txtorcon/master
             :alt: Code Health
         
-        quick start
-        -----------
-        
-        For the impatient, there are two quick ways to install this::
-        
-           $ pip install txtorcon
-        
-        ... or, if you checked out or downloaded the source::
-        
-           $ python setup.py install
-        
-        ... or, better yet, use a virtualenv and the dev requirements::
-        
-           $ virtualenv venv
-           $ ./venv/bin/pip install -e .[dev]
         
-        For OSX, we can install txtorcon with the help of easy_install::
+        txtorcon
+        ========
         
-           $ easy_install txtorcon
+        - **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion
+        - **code**: https://github.com/meejah/txtorcon
+        - ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git``
+        - MIT-licensed;
+        - Python 2.7, PyPy 5.0.0+, Python 3.4+;
+        - depends on
+          `Twisted <https://twistedmatrix.com>`_,
+          `Automat <https://github.com/glyph/automat>`_,
+          (and the `ipaddress <https://pypi.python.org/pypi/ipaddress>`_ backport for non Python 3)
         
-        To avoid installing, you can just add the base of the source to your
-        PYTHONPATH::
+        .. caution::
         
-           $ export PYTHONPATH=`pwd`:$PYTHONPATH
+          Several large, new features have landed on master. If you're working
+          directly from master, note that some of these APIs may change before
+          the next release.
         
-        Then, you will want to explore the examples. Try "python
-        examples/stream\_circuit\_logger.py" for instance.
         
-        On Debian testing (jessie), or with wheezy-backports (big thanks to
-        Lunar^ for all his packaging work) you can install easily::
-        
-            $ apt-get install python-txtorcon
-        
-        You may also like `this asciinema demo <http://asciinema.org/a/5654>`_
-        for an overview.
-        
-        Tor configuration
+        Ten Thousand Feet
         -----------------
         
-        You'll want to have the following options on in your ``torrc``::
-        
-           CookieAuthentication 1
-           CookieAuthFileGroupReadable 1
-        
-        If you want to use unix sockets to speak to tor::
-        
-           ControlSocketsGroupWritable 1
-           ControlSocket /var/run/tor/control
-        
-        The defaults used by py:meth:`txtorcon.build_local_tor_connection` will
-        find a Tor on ``9051`` or ``/var/run/tor/control``
-        
-        
-        overview
-        --------
-        
-        txtorcon is a Twisted-based asynchronous Tor control protocol
-        implementation. Twisted is an event-driven networking engine written
-        in Python and Tor is an onion-routing network designed to improve
-        people's privacy and anonymity on the Internet.
-        
-        The main abstraction of this library is txtorcon.TorControlProtocol
-        which presents an asynchronous API to speak the Tor client protocol in
-        Python. txtorcon also provides abstractions to track and get updates
-        about Tor's state (txtorcon.TorState) and current configuration
-        (including writing it to Tor or disk) in txtorcon.TorConfig, along
-        with helpers to asynchronously launch slave instances of Tor including
-        Twisted endpoint support.
-        
-        txtorcon runs all tests cleanly on:
-        
-        -  Debian "squeeze", "wheezy" and "jessie"
-        -  OS X 10.4 (naif)
-        -  OS X 10.8 (lukas lueg)
-        -  OS X 10.9 (kurt neufeld)
-        -  Fedora 18 (lukas lueg)
-        -  FreeBSD 10 (enrique fynn) (**needed to install "lsof"**)
-        -  RHEL6
-        -  Reports from other OSes appreciated.
-        
-        If instead you want a synchronous (threaded) Python controller
-        library, check out Stem at https://stem.torproject.org/
-        
-        
-        quick implementation overview
-        -----------------------------
-        
-        txtorcon provides a class to track Tor's current state -- such as
-        details about routers, circuits and streams -- called
-        txtorcon.TorState and an abstraction to the configuration values via
-        txtorcon.TorConfig which provides attribute-style accessors to Tor's
-        state (including making changes). txtorcon.TorState provides
-        txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which
-        implement a listener interface so client code may receive updates (in
-        real time) including Tor events.
-        
-        txtorcon uses **trial for unit-tests** and has 100% test-coverage --
-        which is not to say I've covered all the cases, but nearly all of the
-        code is at least exercised somehow by the unit tests.
-        
-        Tor itself is not required to be running for any of the tests. ohcount
-        claims around 2000 lines of code for the core bit; around 4000
-        including tests. About 37% comments in the not-test code.
-        
-        There are a few simple integration tests, based on Docker. More are
-        always welcome!
-        
-        
-        dependencies / requirements
+        txtorcon is an implementation of the `control-spec
+        <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+        for `Tor <https://www.torproject.org/>`_ using the `Twisted
+        <https://twistedmatrix.com/trac/>`_ networking library for `Python
+        <http://python.org/>`_.
+        
+        This is useful for writing utilities to control or make use of Tor in
+        event-based Python programs. If your Twisted program supports
+        endpoints (like ``twistd`` does) your server or client can make use of
+        Tor immediately, with no code changes. Start your own Tor or connect
+        to one and get live stream, circuit, relay updates; read and change
+        config; monitor events; build circuits; create onion services;
+        etcetera (`ReadTheDocs <https://txtorcon.readthedocs.org>`_).
+        
+        
+        Some Possibly Motivational Example Code
+        ---------------------------------------
+        
+        `download <examples/readme.py>`_
+        (also `python3 style <examples/readme3.py>`_)
+        
+        .. code:: python
+        
+            from twisted.internet.task import react
+            from twisted.internet.defer import inlineCallbacks
+            from twisted.internet.endpoints import UNIXClientEndpoint
+            import treq
+            import txtorcon
+        
+            @react
+            @inlineCallbacks
+            def main(reactor):
+                tor = yield txtorcon.connect(
+                    reactor,
+                    UNIXClientEndpoint(reactor, "/var/run/tor/control")
+                )
+        
+                print("Connected to Tor version {}".format(tor.version))
+        
+                url = 'https://www.torproject.org:443'
+                print("Downloading {}".format(url))
+                resp = yield treq.get(url, agent=tor.web_agent())
+        
+                print("   {} bytes".format(resp.length))
+                data = yield resp.text()
+                print("Got {} bytes:\n{}\n[...]{}".format(
+                    len(data),
+                    data[:120],
+                    data[-120:],
+                ))
+        
+                print("Creating a circuit")
+                state = yield tor.create_state()
+                circ = yield state.build_circuit()
+                yield circ.when_built()
+                print("  path: {}".format(" -> ".join([r.ip for r in circ.path])))
+        
+                print("Downloading meejah's public key via above circuit...")
+                resp = yield treq.get(
+                    'https://meejah.ca/meejah.asc',
+                    agent=circ.web_agent(reactor, tor.config.socks_endpoint(reactor)),
+                )
+                data = yield resp.text()
+                print(data)
+        
+        
+        
+        Try It Now On Debian/Ubuntu
         ---------------------------
         
-        - `twisted <http://twistedmatrix.com>`_: txtorcon should work with any
-           Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so
-           does txtorcon (if you find something broken on Py3 please file a bug).
-        
-        -  `GeoIP <https://www.maxmind.com/app/python>`_: **optional** provides location
-           information for ip addresses; you will want to download GeoLite City
-           from `MaxMind <https://www.maxmind.com/app/geolitecity>`_ or pay them
-           for more accuracy. Or use tor-geoip, which makes this sort-of
-           optional, in that we'll query Tor for the IP if the GeoIP database
-           doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
-        
-        -  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
-           documentation. In that case you'll also need something called
-           ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
-           the Interface-derived docs properly.
-        
-        -  development: `coverage <http://nedbatchelder.com/code/coverage/>`_ to
-           run the code-coverage metrics, and Tox
-        
-        -  optional: GraphViz is used in the tests (and to generate state-machine
-           diagrams, if you like) but those tests are skipped if "dot" isn't
-           in your path
-        
-        .. BEGIN_INSTALL
-        
-        In any case, on a `Debian <http://www.debian.org/>`_ wheezy, squeeze or
-        Ubuntu system, this should work::
-        
-            apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor
-            apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development
-        
-        .. END_INSTALL
-        
-        Using pip this would be::
-        
-            pip install Twisted ipaddr pygeoip
-            pip install GeoIP Sphinx repoze.sphinx.autointerface coverage  # for development
-        
-        or::
-        
-            pip install -r requirements.txt
-            pip install -r dev-requirements.txt
-        
-        or for the bare minimum::
-        
-            pip install Twisted  # will install zope.interface too
-        
-        
-        documentation
-        -------------
-        
-        It is likely that you will need to read at least some of
-        `control-spec.txt <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
-        from the torspec git repository so you know what's being abstracted by
-        this library.
-        
-        Run "make doc" to build the Sphinx documentation locally, or rely on
-        ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged
-        release and the latest master.
-        
-        There is also a directory of examples/ scripts, which have inline
-        documentation explaining their use.
-        
-        
-        contact information
-        -------------------
-        
-        For novelty value, the Web site (with built documentation and so forth)
-        can be viewed via Tor at http://timaq4ygg2iegci7.onion although the
-        code itself is hosted via git::
-        
-            torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git
-        
-        or::
+        For example, serve some files via an onion service (*aka* hidden
+        service):
         
-            git clone git://github.com/meejah/txtorcon.git
+        .. code-block:: shell-session
         
-        You may contact me via ``meejah at meejah dot ca`` with GPG key
-        `0xC2602803128069A7
-        <http://pgp.mit.edu:11371/pks/lookup?op=get&search=0xC2602803128069A7>`_
-        or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A
-        2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``.
+            $ sudo apt-get install python-txtorcon
+            $ twistd -n web --port "onion:80" --path ~/public_html
         
-        It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC
-        <http://www.oftc.net/oftc/>`_ but be patient for replies (I do look at
-        scrollback, so putting "meejah: " in front will alert my client).
         
-        More conventionally, you may get the code at GitHub and documentation
-        via ReadTheDocs:
+        Read More
+        ---------
         
-        -  https://github.com/meejah/txtorcon
-        -  https://txtorcon.readthedocs.org
+        All the documentation starts `in docs/index.rst
+        <docs/index.rst>`_. Also hosted at `txtorcon.rtfd.org
+        <https://txtorcon.readthedocs.io/en/latest/>`_.
         
-        Please do **use the GitHub issue-tracker** to report bugs. Patches,
-        pull-requests, comments and criticisms are all welcomed and
-        appreciated.
+        You'll want to start with `the introductions <docs/introduction.rst>`_ (`hosted at RTD
+        <https://txtorcon.readthedocs.org/en/latest/introduction.html>`_).
         
 Keywords: python,twisted,tor,tor controller
 Platform: UNKNOWN
diff --git a/txtorcon.egg-info/SOURCES.txt b/txtorcon.egg-info/SOURCES.txt
index a882602..b61bd2b 100644
--- a/txtorcon.egg-info/SOURCES.txt
+++ b/txtorcon.egg-info/SOURCES.txt
@@ -9,25 +9,25 @@ meejah.asc
 requirements.txt
 setup.py
 docs/Makefile
-docs/README.rst
 docs/apilinks_sphinxext.py
 docs/conf.py
 docs/examples.rst
-docs/howtos.rst
+docs/guide.rst
+docs/hacking.rst
 docs/index.rst
+docs/installing.rst
 docs/introduction.rst
 docs/release-checklist.rst
 docs/releases.rst
-docs/tutorial.rst
 docs/txtorcon-config.rst
+docs/txtorcon-controller.rst
 docs/txtorcon-endpoints.rst
 docs/txtorcon-interface.rst
-docs/txtorcon-launching.rst
 docs/txtorcon-protocol.rst
+docs/txtorcon-socks.rst
 docs/txtorcon-state.rst
 docs/txtorcon-util.rst
 docs/txtorcon.rst
-docs/walkthrough.rst
 docs/_static/avatar.png
 docs/_static/haiku.css
 docs/_static/logo.png
@@ -43,47 +43,59 @@ docs/_themes/alabaster/support.py
 docs/_themes/alabaster/theme.conf
 docs/_themes/alabaster/static/alabaster.css_t
 docs/_themes/alabaster/static/pygments.css
-examples/add_hiddenservice_to_system_tor.py
-examples/attach_streams_by_country.py
-examples/circuit_failure_rates.py
-examples/circuit_for_next_stream.py
+examples/add_hiddenservice_to_system_tor.py.orig
+examples/attach_streams_by_country.py.orig
+examples/circuit_failure_rates.py.orig
+examples/circuit_for_next_stream.py.orig
+examples/connect.py
+examples/connect.py.orig
 examples/disallow_streams_by_port.py
-examples/dump_config.py
-examples/ephemeral_endpoint.py
-examples/gui-boom.py
-examples/gui-cairo.py
-examples/gui-map.py
-examples/gui.py
-examples/gui2.py
-examples/hello_darkweb.py
-examples/hidden-service-systemd.service
+examples/dns_lookups.py
+examples/dns_lookups.py.orig
+examples/dump_config.py.orig
+examples/hello_darkweb.py.orig
+examples/hidden_echo.py
+examples/hidden_echo.py.orig
 examples/launch_tor.py
+examples/launch_tor.py.orig
 examples/launch_tor2web.py
 examples/launch_tor_endpoint.py
+examples/launch_tor_endpoint.py.orig
 examples/launch_tor_endpoint2.py
-examples/launch_tor_with_hiddenservice.py
+examples/launch_tor_endpoint2.py.orig
+examples/launch_tor_unix_sockets.py
+examples/launch_tor_with_hiddenservice.py.orig
 examples/launch_tor_with_simplehttpd.py
 examples/minimal_endpoint.py
 examples/monitor.py
-examples/schedule_bandwidth.py
+examples/readme.py
+examples/readme3.py
 examples/stem_relay_descriptor.py
 examples/stream_circuit_logger.py
-examples/systemd.service
 examples/tor_info.py
-examples/torflow_path_selection.py
-examples/tunnel_tls_through_tor_client.py
 examples/txtorcon.tac
+examples/web_client.py
+examples/web_client.py.orig
+examples/web_client_custom_circuit.py
+examples/web_client_custom_circuit.py.orig
+examples/web_client_treq.py
+examples/web_client_treq.py.orig
 examples/webui_server.py
 scripts/asciinema-demo0.py
 scripts/asciinema-demo1.py
 test/__init__.py
 test/profile_startup.py
+test/py3_torstate.py
 test/test_addrmap.py
+test/test_attacher.py
 test/test_circuit.py
+test/test_controller.py
 test/test_endpoints.py
 test/test_fsm.py
 test/test_log.py
+test/test_microdesc.py
 test/test_router.py
+test/test_socks.py
 test/test_stream.py
 test/test_torconfig.py
 test/test_torcontrolprotocol.py
@@ -91,16 +103,22 @@ test/test_torinfo.py
 test/test_torstate.py
 test/test_util.py
 test/test_util_imports.py
+test/test_web.py
 test/util.py
+test/verify-release.py
 twisted/plugins/txtorcon_endpoint_parser.py
 txtorcon/__init__.py
 txtorcon/_metadata.py
+txtorcon/_microdesc_parser.py
 txtorcon/addrmap.py
+txtorcon/attacher.py
 txtorcon/circuit.py
+txtorcon/controller.py
 txtorcon/endpoints.py
 txtorcon/interface.py
 txtorcon/log.py
 txtorcon/router.py
+txtorcon/socks.py
 txtorcon/spaghetti.py
 txtorcon/stream.py
 txtorcon/torconfig.py
@@ -108,6 +126,7 @@ txtorcon/torcontrolprotocol.py
 txtorcon/torinfo.py
 txtorcon/torstate.py
 txtorcon/util.py
+txtorcon/web.py
 txtorcon.egg-info/PKG-INFO
 txtorcon.egg-info/SOURCES.txt
 txtorcon.egg-info/dependency_links.txt
diff --git a/txtorcon.egg-info/requires.txt b/txtorcon.egg-info/requires.txt
index 7d85bda..c7ecd10 100644
--- a/txtorcon.egg-info/requires.txt
+++ b/txtorcon.egg-info/requires.txt
@@ -1,11 +1,13 @@
-Twisted>=11.1.0
+Twisted[tls]>=15.5.0
 ipaddress>=1.0.16
 zope.interface>=3.6.1
-txsocksx>=1.13.0
+incremental
+automat
 
 [dev]
 tox
 coverage
+cuvner
 setuptools>=0.8.0
 Sphinx
 repoze.sphinx.autointerface>=0.4
@@ -17,4 +19,4 @@ pyflakes
 pep8
 mock
 ipaddress>=1.0.16
-GeoIP
+geoip
diff --git a/txtorcon.egg-info/top_level.txt b/txtorcon.egg-info/top_level.txt
index dc51759..f572450 100644
--- a/txtorcon.egg-info/top_level.txt
+++ b/txtorcon.egg-info/top_level.txt
@@ -1,2 +1,3 @@
+test
 twisted
 txtorcon
diff --git a/txtorcon/__init__.py b/txtorcon/__init__.py
index a2387a8..a24aa2f 100644
--- a/txtorcon/__init__.py
+++ b/txtorcon/__init__.py
@@ -13,7 +13,7 @@ from txtorcon.circuit import Circuit
 from txtorcon.circuit import build_timeout_circuit
 from txtorcon.circuit import CircuitBuildTimedOutError
 from txtorcon.stream import Stream
-from txtorcon.torcontrolprotocol import connect
+from txtorcon.controller import connect
 from txtorcon.torcontrolprotocol import TorControlProtocol
 from txtorcon.torcontrolprotocol import TorProtocolError
 from txtorcon.torcontrolprotocol import TorProtocolFactory
@@ -24,9 +24,11 @@ from txtorcon.torstate import build_local_tor_connection
 from txtorcon.torconfig import TorConfig
 from txtorcon.torconfig import HiddenService
 from txtorcon.torconfig import EphemeralHiddenService
-from txtorcon.torconfig import TorProcessProtocol
-from txtorcon.torconfig import launch_tor
-from txtorcon.torconfig import TorNotFound
+from txtorcon.torconfig import launch_tor  # this one depreceated, use launch()
+from txtorcon.controller import TorProcessProtocol
+from txtorcon.controller import launch  # this is "newer" one
+from txtorcon.controller import Tor
+from txtorcon.controller import TorNotFound
 from txtorcon.torinfo import TorInfo
 from txtorcon.addrmap import AddrMap
 from txtorcon.endpoints import TorOnionAddress
@@ -44,14 +46,15 @@ from txtorcon.interface import (
     ITorControlProtocol,
     IStreamListener, IStreamAttacher, StreamListenerMixin,
     ICircuitContainer, ICircuitListener, CircuitListenerMixin,
-    IRouterContainer, IAddrListener,
+    IRouterContainer, IAddrListener, ITor
 )
 
 __all__ = [
+    "connect", "launch",  # connect, launch return instance of Tor()...
+    "Tor", "ITor",        # ...which is the preferred high-level API
     "Router",
     "Circuit",
     "Stream",
-    "connect",
     "TorControlProtocol", "TorProtocolError", "TorProtocolFactory",
     "TorState", "DEFAULT_VALUE",
     "TorInfo",
diff --git a/txtorcon/_metadata.py b/txtorcon/_metadata.py
index a436b35..a0bf52d 100644
--- a/txtorcon/_metadata.py
+++ b/txtorcon/_metadata.py
@@ -1,6 +1,6 @@
-__version__ = '0.17.0'
+__version__ = '0.19.3'
 __author__ = 'meejah'
 __contact__ = 'meejah at meejah.ca'
 __url__ = 'https://github.com/meejah/txtorcon'
 __license__ = 'MIT'
-__copyright__ = 'Copyright 2012-2016'
+__copyright__ = 'Copyright 2012-2017'
diff --git a/txtorcon/_microdesc_parser.py b/txtorcon/_microdesc_parser.py
new file mode 100644
index 0000000..0ff5b1d
--- /dev/null
+++ b/txtorcon/_microdesc_parser.py
@@ -0,0 +1,112 @@
+
+from .util import find_keywords
+
+
+# putting the old parser back in here for now until there's a solution
+# making Automat faster
+from .spaghetti import FSM, State, Transition
+
+
+class MicrodescriptorParser(object):
+    """
+    Parsers microdescriptors line by line. New relays are emitted via
+    the 'create_relay' callback.
+    """
+
+    def __init__(self, create_relay):
+        self._create_relay = create_relay
+        self._relay_attrs = None
+
+        class die(object):
+            __name__ = 'die'  # FIXME? just to ease spagetti.py:82's pain
+
+            def __init__(self, msg):
+                self.msg = msg
+
+            def __call__(self, *args):
+                raise RuntimeError(self.msg % tuple(args))
+
+        waiting_r = State("waiting_r")
+        waiting_w = State("waiting_w")
+        waiting_p = State("waiting_p")
+        waiting_s = State("waiting_s")
+
+        def ignorable_line(x):
+            x = x.strip()
+            return x in ['.', 'OK', ''] or x.startswith('ns/')
+
+        waiting_r.add_transition(Transition(waiting_r, ignorable_line, None))
+        waiting_r.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))
+        # FIXME use better method/func than die!!
+        waiting_r.add_transition(Transition(waiting_r, lambda x: not x.startswith('r '), die('Expected "r " while parsing routers not "%s"')))
+
+        waiting_s.add_transition(Transition(waiting_w, lambda x: x.startswith('s '), self._router_flags))
+        waiting_s.add_transition(Transition(waiting_s, lambda x: x.startswith('a '), self._router_address))
+        waiting_s.add_transition(Transition(waiting_r, ignorable_line, None))
+        waiting_s.add_transition(Transition(waiting_r, lambda x: not x.startswith('s ') and not x.startswith('a '), die('Expected "s " while parsing routers not "%s"')))
+        waiting_s.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
+
+        waiting_w.add_transition(Transition(waiting_p, lambda x: x.startswith('w '), self._router_bandwidth))
+        waiting_w.add_transition(Transition(waiting_r, ignorable_line, None))
+        waiting_w.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))  # "w" lines are optional
+        waiting_w.add_transition(Transition(waiting_r, lambda x: not x.startswith('w '), die('Expected "w " while parsing routers not "%s"')))
+        waiting_w.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
+
+        waiting_p.add_transition(Transition(waiting_r, lambda x: x.startswith('p '), self._router_policy))
+        waiting_p.add_transition(Transition(waiting_r, ignorable_line, None))
+        waiting_p.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))  # "p" lines are optional
+        waiting_p.add_transition(Transition(waiting_r, lambda x: x[:2] != 'p ', die('Expected "p " while parsing routers not "%s"')))
+        waiting_p.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
+
+        self._machine = FSM([waiting_r, waiting_s, waiting_w, waiting_p])
+        self._relay_attrs = None
+
+    def feed_line(self, line):
+        """
+        A line has been received.
+        """
+        self._machine.process(line)
+
+    def done(self, *args):
+        """
+        All lines have been fed.
+        """
+        self._maybe_callback_router()
+
+    def _maybe_callback_router(self):
+        if self._relay_attrs is not None:
+            self._create_relay(**self._relay_attrs)
+            self._relay_attrs = None
+
+    def _router_begin(self, data):
+        self._maybe_callback_router()
+        args = data.split()[1:]
+        self._relay_attrs = dict(
+            nickname=args[0],
+            idhash=args[1],
+            orhash=args[2],
+            modified=args[3] + ' ' + args[4],
+            ip=args[5],
+            orport=args[6],
+            dirport=args[7],
+        )
+
+    def _router_flags(self, data):
+        args = data.split()[1:]
+        self._relay_attrs['flags'] = args
+
+    def _router_address(self, data):
+        """only for IPv6 addresses"""
+        args = data.split()[1:]
+        try:
+            self._relay_attrs['ip_v6'].extend(args)
+        except KeyError:
+            self._relay_attrs['ip_v6'] = list(args)
+
+    def _router_bandwidth(self, data):
+        args = data.split()[1:]
+        kw = find_keywords(args)
+        self._relay_attrs['bandwidth'] = kw['Bandwidth']
+
+    def _router_policy(self, data):
+        pass
diff --git a/txtorcon/attacher.py b/txtorcon/attacher.py
new file mode 100644
index 0000000..f38e04e
--- /dev/null
+++ b/txtorcon/attacher.py
@@ -0,0 +1,82 @@
+import itertools
+import heapq
+
+from zope.interface import implementer
+
+from .interface import IStreamAttacher
+
+
+# note to self: might be better to make this Way Simpler and just say
+# "order matters", *or* do a simple sort -- so that we can actually
+# remove things.
+
+
+ at implementer(IStreamAttacher)
+class PriorityAttacher(object):
+    """
+    This can fill the role of an IStreamAttacher to which you can add
+    and remove "sub" attachers. These are consulted in order and the
+    first one to return something besides None wins. We use a "heapq"
+    priority queue, with 0 being the "most important" and higher
+    numbers indicating less important.
+
+    For example::
+
+        tor = yield txtorcon.connect(..)
+        attachers = txtorcon.attacher.PriorityAttacher()
+
+        @implementer(IStreamAttacher)
+        class MyAttacher(object):
+            def __init__(self, interesting_host, circuit):
+                self._host = interesting_host
+                self._circuit = circuit
+
+            def attach_stream(self, stream, circuits):
+                if stream.target_host == self._host:
+                    return self._circuit
+                return None
+
+        attachers.add_attacher(MyAttacher('torproject.org', circ1))
+        attachers.add_attacher(MyAttacher('meejah.ca', circ2))
+    """
+
+    def __init__(self):
+        # use only heapq.* to modify this; 0th item is "smallest"
+        # item. contains 3-tuples of (priority, number, attacher)
+        self._attacher_heap = []
+        # need to keep a map so we can delete from the priority-queue :(
+        self._attacher_to_entry = dict()
+        # need to keep a counter so the sorting has a tie-breaker
+        self._counter = itertools.count(0, 1)
+
+    def add_attacher(self, attacher, priority=0):
+        """
+        Add a new IStreamAttacher at a certain priortiy; lower priority
+        values mean more important (that is, 0 is the most important).
+        """
+        item = [priority, next(self._counter), IStreamAttacher(attacher)]
+        self._attacher_to_entry[attacher] = item
+        heapq.heappush(self._attacher_heap, item)
+
+    def remove_attacher(self, attacher):
+        try:
+            item = self._attacher_to_entry.pop(attacher)
+        except KeyError:
+            raise ValueError(
+                "attacher {} not found".format(attacher)
+            )
+        item[-1] = None  # we can't actually remove it from the heap ...
+
+    def attach_stream_failure(self, stream, fail):
+        pass
+    # hmm, should we try to remember which attacher answered
+    # 'something' for this stream, and then report the failure via
+    # it...? or just log all failures here?
+
+    def attach_stream(self, stream, circuits):
+        for _, _, attacher in self._attacher_heap:
+            if attacher is not None:
+                answer = attacher.attach_stream(stream, circuits)
+                if answer is not None:
+                    return answer
+        return None
diff --git a/txtorcon/circuit.py b/txtorcon/circuit.py
index b72072f..bf4d34d 100644
--- a/txtorcon/circuit.py
+++ b/txtorcon/circuit.py
@@ -5,19 +5,142 @@ from __future__ import print_function
 from __future__ import unicode_literals
 from __future__ import with_statement
 
+import six
 import time
 from datetime import datetime
 
 from twisted.python.failure import Failure
 from twisted.python import log
 from twisted.internet import defer
-from .interface import IRouterContainer
-from txtorcon.util import find_keywords
+from twisted.internet.interfaces import IStreamClientEndpoint
+from zope.interface import implementer
+
+from .interface import IRouterContainer, IStreamAttacher
+from txtorcon.util import find_keywords, maybe_ip_addr, SingleObserver
+
 
 # look like "2014-01-25T02:12:14.593772"
 TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
 
 
+ at implementer(IStreamAttacher)
+class _CircuitAttacher(object):
+    """
+    Internal helper.
+
+    If we've ever called .stream_via or .web_agent, then one of these
+    is added as "the" stream-attacher.
+    """
+    def __init__(self):
+        # map real_host (IPAddress) -> circuit
+        self._circuit_targets = dict()
+
+    def add_endpoint(self, target_ep, circuit):
+        """
+        Returns a Deferred that fires when we've attached this endpoint to
+        the provided circuit.
+        """
+        # This can seem a little .. convulted. What's going on is
+        # we're asking the TorCircuitEndpoint to tell us when it gets
+        # the local address (i.e. when "whomever created the endpoit"
+        # actually connects locally). We need this address to
+        # successfully map incoming streams.
+        d = defer.Deferred()
+        target_ep._get_address().addCallback(self._add_real_target, circuit, d)
+        return d
+
+    def _add_real_target(self, real_addr, circuit, d):
+        # joy oh joy, ipaddress wants unicode, Twisted gives us bytes...
+        real_host = maybe_ip_addr(six.text_type(real_addr.host))
+        real_port = real_addr.port
+        self._circuit_targets[(real_host, real_port)] = (circuit, d)
+
+    def attach_stream_failure(self, stream, fail):
+        """
+        IStreamAttacher API
+        """
+        k = (stream.source_addr, stream.source_port)
+        try:
+            (circ, d) = self._circuit_targets.pop(k)
+            d.errback(fail)
+        except KeyError:
+            pass
+        # so this means ... we got an error, but on a stream we either
+        # don't care about or already .callback()'d so should we log
+        # it? or ignore?
+        return None
+
+    @defer.inlineCallbacks
+    def attach_stream(self, stream, circuits):
+        """
+        IStreamAttacher API
+        """
+
+        k = (stream.source_addr, stream.source_port)
+        try:
+            circuit, d = self._circuit_targets.pop(k)
+        except KeyError:
+            return
+
+        try:
+            yield circuit.when_built()
+            if circuit.state in ['FAILED', 'CLOSED', 'DETACHED']:
+                d.errback(Failure(RuntimeError(
+                    "Circuit {circuit.id} in state {circuit.state} so unusable".format(
+                        circuit=circuit,
+                    )
+                )))
+                return
+            d.callback(None)
+            defer.returnValue(circuit)
+        except Exception:
+            d.errback(Failure())
+
+
+ at defer.inlineCallbacks
+def _get_circuit_attacher(reactor, state):
+    if _get_circuit_attacher.attacher is None:
+        _get_circuit_attacher.attacher = _CircuitAttacher()
+        yield state.set_attacher(_get_circuit_attacher.attacher, reactor)
+    defer.returnValue(_get_circuit_attacher.attacher)
+
+
+_get_circuit_attacher.attacher = None
+
+
+ at implementer(IStreamClientEndpoint)
+class TorCircuitEndpoint(object):
+    def __init__(self, reactor, torstate, circuit, target_endpoint):
+        self._reactor = reactor
+        self._state = torstate
+        self._target_endpoint = target_endpoint  # a TorClientEndpoint
+        self._circuit = circuit
+
+    @defer.inlineCallbacks
+    def connect(self, protocol_factory):
+        """IStreamClientEndpoint API"""
+        # need to:
+        # 1. add 'our' attacher to state
+        # 2. do the "underlying" connect
+        # 3. recognize our stream
+        # 4. attach it to our circuit
+
+        attacher = yield _get_circuit_attacher(self._reactor, self._state)
+        # note that we'll only ever add an attacher once, and then it
+        # stays there "forever". so if you never call the .stream_via
+        # or .web_agent APIs, set_attacher won't get called .. but if
+        # you *do*, then you can't call set_attacher yourself (because
+        # that's an error). See discussion in set_attacher on
+        # TorState or issue #169
+
+        yield self._circuit.when_built()
+        connect_d = self._target_endpoint.connect(protocol_factory)
+        attached_d = attacher.add_endpoint(self._target_endpoint, self._circuit)
+        proto = yield connect_d
+        yield attached_d
+        defer.returnValue(proto)
+
+
 class Circuit(object):
     """
     Used by :class:`txtorcon.TorState` to represent one of Tor's circuits.
@@ -40,25 +163,25 @@ class Circuit(object):
     :ivar state:
         contains a string from Tor describing the current state of the
         stream. From control-spec.txt section 4.1.1, these are:
-           - LAUNCHED: circuit ID assigned to new circuit
-           - BUILT: all hops finished, can now accept streams
-           - EXTENDED: one more hop has been completed
-           - FAILED: circuit closed (was not built)
-           - CLOSED: circuit closed (was built)
 
-    :ivar purpose:
-        The reason this circuit was built. Values can currently be one
-        of (but see control-spec.txt 4.1.1):
-          - GENERAL
-          - HS_CLIENT_INTRO
-          - HS_CLIENT_REND
-          - HS_SERVICE_INTRO
-          - HS_SERVICE_REND
-          - TESTING
-          - CONTROLLER
-
-    For most purposes, you'll want to look at GENERAL circuits only.
+            - LAUNCHED: circuit ID assigned to new circuit
+            - BUILT: all hops finished, can now accept streams
+            - EXTENDED: one more hop has been completed
+            - FAILED: circuit closed (was not built)
+            - CLOSED: circuit closed (was built)
 
+    :ivar purpose:
+        The reason this circuit was built. For most purposes, you'll
+        want to look at `GENERAL` circuits only. Values can currently
+        be one of (but see control-spec.txt 4.1.1):
+
+            - GENERAL
+            - HS_CLIENT_INTRO
+            - HS_CLIENT_REND
+            - HS_SERVICE_INTRO
+            - HS_SERVICE_REND
+            - TESTING
+            - CONTROLLER
 
     :ivar id:
         The ID of this circuit, a number (or None if unset).
@@ -71,7 +194,7 @@ class Circuit(object):
         """
         self.listeners = []
         self.router_container = IRouterContainer(routercontainer)
-        self.torstate = routercontainer
+        self._torstate = routercontainer  # XXX FIXME
         self.path = []
         self.streams = []
         self.purpose = None
@@ -83,12 +206,14 @@ class Circuit(object):
         # this is used to hold a Deferred that will callback() when
         # this circuit is being CLOSED or FAILED.
         self._closing_deferred = None
+        # XXX ^ should probably be when_closed() etc etc...
 
         # caches parsed value for time_created()
         self._time_created = None
 
-        # all notifications for when_built
-        self._when_built = []
+        # all notifications for when_built, when_closed
+        self._when_built = SingleObserver()
+        self._when_closed = SingleObserver()
 
     # XXX backwards-compat for old .is_built for now
     @property
@@ -103,13 +228,84 @@ class Circuit(object):
         If it's already BUILT when this is called, you get an
         already-successful Deferred; otherwise, the state must change
         to BUILT.
+
+        If the circuit will never hit BUILT (e.g. it is abandoned by
+        Tor before it gets to BUILT) you will receive an errback
         """
-        d = defer.Deferred()
+        # XXX note to self: we never do an errback; fix this behavior
         if self.state == 'BUILT':
-            d.callback(self)
-        else:
-            self._when_built.append(d)
-        return d
+            return defer.succeed(self)
+        return self._when_built.when_fired()
+
+    def when_closed(self):
+        """
+        Returns a Deferred that callback()'s (with this Circuit instance)
+        when this circuit hits CLOSED or FAILED.
+        """
+        if self.state in ['CLOSED', 'FAILED']:
+            return defer.succeed(self)
+        return self._when_closed.when_fired()
+
+    def web_agent(self, reactor, socks_endpoint, pool=None):
+        """
+        :param socks_endpoint: create one with
+            :meth:`txtorcon.TorConfig.create_socks_endpoint`. Can be a
+            Deferred.
+
+        :param pool: passed on to the Agent (as ``pool=``)
+        """
+        # local import because there isn't Agent stuff on some
+        # platforms we support, so this will only error if you try
+        # this on the wrong platform (pypy [??] and old-twisted)
+        from txtorcon import web
+        return web.tor_agent(
+            reactor,
+            socks_endpoint,
+            circuit=self,
+            pool=pool,
+        )
+
+    # XXX should make this API match above web_agent (i.e. pass a
+    # socks_endpoint) or change the above...
+    def stream_via(self, reactor, host, port,
+                   socks_endpoint,
+                   use_tls=False):
+        """
+        This returns an `IStreamClientEndpoint`_ that will connect to
+        the given ``host``, ``port`` via Tor -- and via this
+        parciular circuit.
+
+        We match the streams up using their source-ports, so even if
+        there are many streams in-flight to the same destination they
+        will align correctly. For example, to cause a stream to go to
+        ``torproject.org:443`` via a particular circuit::
+
+            @inlineCallbacks
+            def main(reactor):
+                circ = yield torstate.build_circuit()  # lets Tor decide the path
+                yield circ.when_built()
+                tor_ep = circ.stream_via(reactor, 'torproject.org', 443)
+                # 'factory' is for your protocol
+                proto = yield tor_ep.connect(factory)
+
+        Note that if you're doing client-side Web requests, you
+        probably want to use `treq
+        <http://treq.readthedocs.org/en/latest/>`_ or ``Agent``
+        directly so call :meth:`txtorcon.Circuit.web_agent` instead.
+
+        :param socks_endpoint: should be a Deferred firing a valid
+            IStreamClientEndpoint pointing at a Tor SOCKS port (or an
+            IStreamClientEndpoint already).
+
+        .. _istreamclientendpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html
+        """
+        from .endpoints import TorClientEndpoint
+        ep = TorClientEndpoint(
+            host, port, socks_endpoint,
+            tls=use_tls,
+            reactor=reactor,
+        )
+        return TorCircuitEndpoint(reactor, self._torstate, self, ep)
 
     @property
     def time_created(self):
@@ -140,18 +336,33 @@ class Circuit(object):
         "IfUnused". So for example: circ.close(IfUnused=True)
 
         :return: Deferred which callbacks with this Circuit instance
-        ONLY after Tor has confirmed it is gone (not simply that the
-        CLOSECIRCUIT command has been queued). This could be a while
-        if you included IfUnused.
+            ONLY after Tor has confirmed it is gone (not simply that the
+            CLOSECIRCUIT command has been queued). This could be a while
+            if you included IfUnused.
         """
 
+        # we're already closed; nothing to do
+        if self.state == 'CLOSED':
+            return defer.succeed(None)
+
+        # someone already called close() but we're not closed yet
+        if self._closing_deferred:
+            d = defer.Deferred()
+
+            def closed(arg):
+                d.callback(arg)
+                return arg
+            self._closing_deferred.addBoth(closed)
+            return d
+
+        # actually-close the circuit
         self._closing_deferred = defer.Deferred()
 
         def close_command_is_queued(*args):
             return self._closing_deferred
-        d = self.torstate.close_circuit(self.id, **kw)
+        d = self._torstate.close_circuit(self.id, **kw)
         d.addCallback(close_command_is_queued)
-        return self._closing_deferred
+        return d
 
     def age(self, now=None):
         """
@@ -182,7 +393,8 @@ class Circuit(object):
         # print "Circuit.update:",args
         if self.id is None:
             self.id = int(args[0])
-            [x.circuit_new(self) for x in self.listeners]
+            for x in self.listeners:
+                x.circuit_new(self)
 
         else:
             if int(args[0]) != self.id:
@@ -198,28 +410,36 @@ class Circuit(object):
 
         if self.state == 'LAUNCHED':
             self.path = []
-            [x.circuit_launched(self) for x in self.listeners]
+            for x in self.listeners:
+                x.circuit_launched(self)
         else:
             if self.state != 'FAILED' and self.state != 'CLOSED':
                 if len(args) > 2:
                     self.update_path(args[2].split(','))
 
         if self.state == 'BUILT':
-            [x.circuit_built(self) for x in self.listeners]
-            for d in self._when_built:
-                d.callback(self)
-            self._when_built = []
+            for x in self.listeners:
+                x.circuit_built(self)
+            self._when_built.fire(self)
 
         elif self.state == 'CLOSED':
             if len(self.streams) > 0:
-                # FIXME it seems this can/does happen if a remote
-                # router crashes or otherwise shuts down a circuit
-                # with streams on it still
-                log.err(RuntimeError("Circuit is %s but still has %d streams" %
-                                     (self.state, len(self.streams))))
+                # it seems this can/does happen if a remote router
+                # crashes or otherwise shuts down a circuit with
+                # streams on it still .. also if e.g. you "carml circ
+                # --delete " the circuit while the stream is
+                # in-progress...can we do better than logging?
+                # *should* we do anything else (the stream should get
+                # closed still by Tor).
+                log.msg(
+                    "Circuit is {} but still has {} streams".format(
+                        self.state, len(self.streams)
+                    )
+                )
             flags = self._create_flags(kw)
             self.maybe_call_closing_deferred()
-            [x.circuit_closed(self, **flags) for x in self.listeners]
+            for x in self.listeners:
+                x.circuit_closed(self, **flags)
 
         elif self.state == 'FAILED':
             if len(self.streams) > 0:
@@ -227,7 +447,8 @@ class Circuit(object):
                                      (self.state, len(self.streams))))
             flags = self._create_flags(kw)
             self.maybe_call_closing_deferred()
-            [x.circuit_failed(self, **flags) for x in self.listeners]
+            for x in self.listeners:
+                x.circuit_failed(self, **flags)
 
     def maybe_call_closing_deferred(self):
         """
@@ -238,6 +459,7 @@ class Circuit(object):
         if self._closing_deferred:
             self._closing_deferred.callback(self)
             self._closing_deferred = None
+        self._when_closed.fire(self)
 
     def update_path(self, path):
         """
@@ -265,8 +487,10 @@ class Circuit(object):
             router = self.router_container.router_from_id(p)
 
             self.path.append(router)
+            # if the path grew, notify listeners
             if len(self.path) > len(oldpath):
-                [x.circuit_extend(self, router) for x in self.listeners]
+                for x in self.listeners:
+                    x.circuit_extend(self, router)
                 oldpath = self.path
 
     def __str__(self):
@@ -276,7 +500,7 @@ class Circuit(object):
 
 
 class CircuitBuildTimedOutError(Exception):
-    """
+        """
     This exception is thrown when using `timed_circuit_build`
     and the circuit build times-out.
     """
@@ -284,17 +508,33 @@ class CircuitBuildTimedOutError(Exception):
 
 def build_timeout_circuit(tor_state, reactor, path, timeout, using_guards=False):
     """
-    returns a deferred which fires when the
-    circuit build succeeds or fails to build.
-    CircuitBuildTimedOutError will be raised unless we
-    receive a circuit build result within the `timeout` duration.
+    Build a new circuit within a timeout.
+
+    CircuitBuildTimedOutError will be raised unless we receive a
+    circuit build result (success or failure) within the `timeout`
+    duration.
+
+    :returns: a Deferred which fires when the circuit build succeeds (or
+        fails to build).
     """
-    d = tor_state.build_circuit(path, using_guards)
-    reactor.callLater(timeout, d.cancel)
+    timed_circuit = []
+    d = tor_state.build_circuit(routers=path, using_guards=using_guards)
+
+    def get_circuit(c):
+        timed_circuit.append(c)
+        return c
 
     def trap_cancel(f):
         f.trap(defer.CancelledError)
-        return Failure(CircuitBuildTimedOutError("circuit build timed out"))
-    d.addCallback(lambda circuit: circuit.when_built())
+        if timed_circuit:
+            d2 = timed_circuit[0].close()
+        else:
+            d2 = defer.succeed(None)
+        d2.addCallback(lambda ign: Failure(CircuitBuildTimedOutError("circuit build timed out")))
+        return d2
+
+    d.addCallback(get_circuit)
+    d.addCallback(lambda circ: circ.when_built())
     d.addErrback(trap_cancel)
+    reactor.callLater(timeout, d.cancel)
     return d
diff --git a/txtorcon/controller.py b/txtorcon/controller.py
new file mode 100644
index 0000000..61b6ac2
--- /dev/null
+++ b/txtorcon/controller.py
@@ -0,0 +1,968 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import with_statement
+
+import os
+import sys
+import six
+import shlex
+import tempfile
+import functools
+import ipaddress
+from io import StringIO
+from collections import Sequence
+from os.path import dirname, exists
+
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, succeed, fail
+from twisted.internet import protocol, error
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.endpoints import UNIXClientEndpoint
+from twisted.internet.interfaces import IReactorTime, IReactorCore
+from twisted.internet.interfaces import IStreamClientEndpoint
+
+from zope.interface import implementer
+
+from txtorcon.util import delete_file_or_tree, find_keywords
+from txtorcon.util import find_tor_binary, available_tcp_port
+from txtorcon.log import txtorlog
+from txtorcon.torcontrolprotocol import TorProtocolFactory
+from txtorcon.torstate import TorState
+from txtorcon.torconfig import TorConfig
+from txtorcon.endpoints import TorClientEndpoint, _create_socks_endpoint
+from . import socks
+from .interface import ITor
+
+if sys.platform in ('linux', 'linux2', 'darwin'):
+    import pwd
+
+
+ at inlineCallbacks
+def launch(reactor,
+           progress_updates=None,
+           control_port=None,
+           data_directory=None,
+           socks_port=None,
+           stdout=None,
+           stderr=None,
+           timeout=None,
+           tor_binary=None,
+           user=None,  # XXX like the config['User'] special-casing from before
+           # 'users' probably never need these:
+           connection_creator=None,
+           kill_on_stderr=True,
+           _tor_config=None,  # a TorConfig instance, mostly for tests
+           ):
+    """
+    launches a new Tor process, and returns a Deferred that fires with
+    a new :class:`txtorcon.Tor` instance. From this instance, you can
+    create or get any "interesting" instances you need: the
+    :class:`txtorcon.TorConfig` instance, create endpoints, create
+    :class:`txtorcon.TorState` instance(s), etc.
+
+    Note that there is NO way to pass in a config; we only expost a
+    couple of basic Tor options. If you need anything beyond these,
+    you can access the ``TorConfig`` instance (via ``.config``)
+    and make any changes there, reflecting them in tor with
+    ``.config.save()``.
+
+    You can igore all the options and safe defaults will be
+    provided. However, **it is recommended to pass data_directory**
+    especially if you will be starting up Tor frequently, as it saves
+    a bunch of time (and bandwidth for the directory
+    authorities). "Safe defaults" means:
+
+      - a tempdir for a ``DataDirectory`` is used (respecting ``TMP``)
+        and is deleted when this tor is shut down (you therefore
+        *probably* want to supply the ``data_directory=`` kwarg);
+      - a random, currently-unused local TCP port is used as the
+        ``SocksPort`` (specify ``socks_port=`` if you want your
+        own). If you want no SOCKS listener at all, pass
+        ``socks_port=0``
+      - we set ``__OwningControllerProcess`` and call
+        ``TAKEOWNERSHIP`` so that if our control connection goes away,
+        tor shuts down (see `control-spec
+        <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+        3.23).
+      - the launched Tor will use ``COOKIE`` authentication.
+
+    :param reactor: a Twisted IReactorCore implementation (usually
+        twisted.internet.reactor)
+
+    :param progress_updates: a callback which gets progress updates; gets 3
+         args: percent, tag, summary (FIXME make an interface for this).
+
+    :param data_directory: set as the ``DataDirectory`` option to Tor,
+        this is where tor keeps its state information (cached relays,
+        etc); starting with an already-populated state directory is a lot
+        faster. If ``None`` (the default), we create a tempdir for this
+        **and delete it on exit**. It is recommended you pass something here.
+
+    :param stdout: a file-like object to which we write anything that
+        Tor prints on stdout (just needs to support write()).
+
+    :param stderr: a file-like object to which we write anything that
+        Tor prints on stderr (just needs .write()). Note that we kill
+        Tor off by default if anything appears on stderr; pass
+        "kill_on_stderr=False" if you don't want this behavior.
+
+    :param tor_binary: path to the Tor binary to run. If None (the
+        default), we try to find the tor binary.
+
+    :param kill_on_stderr:
+        When True (the default), if Tor prints anything on stderr we
+        kill off the process, close the TorControlProtocol and raise
+        an exception.
+
+    :param connection_creator: is mostly available to ease testing, so
+        you probably don't want to supply this. If supplied, it is a
+        callable that should return a Deferred that delivers an
+        :api:`twisted.internet.interfaces.IProtocol <IProtocol>` or
+        ConnectError.
+        See :api:`twisted.internet.interfaces.IStreamClientEndpoint`.connect
+        Note that this parameter is ignored if config.ControlPort == 0
+
+    :return: a Deferred which callbacks with :class:`txtorcon.Tor`
+        instance, from which you can retrieve the TorControlProtocol
+        instance via the ``.protocol`` property.
+
+    HACKS:
+
+     1. It's hard to know when Tor has both (completely!) written its
+        authentication cookie file AND is listening on the control
+        port. It seems that waiting for the first 'bootstrap' message on
+        stdout is sufficient. Seems fragile...and doesn't work 100% of
+        the time, so FIXME look at Tor source.
+
+
+
+    XXX this "User" thing was, IIRC, a feature for root-using scripts
+    (!!) that were going to launch tor, but where tor would drop to a
+    different user. Do we still want to support this? Probably
+    relevant to Docker (where everything is root! yay!)
+
+    ``User``: if this exists, we attempt to set ownership of the tempdir
+    to this user (but only if our effective UID is 0).
+    """
+
+    # We have a slight problem with the approach: we need to pass a
+    # few minimum values to a torrc file so that Tor will start up
+    # enough that we may connect to it. Ideally, we'd be able to
+    # start a Tor up which doesn't really do anything except provide
+    # "AUTHENTICATE" and "GETINFO config/names" so we can do our
+    # config validation.
+
+    if not IReactorCore.providedBy(reactor):
+        raise ValueError(
+            "'reactor' argument must provide IReactorCore"
+            " (got '{}': {})".format(
+                type(reactor).__class__.__name__,
+                repr(reactor)
+            )
+        )
+
+    if tor_binary is None:
+        tor_binary = find_tor_binary()
+    if tor_binary is None:
+        # We fail right here instead of waiting for the reactor to start
+        raise TorNotFound('Tor binary could not be found')
+
+    # make sure we got things that have write() for stderr, stdout
+    # kwargs (XXX is there a "better" way to check for file-like object?)
+    for arg in [stderr, stdout]:
+        if arg and not getattr(arg, "write", None):
+            raise RuntimeError(
+                'File-like object needed for stdout or stderr args.'
+            )
+
+    config = _tor_config or TorConfig()
+    if data_directory is not None:
+        user_set_data_directory = True
+        config.DataDirectory = data_directory
+        try:
+            os.mkdir(data_directory, 0o0700)
+        except OSError:
+            pass
+    else:
+        user_set_data_directory = False
+        data_directory = tempfile.mkdtemp(prefix='tortmp')
+        config.DataDirectory = data_directory
+        # note: we also set up the ProcessProtocol to delete this when
+        # Tor exits, this is "just in case" fallback:
+        reactor.addSystemEventTrigger(
+            'before', 'shutdown',
+            functools.partial(delete_file_or_tree, data_directory)
+        )
+
+    # things that used launch_tor() had to set ControlPort and/or
+    # SocksPort on the config to pass them, so we honour that here.
+    if control_port is None and _tor_config is not None:
+        try:
+            control_port = config.ControlPort
+        except KeyError:
+            control_port = None
+
+    if socks_port is None and _tor_config is not None:
+        try:
+            socks_port = config.SocksPort
+        except KeyError:
+            socks_port = None
+
+    if socks_port is None:
+        socks_port = yield available_tcp_port(reactor)
+    config.SOCKSPort = socks_port
+
+    try:
+        our_user = user or config.User
+    except KeyError:
+        pass
+    else:
+        if sys.platform in ('linux', 'linux2', 'darwin') and os.geteuid() == 0:
+            os.chown(data_directory, pwd.getpwnam(our_user).pw_uid, -1)
+
+    # user can pass in a control port, or we set one up here
+    if control_port is None:
+        # on posix-y systems, we can use a unix-socket
+        if sys.platform in ('linux', 'linux2', 'darwin'):
+            # note: tor will not accept a relative path for ControlPort
+            control_port = 'unix:{}'.format(
+                os.path.join(os.path.realpath(data_directory), 'control.socket')
+            )
+        else:
+            control_port = yield available_tcp_port(reactor)
+    else:
+        if str(control_port).startswith('unix:'):
+            control_path = control_port.lstrip('unix:')
+            containing_dir = dirname(control_path)
+            if not exists(containing_dir):
+                raise ValueError(
+                    "The directory containing '{}' must exist".format(
+                        containing_dir
+                    )
+                )
+            # Tor will be sad if the directory isn't 0700
+            mode = (0o0777 & os.stat(containing_dir).st_mode)
+            if mode & ~(0o0700):
+                raise ValueError(
+                    "The directory containing a unix control-socket ('{}') "
+                    "must only be readable by the user".format(containing_dir)
+                )
+    config.ControlPort = control_port
+
+    config.CookieAuthentication = 1
+    config.__OwningControllerProcess = os.getpid()
+    if connection_creator is None:
+        if str(control_port).startswith('unix:'):
+            connection_creator = functools.partial(
+                UNIXClientEndpoint(reactor, control_port[5:]).connect,
+                TorProtocolFactory()
+            )
+        else:
+            connection_creator = functools.partial(
+                TCP4ClientEndpoint(reactor, 'localhost', control_port).connect,
+                TorProtocolFactory()
+            )
+    # not an "else" on purpose; if we passed in "control_port=0" *and*
+    # a custom connection creator, we should still set this to None so
+    # it's never called (since we can't connect with ControlPort=0)
+    if control_port == 0:
+        connection_creator = None
+
+    # NOTE well, that if we don't pass "-f" then Tor will merrily load
+    # its default torrc, and apply our options over top... :/ should
+    # file a bug probably? --no-defaults or something maybe? (does
+    # --defaults-torrc - or something work?)
+    config_args = ['-f', '/dev/null/non-existant-on-purpose', '--ignore-missing-torrc']
+
+    # ...now add all our config options on the command-line. This
+    # avoids writing a temporary torrc.
+    for (k, v) in config.config_args():
+        config_args.append(k)
+        config_args.append(v)
+
+    process_protocol = TorProcessProtocol(
+        connection_creator,
+        progress_updates,
+        config, reactor,
+        timeout,
+        kill_on_stderr,
+        stdout,
+        stderr,
+    )
+    if control_port == 0:
+        connected_cb = succeed(None)
+    else:
+        connected_cb = process_protocol.when_connected()
+
+    # we set both to_delete and the shutdown events because this
+    # process might be shut down way before the reactor, but if the
+    # reactor bombs out without the subprocess getting closed cleanly,
+    # we'll want the system shutdown events triggered so the temporary
+    # files get cleaned up either way
+
+    # we don't want to delete the user's directories, just temporary
+    # ones this method created.
+    if not user_set_data_directory:
+        process_protocol.to_delete = [data_directory]
+        reactor.addSystemEventTrigger(
+            'before', 'shutdown',
+            functools.partial(delete_file_or_tree, data_directory)
+        )
+
+    log.msg('Spawning tor process with DataDirectory', data_directory)
+    args = [tor_binary] + config_args
+    # XXX note to self; we create data_directory above, so when this
+    # is master we can close
+    # https://github.com/meejah/txtorcon/issues/178
+    transport = reactor.spawnProcess(
+        process_protocol,
+        tor_binary,
+        args=args,
+        env={'HOME': data_directory},
+        path=data_directory if os.path.exists(data_directory) else None,  # XXX error if it doesn't exist?
+    )
+    # FIXME? don't need rest of the args: uid, gid, usePTY, childFDs)
+    transport.closeStdin()
+    proto = yield connected_cb
+    # note "proto" here is a TorProcessProtocol
+
+    # we might need to attach this protocol to the TorConfig
+    if config.protocol is None and proto is not None and proto.tor_protocol is not None:
+        # proto is None in the ControlPort=0 case
+        yield config.attach_protocol(proto.tor_protocol)
+        # note that attach_protocol waits for the protocol to be
+        # boostrapped if necessary
+
+    returnValue(
+        Tor(
+            reactor,
+            config.protocol,
+            _tor_config=config,
+            _process_proto=process_protocol,
+        )
+    )
+
+
+# XXX
+# what about control_endpoint_or_endpoints? (i.e. allow a list to try?)
+# what about if it's None (default?) and we try some candidates?
+
+ at inlineCallbacks
+def connect(reactor, control_endpoint=None, password_function=None):
+    """
+    Creates a :class:`txtorcon.Tor` instance by connecting to an
+    already-running tor's control port. For example, a common default
+    tor uses is UNIXClientEndpoint(reactor, '/var/run/tor/control') or
+    TCP4ClientEndpoint(reactor, 'localhost', 9051)
+
+    If only password authentication is available in the tor we connect
+    to, the ``password_function`` is called (if supplied) to retrieve
+    a valid password. This function can return a Deferred.
+
+    For example::
+
+        import txtorcon
+        from twisted.internet.task import react
+        from twisted.internet.defer import inlineCallbacks
+
+        @inlineCallbacks
+        def main(reactor):
+            tor = yield txtorcon.connect(
+                TCP4ClientEndpoint(reactor, "localhost", 9051)
+            )
+            state = yield tor.create_state()
+            for circuit in state.circuits:
+                print(circuit)
+
+    :param control_endpoint: None, an IStreamClientEndpoint to connect
+        to, or a Sequence of IStreamClientEndpoint instances to connect
+        to. If None, a list of defaults are tried.
+
+    :param password_function:
+        See :class:`txtorcon.TorControlProtocol`
+
+    :return:
+        a Deferred that fires with a :class:`txtorcon.Tor` instance
+    """
+
+    @inlineCallbacks
+    def try_endpoint(control_ep):
+        assert IStreamClientEndpoint.providedBy(control_ep)
+        proto = yield control_ep.connect(
+            TorProtocolFactory(
+                password_function=password_function
+            )
+        )
+        config = yield TorConfig.from_protocol(proto)
+        tor = Tor(reactor, proto, _tor_config=config)
+        returnValue(tor)
+
+    if control_endpoint is None:
+        to_try = [
+            UNIXClientEndpoint(reactor, '/var/run/tor/control'),
+            TCP4ClientEndpoint(reactor, '127.0.0.1', 9051),
+            TCP4ClientEndpoint(reactor, '127.0.0.1', 9151),
+        ]
+    elif IStreamClientEndpoint.providedBy(control_endpoint):
+        to_try = [control_endpoint]
+    elif isinstance(control_endpoint, Sequence):
+        to_try = control_endpoint
+        for ep in control_endpoint:
+            if not IStreamClientEndpoint.providedBy(ep):
+                raise ValueError(
+                    "For control_endpoint=, '{}' must provide"
+                    " IStreamClientEndpoint".format(ep)
+                )
+    else:
+        raise ValueError(
+            "For control_endpoint=, '{}' must provide"
+            " IStreamClientEndpoint".format(control_endpoint)
+        )
+
+    errors = []
+    for idx, ep in enumerate(to_try):
+        try:
+            tor = yield try_endpoint(ep)
+            txtorlog.msg("Connected via '{}'".format(ep))
+            returnValue(tor)
+        except Exception as e:
+            errors.append(e)
+    if len(errors) == 1:
+        raise errors[0]
+    raise RuntimeError(
+        'Failed to connect to: {}'.format(
+            ', '.join(
+                '{}: {}'.format(ep, err) for ep, err in zip(to_try, errors)
+            )
+        )
+    )
+
+
+ at implementer(ITor)
+class Tor(object):
+    """
+    I represent a single instance of Tor and act as a Builder/Factory
+    for several useful objects you will probably want. There are two
+    ways to create a Tor instance:
+
+       - :func:`txtorcon.connect` to connect to a Tor that is already
+         running (e.g. Tor Browser Bundle, a system Tor, ...).
+       - :func:`txtorcon.launch` to launch a fresh Tor instance
+
+    The stable API provided by this class is :class:`txtorcon.interface.ITor`
+
+    If you desire more control, there are "lower level" APIs which are
+    the very ones used by this class. However, this "highest level"
+    API should cover many use-cases::
+
+        import txtorcon
+
+        @inlineCallbacks
+        def main(reactor):
+            # tor = yield txtorcon.connect(UNIXClientEndpoint(reactor, "/var/run/tor/control"))
+            tor = yield txtorcon.launch(reactor)
+
+            onion_ep = tor.create_onion_endpoint(port=80)
+            port = yield onion_ep.listen(Site())
+            print(port.getHost())
+    """
+
+    def __init__(self, reactor, control_protocol, _tor_config=None, _process_proto=None):
+        """
+        don't instantiate this class yourself -- instead use the factory
+        methods :func:`txtorcon.launch` or :func:`txtorcon.connect`
+        """
+        self._protocol = control_protocol
+        self._config = _tor_config
+        self._reactor = reactor
+        # this only passed/set when we launch()
+        self._process_protocol = _process_proto
+        # cache our preferred socks port (please use
+        # self._default_socks_endpoint() to get one)
+        self._socks_endpoint = None
+
+    @inlineCallbacks
+    def quit(self):
+        """
+        Closes the control connection, and if we launched this Tor
+        instance we'll send it a TERM and wait until it exits.
+        """
+        if self._protocol is not None:
+            yield self._protocol.quit()
+        if self._process_protocol is not None:
+            yield self._process_protocol.quit()
+        if self._protocol is None and self._process_protocol is None:
+            raise RuntimeError(
+                "This Tor has no protocol instance; we can't quit"
+            )
+
+    # XXX bikeshed on this name?
+    @property
+    def process(self):
+        if self._process_protocol:
+            return self._process_protocol
+        raise RuntimeError(
+            "This Tor instance was not launched by us; no process to return"
+        )
+
+    @property
+    def protocol(self):
+        """
+        The TorControlProtocol instance that is communicating with this
+        Tor instance.
+        """
+        return self._protocol
+
+    @property
+    def version(self):
+        return self._protocol.version
+
+    @inlineCallbacks
+    def get_config(self):
+        """
+        :return: a Deferred that fires with a TorConfig instance. This
+            instance represents up-to-date configuration of the tor
+            instance (even if another controller is connected). If you
+            call this more than once you'll get the same TorConfig back.
+        """
+        if self._config is None:
+            self._config = yield TorConfig.from_protocol(self._protocol)
+        returnValue(self._config)
+
+    def web_agent(self, pool=None, socks_endpoint=None):
+        """
+        :param socks_endpoint: If ``None`` (the default), a suitable
+            SOCKS port is chosen from our config (or added). If supplied,
+            should be a Deferred which fires an IStreamClientEndpoint
+            (e.g. the return-value from
+            :meth:`txtorcon.TorConfig.socks_endpoint`) or an immediate
+            IStreamClientEndpoint You probably don't need to mess with
+            this.
+
+        :param pool: passed on to the Agent (as ``pool=``)
+        """
+        # local import since not all platforms have this
+        from txtorcon import web
+
+        if socks_endpoint is None:
+            socks_endpoint = _create_socks_endpoint(self._reactor, self._protocol)
+        if not isinstance(socks_endpoint, Deferred):
+            if not IStreamClientEndpoint.providedBy(socks_endpoint):
+                raise ValueError(
+                    "'socks_endpoint' should be a Deferred or an IStreamClient"
+                    "Endpoint (got '{}')".format(type(socks_endpoint))
+                )
+        return web.tor_agent(
+            self._reactor,
+            socks_endpoint,
+            pool=pool,
+        )
+
+    @inlineCallbacks
+    def dns_resolve(self, hostname):
+        """
+        :param hostname: a string
+
+        :returns: a Deferred that calbacks with the hostname as looked-up
+            via Tor (or errback).  This uses Tor's custom extension to the
+            SOCKS5 protocol.
+        """
+        socks_ep = yield self._default_socks_endpoint()
+        ans = yield socks.resolve(socks_ep, hostname)
+        returnValue(ans)
+
+    @inlineCallbacks
+    def dns_resolve_ptr(self, ip):
+        """
+        :param ip: a string, like "127.0.0.1"
+
+        :returns: a Deferred that calbacks with the IP address as
+            looked-up via Tor (or errback).  This uses Tor's custom
+            extension to the SOCKS5 protocol.
+        """
+        socks_ep = yield self._default_socks_endpoint()
+        ans = yield socks.resolve_ptr(socks_ep, ip)
+        returnValue(ans)
+
+    def stream_via(self, host, port, tls=False, socks_endpoint=None):
+        """
+        This returns an IStreamClientEndpoint_ instance that will use this
+        Tor (via SOCKS) to visit the ``(host, port)`` indicated.
+
+        :param host: The host to connect to. You MUST pass host-names
+            to this. If you absolutely know that you've not leaked DNS
+            (e.g. you save IPs in your app's configuration or similar)
+            then you can pass an IP.
+
+        :param port: Port to connect to.
+
+        :param tls: If True, it will wrap the return endpoint in one
+            that does TLS (default: False).
+
+        :param socks_endpoint: Normally not needed (default: None)
+            but you can pass an IStreamClientEndpoint_ directed at one
+            of the local Tor's SOCKS5 ports (e.g. created with
+            :meth:`txtorcon.TorConfig.create_socks_endpoint`). Can be
+            a Deferred.
+
+        .. _IStreamClientEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html
+        """
+        if _is_non_public_numeric_address(host):
+            raise ValueError("'{}' isn't going to work over Tor".format(host))
+
+        if socks_endpoint is None:
+            socks_endpoint = self._default_socks_endpoint()
+        # socks_endpoint may be a a Deferred, but TorClientEndpoint handles it
+        return TorClientEndpoint(
+            host, port,
+            socks_endpoint=socks_endpoint,
+            tls=tls,
+            reactor=self._reactor,
+        )
+
+    # XXX note to self: insert onion endpoint-creation functions when
+    # merging onion.py
+
+    # XXX or get_state()? and make there be always 0 or 1 states; cf. convo w/ Warner
+    @inlineCallbacks
+    def create_state(self):
+        """
+        returns a Deferred that fires with a ready-to-go
+        :class:`txtorcon.TorState` instance.
+        """
+        state = TorState(self.protocol)
+        yield state.post_bootstrap
+        returnValue(state)
+
+    def __str__(self):
+        return "<Tor version='{tor_version}'>".format(
+            tor_version=self._protocol.version,
+        )
+
+    @inlineCallbacks
+    def _default_socks_endpoint(self):
+        """
+        Returns a Deferred that fires with our default SOCKS endpoint
+        (which might mean setting one up in our attacked Tor if it
+        doesn't have one)
+        """
+        if self._socks_endpoint is None:
+            self._socks_endpoint = yield _create_socks_endpoint(self._reactor, self._protocol)
+        returnValue(self._socks_endpoint)
+
+
+# XXX from magic-wormhole
+def _is_non_public_numeric_address(host):
+    # for numeric hostnames, skip RFC1918 addresses, since no Tor exit
+    # node will be able to reach those. Likewise ignore IPv6 addresses.
+    try:
+        a = ipaddress.ip_address(six.text_type(host))
+    except ValueError:
+        return False        # non-numeric, let Tor try it
+    if a.is_loopback or a.is_multicast or a.is_private or a.is_reserved \
+       or a.is_unspecified:
+        return True         # too weird, don't connect
+    return False
+
+
+class TorNotFound(RuntimeError):
+    """
+    Raised by launch_tor() in case the tor binary was unspecified and could
+    not be found by consulting the shell.
+    """
+
+
+class TorProcessProtocol(protocol.ProcessProtocol):
+
+    def __init__(self, connection_creator, progress_updates=None, config=None,
+                 ireactortime=None, timeout=None, kill_on_stderr=True,
+                 stdout=None, stderr=None):
+        """
+        This will read the output from a Tor process and attempt a
+        connection to its control port when it sees any 'Bootstrapped'
+        message on stdout. You probably don't need to use this
+        directly except as the return value from the
+        :func:`txtorcon.launch_tor` method. tor_protocol contains a
+        valid :class:`txtorcon.TorControlProtocol` instance by that
+        point.
+
+        connection_creator is a callable that should return a Deferred
+        that callbacks with a :class:`txtorcon.TorControlProtocol`;
+        see :func:`txtorcon.launch_tor` for the default one which is a
+        functools.partial that will call
+        ``connect(TorProtocolFactory())`` on an appropriate
+        :api:`twisted.internet.endpoints.TCP4ClientEndpoint`
+
+        :param connection_creator: A no-parameter callable which
+            returns a Deferred which promises a
+            :api:`twisted.internet.interfaces.IStreamClientEndpoint
+            <IStreamClientEndpoint>`. If this is None, we do NOT
+            attempt to connect to the underlying Tor process.
+
+        :param progress_updates: A callback which received progress
+            updates with three args: percent, tag, summary
+
+        :param config: a TorConfig object to connect to the
+            TorControlProtocl from the launched tor (should it succeed)
+
+        :param ireactortime:
+            An object implementing IReactorTime (i.e. a reactor) which
+            needs to be supplied if you pass a timeout.
+
+        :param timeout:
+            An int representing the timeout in seconds. If we are
+            unable to reach 100% by this time we will consider the
+            setting up of Tor to have failed. Must supply ireactortime
+            if you supply this.
+
+        :param kill_on_stderr:
+            When True, kill subprocess if we receive anything on stderr
+
+        :param stdout:
+            Anything subprocess writes to stdout is sent to .write() on this
+
+        :param stderr:
+            Anything subprocess writes to stderr is sent to .write() on this
+
+        :ivar tor_protocol: The TorControlProtocol instance connected
+            to the Tor this
+            :api:`twisted.internet.protocol.ProcessProtocol
+            <ProcessProtocol>`` is speaking to. Will be valid after
+            the Deferred returned from
+            :meth:`TorProcessProtocol.when_connected` is triggered.
+        """
+
+        self.config = config
+        self.tor_protocol = None
+        self.progress_updates = progress_updates
+
+        # XXX if connection_creator is not None .. is connected_cb
+        # tied to connection_creator...?
+        if connection_creator:
+            self.connection_creator = connection_creator
+        else:
+            self.connection_creator = None
+        # use SingleObserver
+        self._connected_listeners = []  # list of Deferred (None when we're connected)
+
+        self.attempted_connect = False
+        self.to_delete = []
+        self.kill_on_stderr = kill_on_stderr
+        self.stderr = stderr
+        self.stdout = stdout
+        self.collected_stdout = StringIO()
+
+        self._setup_complete = False
+        self._did_timeout = False
+        self._timeout_delayed_call = None
+        self._on_exit = []  # Deferred's we owe a call/errback to when we exit
+        if timeout:
+            if not ireactortime:
+                raise RuntimeError(
+                    'Must supply an IReactorTime object when supplying a '
+                    'timeout')
+            ireactortime = IReactorTime(ireactortime)
+            self._timeout_delayed_call = ireactortime.callLater(
+                timeout, self._timeout_expired)
+
+    def when_connected(self):
+        if self._connected_listeners is None:
+            return succeed(self)
+        d = Deferred()
+        self._connected_listeners.append(d)
+        return d
+
+    def _maybe_notify_connected(self, arg):
+        """
+        Internal helper.
+
+        .callback or .errback on all Deferreds we've returned from
+        `when_connected`
+        """
+        if self._connected_listeners is None:
+            return
+        for d in self._connected_listeners:
+            # Twisted will turn this into an errback if "arg" is a
+            # Failure
+            d.callback(arg)
+        self._connected_listeners = None
+
+    def quit(self):
+        """
+        This will terminate (with SIGTERM) the underlying Tor process.
+
+        :returns: a Deferred that callback()'s (with None) when the
+            process has actually exited.
+        """
+
+        try:
+            self.transport.signalProcess('TERM')
+            d = Deferred()
+            self._on_exit.append(d)
+
+        except error.ProcessExitedAlready:
+            self.transport.loseConnection()
+            d = succeed(None)
+        except Exception:
+            d = fail()
+        return d
+
+    def _signal_on_exit(self, reason):
+        to_notify = self._on_exit
+        self._on_exit = []
+        for d in to_notify:
+            d.callback(None)
+
+    def outReceived(self, data):
+        """
+        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
+        """
+
+        if self.stdout:
+            self.stdout.write(data.decode('ascii'))
+
+        # minor hack: we can't try this in connectionMade because
+        # that's when the process first starts up so Tor hasn't
+        # opened any ports properly yet. So, we presume that after
+        # its first output we're good-to-go. If this fails, we'll
+        # reset and try again at the next output (see this class'
+        # tor_connection_failed)
+        txtorlog.msg(data)
+        if not self.attempted_connect and self.connection_creator \
+                and b'Bootstrap' in data:
+            self.attempted_connect = True
+            # hmmm, we don't "do" anything with this Deferred?
+            # (should it be connected to the when_connected
+            # Deferreds?)
+            d = self.connection_creator()
+            d.addCallback(self._tor_connected)
+            d.addErrback(self._tor_connection_failed)
+# XXX 'should' be able to improve the error-handling by directly tying
+# this Deferred into the notifications -- BUT we might try again, so
+# we need to know "have we given up -- had an error" and only in that
+# case send to the connected things. I think?
+#            d.addCallback(self._maybe_notify_connected)
+
+    def _timeout_expired(self):
+        """
+        A timeout was supplied during setup, and the time has run out.
+        """
+        self._did_timeout = True
+        try:
+            self.transport.signalProcess('TERM')
+        except error.ProcessExitedAlready:
+            # XXX why don't we just always do this?
+            self.transport.loseConnection()
+
+        fail = Failure(RuntimeError("timeout while launching Tor"))
+        self._maybe_notify_connected(fail)
+
+    def errReceived(self, data):
+        """
+        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
+        """
+
+        if self.stderr:
+            self.stderr.write(data)
+
+        if self.kill_on_stderr:
+            self.transport.loseConnection()
+            raise RuntimeError(
+                "Received stderr output from slave Tor process: " + data)
+
+    def cleanup(self):
+        """
+        Clean up my temporary files.
+        """
+
+        all([delete_file_or_tree(f) for f in self.to_delete])
+        self.to_delete = []
+
+    def processExited(self, reason):
+        self._signal_on_exit(reason)
+
+    def processEnded(self, status):
+        """
+        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
+        """
+        self.cleanup()
+
+        if status.value.exitCode is None:
+            if self._did_timeout:
+                err = RuntimeError("Timeout waiting for Tor launch.")
+            else:
+                err = RuntimeError(
+                    "Tor was killed (%s)." % status.value.signal)
+        else:
+            err = RuntimeError(
+                "Tor exited with error-code %d" % status.value.exitCode)
+
+        # hmmm, this log() should probably go away...not always an
+        # error (e.g. .quit()
+        log.err(err)
+        self._maybe_notify_connected(Failure(err))
+
+    def progress(self, percent, tag, summary):
+        """
+        Can be overridden or monkey-patched if you want to get
+        progress updates yourself.
+        """
+
+        if self.progress_updates:
+            self.progress_updates(percent, tag, summary)
+
+    # the below are all callbacks
+
+    def _tor_connection_failed(self, failure):
+        # FIXME more robust error-handling please, like a timeout so
+        # we don't just wait forever after 100% bootstrapped (that
+        # is, we're ignoring these errors, but shouldn't do so after
+        # we'll stop trying)
+        # XXX also, should check if the failure is e.g. a syntax error
+        # or an actually connection failure
+
+        # okay, so this is a little trickier than I thought at first:
+        # we *can* just relay this back to the
+        # connection_creator()-returned Deferred, *but* we don't know
+        # if this is "the last" error and we're going to try again
+        # (and thus e.g. should fail all the when_connected()
+        # Deferreds) or not.
+        log.err(failure)
+        self.attempted_connect = False
+        return None
+
+    def _status_client(self, arg):
+        args = shlex.split(arg)
+        if args[1] != 'BOOTSTRAP':
+            return
+
+        kw = find_keywords(args)
+        prog = int(kw['PROGRESS'])
+        tag = kw['TAG']
+        summary = kw['SUMMARY']
+        self.progress(prog, tag, summary)
+
+        if prog == 100:
+            if self._timeout_delayed_call:
+                self._timeout_delayed_call.cancel()
+                self._timeout_delayed_call = None
+            self._maybe_notify_connected(self)
+
+    @inlineCallbacks
+    def _tor_connected(self, proto):
+        txtorlog.msg("tor_connected %s" % proto)
+
+        self.tor_protocol = proto
+        self.tor_protocol.is_owned = self.transport.pid
+
+        yield self.tor_protocol.post_bootstrap
+        txtorlog.msg("Protocol is bootstrapped")
+        yield self.tor_protocol.add_event_listener('STATUS_CLIENT', self._status_client)
+        yield self.tor_protocol.queue_command('TAKEOWNERSHIP')
+        yield self.tor_protocol.queue_command('RESETCONF __OwningControllerProcess')
+        if self.config is not None and self.config.protocol is None:
+            yield self.config.attach_protocol(proto)
+        returnValue(self)  # XXX or "proto"?
diff --git a/txtorcon/endpoints.py b/txtorcon/endpoints.py
index ef30007..7708f39 100644
--- a/txtorcon/endpoints.py
+++ b/txtorcon/endpoints.py
@@ -11,26 +11,10 @@ import tempfile
 import functools
 
 from txtorcon.util import available_tcp_port
+from txtorcon.socks import TorSocksEndpoint
 
-# backwards-compatibility dance: we "should" be using the
-# ...WithReactor class, but in Twisted prior to 14, there is no such
-# class (and the parse() doesn't provide a 'reactor' argument).
-try:
-    from twisted.internet.interfaces import IStreamClientEndpointStringParserWithReactor
-    _HAVE_TX_14 = True
-except ImportError:
-    from twisted.internet.interfaces import IStreamClientEndpointStringParser as IStreamClientEndpointStringParserWithReactor
-    _HAVE_TX_14 = False
-
-try:
-    from twisted.internet.ssl import optionsForClientTLS
-    from txsocksx.tls import TLSWrapClientEndpoint
-    _HAVE_TLS = True
-except ImportError:
-    _HAVE_TLS = False
-
-
-from twisted.internet import defer, reactor
+from twisted.internet.interfaces import IStreamClientEndpointStringParserWithReactor
+from twisted.internet import defer, error
 from twisted.python import log
 from twisted.internet.interfaces import IStreamServerEndpointStringParser
 from twisted.internet.interfaces import IStreamServerEndpoint
@@ -40,17 +24,17 @@ from twisted.internet.interfaces import IAddress
 from twisted.internet.endpoints import serverFromString
 from twisted.internet.endpoints import clientFromString
 from twisted.internet.endpoints import TCP4ClientEndpoint
-from twisted.internet import error
+# from twisted.internet.endpoints import UNIXClientEndpoint
+# from twisted.internet import error
 from twisted.plugin import IPlugin
 from twisted.python.util import FancyEqMixin
 
 from zope.interface import implementer
 from zope.interface import Interface, Attribute
 
-from txsocksx.client import SOCKS5ClientEndpoint
-
 from .torconfig import TorConfig, launch_tor, HiddenService
-from .torstate import build_tor_connection
+from .torconfig import _endpoint_from_socksport_line
+from .util import SingleObserver
 
 
 _global_tor_config = None
@@ -188,7 +172,7 @@ class TCPHiddenServiceEndpoint(object):
 
 
     :ivar hiddenServiceDir: the data directory, either passed in or created
-        with ``tempfile.mkstemp``
+        with ``tempfile.mkdtemp``
 
     """
 
@@ -208,14 +192,11 @@ class TCPHiddenServiceEndpoint(object):
             only have Group access. XXX FIXME re-test
         """
 
-        @defer.inlineCallbacks
-        def _connect():
-            tor_protocol = yield build_tor_connection(control_endpoint,
-                                                      build_state=False)
-            config = TorConfig(tor_protocol)
-            yield config.post_bootstrap
-            defer.returnValue(config)
-        return TCPHiddenServiceEndpoint(reactor, _connect(), public_port,
+        from txtorcon.controller import connect
+        tor = connect(reactor, control_endpoint)
+        tor.addCallback(lambda t: t.get_config())
+        # tor is a Deferred
+        return TCPHiddenServiceEndpoint(reactor, tor, public_port,
                                         hidden_service_dir=hidden_service_dir,
                                         local_port=local_port)
 
@@ -235,7 +216,7 @@ class TCPHiddenServiceEndpoint(object):
         unless you have a specific need to.
 
         You can also access this global txtorcon instance via
-        :method:`txtorcon.get_global_tor` (which is precisely what
+        :meth:`txtorcon.get_global_tor` (which is precisely what
         this method uses to get it).
 
         All keyword options have defaults (e.g. random ports, or
@@ -299,11 +280,7 @@ class TCPHiddenServiceEndpoint(object):
             :api:`twisted.internet.interfaces.IReactorTCP` provider
 
         :param config:
-            :class:`txtorcon.TorConfig` instance (doesn't need to be
-            bootstrapped) or a Deferred. Note that ``save()`` will be
-            called on this at least once. FIXME should I just accept a
-            TorControlProtocol instance instead, and create my own
-            TorConfig?
+            :class:`txtorcon.TorConfig` instance.
 
         :param public_port:
             The port number we will advertise in the hidden serivces
@@ -316,7 +293,7 @@ class TCPHiddenServiceEndpoint(object):
         :param hidden_service_dir:
             If not None, point to a HiddenServiceDir directory
             (i.e. with "hostname" and "private_key" files in it). If
-            not provided, one is created with temp.mkstemp() AND
+            not provided, one is created with temp.mkdtemp() AND
             DELETED when the reactor shuts down.
 
         :param stealth_auth:
@@ -346,7 +323,7 @@ class TCPHiddenServiceEndpoint(object):
             self.hidden_service_dir = tempfile.mkdtemp(prefix='tortmp')
             log.msg('Will delete "%s" at shutdown.' % self.hidden_service_dir)
             delete = functools.partial(shutil.rmtree, self.hidden_service_dir)
-            reactor.addSystemEventTrigger('before', 'shutdown', delete)
+            self.reactor.addSystemEventTrigger('before', 'shutdown', delete)
 
     @property
     def onion_uri(self):
@@ -372,12 +349,16 @@ class TCPHiddenServiceEndpoint(object):
 
     def _tor_progress_update(self, prog, tag, summary):
         log.msg('%d%% %s' % (prog, summary))
+        # we re-adjust the percentage-scale, using 105% and 110% for
+        # the two parts of waiting for descriptor upload. That is, we
+        # want: 110 * constant == 100.0
         for p in self.progress_listeners:
-            p(prog, tag, summary)
+            p(prog * (100.0 / 110.0), tag, summary)
 
     @defer.inlineCallbacks
     def listen(self, protocolfactory):
-        """Implement :api:`twisted.internet.interfaces.IStreamServerEndpoint
+        """
+        Implement :api:`twisted.internet.interfaces.IStreamServerEndpoint
         <IStreamServerEndpoint>`.
 
         Returns a Deferred that delivers an
@@ -597,7 +578,7 @@ class TCPHiddenServiceEndpointParser(object):
     the OS.
 
     If ``hiddenServiceDir`` is not specified, one is created with
-    ``tempfile.mkstemp()``. The IStreamServerEndpoint returned will be
+    ``tempfile.mkdtemp()``. The IStreamServerEndpoint returned will be
     an instance of :class:`txtorcon.TCPHiddenServiceEndpoint`
     """
     prefix = "onion"
@@ -641,49 +622,135 @@ class TCPHiddenServiceEndpointParser(object):
                                                    control_port=controlPort)
 
 
+ at defer.inlineCallbacks
+def _create_socks_endpoint(reactor, control_protocol, socks_config=None):
+    """
+    Internal helper.
+
+    This uses an already-configured SOCKS endpoint from the attached
+    Tor, or creates a new TCP one (and configures Tor with it). If
+    socks_config is non-None, it is a SOCKSPort line and will either
+    be used if it already exists or will be created.
+    """
+    socks_ports = yield control_protocol.get_conf('SOCKSPort')
+    if socks_ports:
+        socks_ports = list(socks_ports.values())[0]
+        if not isinstance(socks_ports, list):
+            socks_ports = [socks_ports]
+    else:
+        # return from get_conf was an empty dict; we want a list
+        socks_ports = []
+
+    # everything in the SocksPort list can include "options" after the
+    # initial value. We don't care about those, but do need to strip
+    # them.
+    socks_ports = [port.split()[0] for port in socks_ports]
+
+    # could check platform? but why would you have unix ports on a
+    # platform that doesn't?
+    unix_ports = set([p.startswith('unix:') for p in socks_ports])
+    tcp_ports = set(socks_ports) - unix_ports
+
+    socks_endpoint = None
+    for p in list(unix_ports) + list(tcp_ports):  # prefer unix-ports
+        if socks_config and p != socks_config:
+            continue
+        try:
+            socks_endpoint = _endpoint_from_socksport_line(reactor, p)
+        except Exception as e:
+            log.msg("clientFromString('{}') failed: {}".format(p, e))
+
+    # if we still don't have an endpoint, nothing worked (or there
+    # were no SOCKSPort lines at all) so we add config to tor
+    if socks_endpoint is None:
+        if socks_config is None:
+            # is a unix-socket in /tmp on a supported platform better than
+            # this?
+            port = yield available_tcp_port(reactor)
+            socks_config = str(port)
+        socks_ports.append(socks_config)
+
+        # NOTE! We must set all the ports in one command or we'll
+        # destroy pre-existing config
+        args = []
+        for p in socks_ports:
+            args.append('SOCKSPort')
+            args.append(p)
+        yield control_protocol.set_conf(*args)
+        socks_endpoint = _endpoint_from_socksport_line(reactor, socks_config)
+
+    defer.returnValue(socks_endpoint)
+
+
 @implementer(IStreamClientEndpoint)
 class TorClientEndpoint(object):
     """
-    I am an endpoint class who attempts to establish a SOCKS5
-    connection with the system tor process. If no socks_endpoint is
-    given, I will try TCP4 to localhost on ports 9050 then 9150.
+    An IStreamClientEndpoint which establishes a connection via Tor.
+
+    You should not instantiate these directly; use
+    ``clientFromString()``, :meth:`txtorcon.Tor.stream_via` or
+    :meth:`txtorcon.Circuit.stream_via`
 
-    :param socks_endpoint:
-        An IStreamClientEndpoint that will connect to a SOCKS5
-        port. Tor can speak SOCKS5 over either TCP4 or Unix sockets.
+    :param host:
+        The hostname to connect to. This of course can be a Tor Hidden
+        Service onion address.
 
-    :param tls:
-        If True, we will attemp TLS negotiation after the SOCKS forwarding
-        is set up.
+    :param port: The tcp port or Tor Hidden Service port.
+
+    :param socks_endpoint: An IStreamClientEndpoint pointing at (one
+        of) our Tor's SOCKS ports. These can be instantiated with
+        :meth:`txtorcon.TorConfig.socks_endpoint`.
+
+    :param tls: Can be False or True (to get default Browser-like
+        hostname verification) or the result of calling
+        optionsForClientTLS() yourself. Default is True.
     """
-    # XXX should get these via the control connection, i.e. ask Tor
-    # via GETINFO net/listeners/socks or whatever
+
     socks_ports_to_try = [9050, 9150]
 
-    def __init__(self, host, port,
-                 socks_endpoint=None,
+    @classmethod
+    def from_connection(cls, reactor, control_protocol, host, port,
+                        tls=None,
+                        socks_endpoint=None):
+        if socks_endpoint is None:
+            socks_endpoint = _create_socks_endpoint(reactor, control_protocol)
+        return TorClientEndpoint(
+            host, port,
+            socks_endpoint=socks_endpoint,
+            tls=tls,
+            reactor=reactor,
+        )
+
+    def __init__(self,
+                 host, port,
+                 socks_endpoint=None,  # can be Deferred
+                 tls=False,
+
+                 # XXX our custom SOCKS stuff doesn't support auth (yet?)
                  socks_username=None, socks_password=None,
-                 tls=False, **kw):
+                 reactor=None, **kw):
         if host is None or port is None:
             raise ValueError('host and port must be specified')
 
         self.host = host
         self.port = int(port)
-        self.socks_endpoint = socks_endpoint
-        self.socks_username = socks_username
-        self.socks_password = socks_password
-        self.tls = tls
-
-        if self.tls and not _HAVE_TLS:
-            raise ValueError(
-                "'tls=True' but we don't have TLS support"
-            )
+        self._socks_endpoint = socks_endpoint
+        self._socks_username = socks_username
+        self._socks_password = socks_password
+        self._tls = tls
+        # XXX FIXME we 'should' probably include 'reactor' as the
+        # first arg to this class, but technically that's a
+        # breaking change :(
+        self._reactor = reactor
+        if reactor is None:
+            from twisted.internet import reactor
+            self._reactor = reactor
 
         # backwards-compatibility: you used to specify a TCP SOCKS
-        # endpoint via socks_hostname= and socks_port= kwargs
-        if self.socks_endpoint is None:
+        # endpoint via socks_host= and socks_port= kwargs
+        if self._socks_endpoint is None:
             try:
-                self.socks_endpoint = TCP4ClientEndpoint(
+                self._socks_endpoint = TCP4ClientEndpoint(
                     reactor,
                     kw['socks_hostname'],
                     kw['socks_port'],
@@ -696,42 +763,61 @@ class TorClientEndpoint(object):
         # was None but the user specified the (old)
         # socks_hostname/socks_port (in which case we do NOT want
         # guessing_enabled
-        if self.socks_endpoint is None:
+        if self._socks_endpoint is None:
             self._socks_port_iter = iter(self.socks_ports_to_try)
             self._socks_guessing_enabled = True
         else:
             self._socks_guessing_enabled = False
 
+        # XXX think, do we want to expose these like this? Or some
+        # other way (because they're for stream-isolation, not actual
+        # auth)
+        self._socks_username = socks_username
+        self._socks_password = socks_password
+        self._when_address = SingleObserver()
+
+    def _get_address(self):
+        """
+        internal helper.
+
+        *le sigh*. This is basically just to support
+        TorCircuitEndpoint; see TorSocksEndpoint._get_address(). There
+        shouldn't be any need for "actual users" to need this!
+
+        This returns a Deferred that fires once:
+          - we have an underlying SOCKS5 endpoint
+          - ...and it has received a local connection (and hence the address/port)
+        """
+        return self._when_address.when_fired()
+
     @defer.inlineCallbacks
     def connect(self, protocolfactory):
         last_error = None
-        kwargs = dict()
-        if self.socks_username is not None and self.socks_password is not None:
-            kwargs['methods'] = dict(
-                login=(self.socks_username, self.socks_password),
+        # XXX fix in socks.py stuff for socks_username, socks_password
+        if self._socks_username or self._socks_password:
+            raise RuntimeError(
+                "txtorcon socks support doesn't yet do username/password"
             )
-        if self.socks_endpoint is not None:
-            args = (self.host, self.port, self.socks_endpoint)
-            socks_ep = SOCKS5ClientEndpoint(*args, **kwargs)
-            if self.tls:
-                context = optionsForClientTLS(unicode(self.host))
-                socks_ep = TLSWrapClientEndpoint(context, socks_ep)
+        if self._socks_endpoint is not None:
+            socks_ep = TorSocksEndpoint(
+                self._socks_endpoint,
+                self.host, self.port,
+                self._tls,
+            )
+            # forward the address to any listeners we have
+            socks_ep._get_address().addCallback(self._when_address.fire)
             proto = yield socks_ep.connect(protocolfactory)
             defer.returnValue(proto)
         else:
             for socks_port in self._socks_port_iter:
                 tor_ep = TCP4ClientEndpoint(
-                    reactor,
-                    "127.0.0.1",
+                    self._reactor,
+                    "127.0.0.1",  # XXX socks_hostname, no?
                     socks_port,
                 )
-                args = (self.host, self.port, tor_ep)
-                socks_ep = SOCKS5ClientEndpoint(*args, **kwargs)
-                if self.tls:
-                    # XXX only twisted 14+
-                    context = optionsForClientTLS(unicode(self.host))
-                    socks_ep = TLSWrapClientEndpoint(context, socks_ep)
-
+                socks_ep = TorSocksEndpoint(tor_ep, self.host, self.port, self._tls)
+                # forward the address to any listeners we have
+                socks_ep._get_address().addCallback(self._when_address.fire)
                 try:
                     proto = yield socks_ep.connect(protocolfactory)
                     defer.returnValue(proto)
@@ -763,8 +849,7 @@ class TorClientEndpointStringParser(object):
     ``tor:host=torproject.org:port=443:socksUsername=foo:socksPassword=bar``
 
     If ``socksPort`` is specified, it means only use that port to
-    attempt to proxy through Tor. If unspecified then try some likely
-    socksPorts such as [9050, 9150].
+    attempt to proxy through Tor. If unspecified, we ... XXX?
 
     NOTE that I'm using camelCase variable names in the endpoint
     string to be consistent with the rest of Twisted's naming (and
@@ -776,19 +861,18 @@ class TorClientEndpointStringParser(object):
     """
     prefix = "tor"
 
-    def _parseClient(self, host=None, port=None,
+    def _parseClient(self, reactor,
+                     host=None, port=None,
                      socksHostname=None, socksPort=None,
                      socksUsername=None, socksPassword=None):
         if port is not None:
             port = int(port)
-        if socksHostname is None:
-            socksHostname = '127.0.0.1'
-        if socksPort is not None:
-            socksPort = int(socksPort)
 
         ep = None
         if socksPort is not None:
-            ep = TCP4ClientEndpoint(reactor, socksHostname, socksPort)
+            # Tor can speak SOCKS over unix, too, but this doesn't let
+            # us pass one ...
+            ep = TCP4ClientEndpoint(reactor, socksHostname, int(socksPort))
         return TorClientEndpoint(
             host, port,
             socks_endpoint=ep,
@@ -799,6 +883,4 @@ class TorClientEndpointStringParser(object):
     def parseStreamClient(self, *args, **kwargs):
         # for Twisted 14 and 15 (and more) the first argument is
         # 'reactor', for older Twisteds it's not
-        if _HAVE_TX_14:
-            return self._parseClient(*args[1:], **kwargs)
         return self._parseClient(*args, **kwargs)
diff --git a/txtorcon/interface.py b/txtorcon/interface.py
index 17592ec..3dbe424 100644
--- a/txtorcon/interface.py
+++ b/txtorcon/interface.py
@@ -9,6 +9,92 @@ from zope.interface import implementer
 from zope.interface import Interface, Attribute
 
 
+class ITor(Interface):
+    """
+    Represents a tor instance. This high-level API should provide all
+    objects you need to interact with tor.
+    """
+
+    process = Attribute("TorProcessProtocol instance if we launched this Tor")
+    protocol = Attribute("A TorControlProtocol connected to our Tor")
+    version = Attribute("The version of the Tor we're connected to")
+
+    def quit(self):
+        """
+        Closes the control connection, and if we launched this Tor
+        instance we'll send it a TERM and wait until it exits.
+
+        :return: a Deferred that fires when we've quit
+        """
+
+    def get_config(self):
+        """
+        :return: a Deferred that fires with a TorConfig instance. This
+            instance represents up-to-date configuration of the tor
+            instance (even if another controller is connected). If you
+            call this more than once you'll get the same TorConfig back.
+        """
+
+    def create_state(self):
+        """
+        returns a Deferred that fires with a ready-to-go
+        :class:`txtorcon.TorState` instance.
+        """
+
+    def web_agent(self, pool=None, _socks_endpoint=None):
+        """
+        :param _socks_endpoint: If ``None`` (the default), a suitable
+            SOCKS port is chosen from our config (or added). If supplied,
+            should be a Deferred which fires an IStreamClientEndpoint
+            (e.g. the return-value from
+            :meth:`txtorcon.TorConfig.socks_endpoint`) or an immediate
+            IStreamClientEndpoint You probably don't need to mess with
+            this.
+
+        :param pool: passed on to the Agent (as ``pool=``)
+        """
+
+    def dns_resolve(self, hostname):
+        """
+        :param hostname: a string
+
+        :returns: a Deferred that calbacks with the hostname as looked-up
+            via Tor (or errback).  This uses Tor's custom extension to the
+            SOCKS5 protocol.
+        """
+
+    def dns_resolve_ptr(self, ip):
+        """
+        :param ip: a string, like "127.0.0.1"
+
+        :returns: a Deferred that calbacks with the IP address as
+            looked-up via Tor (or errback).  This uses Tor's custom
+            extension to the SOCKS5 protocol.
+        """
+
+    def stream_via(self, host, port, tls=False, _socks_endpoint=None):
+        """
+        This returns an IStreamClientEndpoint instance that will use this
+        Tor (via SOCKS) to visit the ``(host, port)`` indicated.
+
+        :param host: The host to connect to. You MUST pass host-names
+            to this. If you absolutely know that you've not leaked DNS
+            (e.g. you save IPs in your app's configuration or similar)
+            then you can pass an IP.
+
+        :param port: Port to connect to.
+
+        :param tls: If True, it will wrap the return endpoint in one
+            that does TLS (default: False).
+
+        :param _socks_endpoint: Normally not needed (default: None)
+            but you can pass an IStreamClientEndpoint_ directed at one
+            of the local Tor's SOCKS5 ports (e.g. created with
+            :meth:`txtorcon.TorConfig.create_socks_endpoint`). Can be
+            a Deferred.
+        """
+
+
 class IStreamListener(Interface):
     """
     Notifications about changes to a :class:`txtorcon.Stream`.
@@ -95,8 +181,23 @@ class IStreamAttacher(Interface):
     Each time a new :class:`txtorcon.Stream` is created, this
     interface will be queried to find out which
     :class:`txtorcon.Circuit` it should be attached to.
+
+    Only advanced use-cases should need to use this directly; for most
+    users, using the :func:`txtorcon.Circuit.stream_via` interface
+    should be preferred.
     """
 
+    def attach_stream_failure(stream, fail):
+        """
+        :param stream:
+            The stream we were trying to attach.
+
+        :param fail:
+            A Failure instance.
+
+        A failure has occurred while trying to attach the stream.
+        """
+
     def attach_stream(stream, circuits):
         """
         :param stream:
@@ -115,9 +216,6 @@ class IStreamAttacher(Interface):
         the callback from :meth:`txtorcon.TorState.build_circuit` does
         not wait for the circuit to be in BUILT state.
 
-        See :ref:`attach_streams_by_country.py` for a complete
-        example of using a Deferred in an IStreamAttacher.
-
         Alternatively, you may return None in which case the Tor
         controller will be told to choose a circuit itself.
 
diff --git a/txtorcon/router.py b/txtorcon/router.py
index cb4945f..03b6163 100644
--- a/txtorcon/router.py
+++ b/txtorcon/router.py
@@ -5,17 +5,26 @@ from __future__ import print_function
 from __future__ import unicode_literals
 from __future__ import with_statement
 
+import json
 from datetime import datetime
 from .util import NetLocation
-from .util import basestring
+import six
+from base64 import b64encode, b64decode
+from binascii import b2a_hex, a2b_hex
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.web.client import readBody
 
 
 def hexIdFromHash(thehash):
     """
     From the base-64 encoded hashes Tor uses, this produces the longer
     hex-encoded hashes.
+
+    :param thehash: base64-encoded str
+    :return: hex-encoded hash
     """
-    return "$" + (thehash + "=").decode("base64").encode("hex").upper()
+    return '$' + b2a_hex(b64decode(thehash + '=')).decode('ascii').upper()
 
 
 def hashFromHexId(hexid):
@@ -24,7 +33,7 @@ def hashFromHexId(hexid):
     """
     if hexid[0] == '$':
         hexid = hexid[1:]
-    return hexid.decode("hex").encode("base64")[:-2]
+    return b64encode(a2b_hex(hexid))[:-1].decode('ascii')
 
 
 class PortRange(object):
@@ -35,10 +44,8 @@ class PortRange(object):
         self.min = a
         self.max = b
 
-    def __cmp__(self, b):
-        if b >= self.min and b <= self.max:
-            return 0
-        return 1
+    def __eq__(self, b):
+        return b >= self.min and b <= self.max
 
     def __str__(self):
         return "%d-%d" % (self.min, self.max)
@@ -79,6 +86,14 @@ class Router(object):
 
     @property
     def modified(self):
+        """
+        This is the time of 'the publication time of its most recent
+        descriptor' (in UTC).
+
+        See also dir-spec.txt.
+
+        """
+        # "... in the form YYYY-MM-DD HH:MM:SS, in UTC"
         if self._modified is None:
             self._modified = datetime.strptime(
                 self._modified_unparsed,
@@ -99,6 +114,9 @@ class Router(object):
         self._location = None
 
         self.id_hex = hexIdFromHash(self.id_hash)
+        # for py3, these should be valid (but *not* py2)
+        # assert type(idhash) is not bytes
+        # assert type(orhash) is not bytes
 
     @property
     def location(self):
@@ -139,7 +157,7 @@ class Router(object):
         There is some current work in Twisted for open-ended constants
         (enums) support however, it seems.
         """
-        if isinstance(flags, basestring):
+        if isinstance(flags, (six.text_type, bytes)):
             flags = flags.split()
         self._flags = [x.lower() for x in flags]
         self.name_is_unique = 'named' in self._flags
@@ -153,6 +171,41 @@ class Router(object):
     def bandwidth(self, bw):
         self._bandwidth = int(bw)
 
+    @inlineCallbacks
+    def get_onionoo_details(self, agent):
+        """
+        Requests the 'details' document from onionoo.torproject.org via
+        the given `twisted.web.iweb.IAgent` -- you can get a suitable
+        instance to pass here by calling either :meth:`txtorcon.Tor.web_agent` or
+        :meth:`txtorcon.Circuit.web_agent`.
+        """
+
+        uri = 'https://onionoo.torproject.org/details?lookup={}'.format(self.id_hex[1:]).encode('ascii')
+
+        resp = yield agent.request(b'GET', uri)
+        if resp.code != 200:
+            raise RuntimeError(
+                'Failed to lookup relay details for {}'.format(self.id_hex)
+            )
+        body = yield readBody(resp)
+        data = json.loads(body.decode('ascii'))
+        if len(data['relays']) != 1:
+            raise RuntimeError(
+                'Got multiple relays for {}'.format(self.id_hex)
+            )
+        relay_data = data['relays'][0]
+        if relay_data['fingerprint'].lower() != self.id_hex[1:].lower():
+            raise RuntimeError(
+                'Expected "{}" but got data for "{}"'.format(self.id_hex, relay_data['fingerprint'])
+            )
+        returnValue(relay_data)
+
+    # note that exit-policy is no longer included in the
+    # microdescriptors by default, so this stuff is mostly here as a
+    # historic artifact. If you want to use exit-policy for things
+    # your best bet is to tell your tor to download full descriptors
+    # (SETCONF UseMicrodescriptors 0) instead.
+
     @property
     def policy(self):
         """
diff --git a/txtorcon/socks.py b/txtorcon/socks.py
new file mode 100644
index 0000000..f9b4581
--- /dev/null
+++ b/txtorcon/socks.py
@@ -0,0 +1,744 @@
+# in-progress; implementing SOCKS5 client-side stuff as extended by
+# tor because txsocksx will not be getting Python3 support any time
+# soon, and its underlying dependency (Parsely) also doesn't support
+# Python3. Also, Tor's SOCKS5 implementation is especially simple,
+# since it doesn't do BIND or UDP ASSOCIATE.
+
+from __future__ import print_function
+
+import six
+import struct
+from socket import inet_pton, inet_ntoa, inet_aton, AF_INET6, AF_INET
+
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet.address import IPv4Address, IPv6Address, HostnameAddress
+from twisted.python.failure import Failure
+from twisted.protocols import portforward
+from twisted.protocols import tls
+from twisted.internet.interfaces import IStreamClientEndpoint
+from zope.interface import implementer
+import ipaddress
+import automat
+
+from txtorcon import util
+
+
+__all__ = (
+    'resolve',
+    'resolve_ptr',
+    'SocksError',
+    'GeneralServerFailureError',
+    'ConnectionNotAllowedError',
+    'NetworkUnreachableError',
+    'HostUnreachableError',
+    'ConnectionRefusedError',
+    'TtlExpiredError',
+    'CommandNotSupportedError',
+    'AddressTypeNotSupportedError',
+    'TorSocksEndpoint',
+)
+
+
+def _create_ip_address(host, port):
+    if not isinstance(host, six.text_type):
+        raise ValueError(
+            "'host' must be {}, not {}".format(six.text_type, type(host))
+        )
+    try:
+        a = ipaddress.ip_address(host)
+    except ValueError:
+        a = None
+    if isinstance(a, ipaddress.IPv4Address):
+        return IPv4Address('TCP', host, port)
+    if isinstance(a, ipaddress.IPv6Address):
+        return IPv6Address('TCP', host, port)
+    addr = HostnameAddress(host, port)
+    addr.host = host
+    return addr
+
+
+class _SocksMachine(object):
+    """
+    trying to prototype the SOCKS state-machine in automat
+
+    This is a SOCKS state machine to make a single request.
+    """
+
+    _machine = automat.MethodicalMachine()
+    SUCCEEDED = 0x00
+    REPLY_IPV4 = 0x01
+    REPLY_HOST = 0x03
+    REPLY_IPV6 = 0x04
+
+    # XXX address = (host, port) instead
+    def __init__(self, req_type, host,
+                 port=0,
+                 on_disconnect=None,
+                 on_data=None,
+                 create_connection=None):
+        if req_type not in self._dispatch:
+            raise ValueError(
+                "Unknown request type '{}'".format(req_type)
+            )
+        if req_type == 'CONNECT' and create_connection is None:
+            raise ValueError(
+                "create_connection function required for '{}'".format(
+                    req_type
+                )
+            )
+        if not isinstance(host, (bytes, str, six.text_type)):
+            raise ValueError(
+                "'host' must be text".format(type(host))
+            )
+        # XXX what if addr is None?
+        self._req_type = req_type
+        self._addr = _create_ip_address(six.text_type(host), port)
+        self._data = b''
+        self._on_disconnect = on_disconnect
+        self._create_connection = create_connection
+        # XXX FIXME do *one* of these:
+        self._on_data = on_data
+        self._outgoing_data = []
+        # the other side of our proxy
+        self._sender = None
+        self._when_done = util.SingleObserver()
+
+    def when_done(self):
+        """
+        Returns a Deferred that fires when we're done
+        """
+        return self._when_done.when_fired()
+
+    def _data_to_send(self, data):
+        if self._on_data:
+            self._on_data(data)
+        else:
+            self._outgoing_data.append(data)
+
+    def send_data(self, callback):
+        """
+        drain all pending data by calling `callback()` on it
+        """
+        # a "for x in self._outgoing_data" would potentially be more
+        # efficient, but then there's no good way to bubble exceptions
+        # from callback() out without lying about how much data we
+        # processed .. or eat the exceptions in here.
+        while len(self._outgoing_data):
+            data = self._outgoing_data.pop(0)
+            callback(data)
+
+    def feed_data(self, data):
+        # I feel like maybe i'm doing all this buffering-stuff
+        # wrong. but I also don't want a bunch of "received 1 byte"
+        # etc states hanging off everything that can "get data"
+        self._data += data
+        self.got_data()
+
+    @_machine.output()
+    def _parse_version_reply(self):
+        "waiting for a version reply"
+        if len(self._data) >= 2:
+            reply = self._data[:2]
+            self._data = self._data[2:]
+            (version, method) = struct.unpack('BB', reply)
+            if version == 5 and method in [0x00, 0x02]:
+                self.version_reply(method)
+            else:
+                if version != 5:
+                    self.version_error(SocksError(
+                        "Expected version 5, got {}".format(version)))
+                else:
+                    self.version_error(SocksError(
+                        "Wanted method 0 or 2, got {}".format(method)))
+
+    def _parse_ipv4_reply(self):
+        if len(self._data) >= 10:
+            addr = inet_ntoa(self._data[4:8])
+            port = struct.unpack('H', self._data[8:10])[0]
+            self._data = self._data[10:]
+            if self._req_type == 'CONNECT':
+                self.reply_ipv4(addr, port)
+            else:
+                self.reply_domain_name(addr)
+
+    def _parse_ipv6_reply(self):
+        if len(self._data) >= 22:
+            addr = self._data[4:20]
+            port = struct.unpack('H', self._data[20:22])[0]
+            self._data = self._data[22:]
+            self.reply_ipv6(addr, port)
+
+    def _parse_domain_name_reply(self):
+        assert len(self._data) >= 8  # _parse_request_reply checks this
+        addrlen = struct.unpack('B', self._data[4:5])[0]
+        # may simply not have received enough data yet...
+        if len(self._data) < (5 + addrlen + 2):
+            return
+        addr = self._data[5:5 + addrlen]
+        # port = struct.unpack('H', self._data[5 + addrlen:5 + addrlen + 2])[0]
+        self._data = self._data[5 + addrlen + 2:]
+        self.reply_domain_name(addr)
+
+    @_machine.output()
+    def _parse_request_reply(self):
+        "waiting for a reply to our request"
+        # we need at least 6 bytes of data: 4 for the "header", such
+        # as it is, and 2 more if it's DOMAINNAME (for the size) or 4
+        # or 16 more if it's an IPv4/6 address reply. plus there's 2
+        # bytes on the end for the bound port.
+        if len(self._data) < 8:
+            return
+        msg = self._data[:4]
+
+        # not changing self._data yet, in case we've not got
+        # enough bytes so far.
+        (version, reply, _, typ) = struct.unpack('BBBB', msg)
+
+        if version != 5:
+            self.reply_error(SocksError(
+                "Expected version 5, got {}".format(version)))
+            return
+
+        if reply != self.SUCCEEDED:
+            self.reply_error(_create_socks_error(reply))
+            return
+
+        reply_dispatcher = {
+            self.REPLY_IPV4: self._parse_ipv4_reply,
+            self.REPLY_HOST: self._parse_domain_name_reply,
+            self.REPLY_IPV6: self._parse_ipv6_reply,
+        }
+        try:
+            method = reply_dispatcher[typ]
+        except KeyError:
+            self.reply_error(SocksError(
+                "Unexpected response type {}".format(typ)))
+            return
+        method()
+
+    @_machine.output()
+    def _make_connection(self, addr, port):
+        "make our proxy connection"
+        sender = self._create_connection(addr, port)
+        # XXX look out! we're depending on this "sender" implementing
+        # certain Twisted APIs, and the state-machine shouldn't depend
+        # on that.
+
+        # XXX also, if sender implements producer/consumer stuff, we
+        # should register ourselves (and implement it to) -- but this
+        # should really be taking place outside the state-machine in
+        # "the I/O-doing" stuff
+        self._sender = sender
+        self._when_done.fire(sender)
+
+    @_machine.output()
+    def _domain_name_resolved(self, domain):
+        self._when_done.fire(domain)
+
+    @_machine.input()
+    def connection(self):
+        "begin the protocol (i.e. connection made)"
+
+    @_machine.input()
+    def disconnected(self, error):
+        "the connection has gone away"
+
+    @_machine.input()
+    def got_data(self):
+        "we recevied some data and buffered it"
+
+    @_machine.input()
+    def version_reply(self, auth_method):
+        "the SOCKS server replied with a version"
+
+    @_machine.input()
+    def version_error(self, error):
+        "the SOCKS server replied, but we don't understand"
+
+    @_machine.input()
+    def reply_error(self, error):
+        "the SOCKS server replied with an error"
+
+    @_machine.input()
+    def reply_ipv4(self, addr, port):
+        "the SOCKS server told me an IPv4 addr, port"
+
+    @_machine.input()
+    def reply_ipv6(self, addr, port):
+        "the SOCKS server told me an IPv6 addr, port"
+
+    @_machine.input()
+    def reply_domain_name(self, domain):
+        "the SOCKS server told me a domain-name"
+
+    @_machine.input()
+    def answer(self):
+        "the SOCKS server replied with an answer"
+
+    @_machine.output()
+    def _send_version(self):
+        "sends a SOCKS version reply"
+        self._data_to_send(
+            # for anonymous(0) *and* authenticated (2): struct.pack('BBBB', 5, 2, 0, 2)
+            struct.pack('BBB', 5, 1, 0)
+        )
+
+    @_machine.output()
+    def _disconnect(self, error):
+        "done"
+        if self._on_disconnect:
+            self._on_disconnect(str(error))
+        if self._sender:
+            self._sender.connectionLost(Failure(error))
+        self._when_done.fire(Failure(error))
+
+    @_machine.output()
+    def _send_request(self, auth_method):
+        "send the request (connect, resolve or resolve_ptr)"
+        assert auth_method == 0x00  # "no authentication required"
+        return self._dispatch[self._req_type](self)
+
+    @_machine.output()
+    def _relay_data(self):
+        "relay any data we have"
+        if self._data:
+            d = self._data
+            self._data = b''
+            # XXX this is "doing I/O" in the state-machine and it
+            # really shouldn't be ... probably want a passed-in
+            # "relay_data" callback or similar?
+            self._sender.dataReceived(d)
+
+    def _send_connect_request(self):
+        "sends CONNECT request"
+        # XXX needs to support v6 ... or something else does
+        host = self._addr.host
+        port = self._addr.port
+
+        if isinstance(self._addr, (IPv4Address, IPv6Address)):
+            is_v6 = isinstance(self._addr, IPv6Address)
+            self._data_to_send(
+                struct.pack(
+                    '!BBBB4sH',
+                    5,                   # version
+                    0x01,                # command
+                    0x00,                # reserved
+                    0x04 if is_v6 else 0x01,
+                    inet_pton(AF_INET6 if is_v6 else AF_INET, host),
+                    port,
+                )
+            )
+        else:
+            host = host.encode('ascii')
+            self._data_to_send(
+                struct.pack(
+                    '!BBBBB{}sH'.format(len(host)),
+                    5,                   # version
+                    0x01,                # command
+                    0x00,                # reserved
+                    0x03,
+                    len(host),
+                    host,
+                    port,
+                )
+            )
+
+    @_machine.output()
+    def _send_resolve_request(self):
+        "sends RESOLVE_PTR request (Tor custom)"
+        host = self._addr.host.encode()
+        self._data_to_send(
+            struct.pack(
+                '!BBBBB{}sH'.format(len(host)),
+                5,                   # version
+                0xF0,                # command
+                0x00,                # reserved
+                0x03,                # DOMAINNAME
+                len(host),
+                host,
+                0,  # self._addr.port?
+            )
+        )
+
+    @_machine.output()
+    def _send_resolve_ptr_request(self):
+        "sends RESOLVE_PTR request (Tor custom)"
+        addr_type = 0x04 if isinstance(self._addr, ipaddress.IPv4Address) else 0x01
+        encoded_host = inet_aton(self._addr.host)
+        self._data_to_send(
+            struct.pack(
+                '!BBBB4sH',
+                5,                   # version
+                0xF1,                # command
+                0x00,                # reserved
+                addr_type,
+                encoded_host,
+                0,                   # port; unused? SOCKS is fun
+            )
+        )
+
+    @_machine.state(initial=True)
+    def unconnected(self):
+        "not yet connected"
+
+    @_machine.state()
+    def sent_version(self):
+        "we've sent our version request"
+
+    @_machine.state()
+    def sent_request(self):
+        "we've sent our stream/etc request"
+
+    @_machine.state()
+    def relaying(self):
+        "received our response, now we can relay"
+
+    @_machine.state()
+    def abort(self, error_message):
+        "we've encountered an error"
+
+    @_machine.state()
+    def done(self):
+        "operations complete"
+
+    unconnected.upon(
+        connection,
+        enter=sent_version,
+        outputs=[_send_version],
+    )
+
+    sent_version.upon(
+        got_data,
+        enter=sent_version,
+        outputs=[_parse_version_reply],
+    )
+    sent_version.upon(
+        version_error,
+        enter=abort,
+        outputs=[_disconnect],
+    )
+    sent_version.upon(
+        version_reply,
+        enter=sent_request,
+        outputs=[_send_request],
+    )
+    sent_version.upon(
+        disconnected,
+        enter=unconnected,
+        outputs=[_disconnect]
+    )
+
+    sent_request.upon(
+        got_data,
+        enter=sent_request,
+        outputs=[_parse_request_reply],
+    )
+    sent_request.upon(
+        reply_ipv4,
+        enter=relaying,
+        outputs=[_make_connection],
+    )
+    sent_request.upon(
+        reply_ipv6,
+        enter=relaying,
+        outputs=[_make_connection],
+    )
+    # XXX this isn't always a _domain_name_resolved -- if we're a
+    # req_type CONNECT then it's _make_connection_domain ...
+    sent_request.upon(
+        reply_domain_name,
+        enter=done,
+        outputs=[_domain_name_resolved],
+    )
+    sent_request.upon(
+        reply_error,
+        enter=abort,
+        outputs=[_disconnect],
+    )
+# XXX FIXME this needs a test
+    sent_request.upon(
+        disconnected,
+        enter=abort,
+        outputs=[_disconnect],  # ... or is this redundant?
+    )
+
+    relaying.upon(
+        got_data,
+        enter=relaying,
+        outputs=[_relay_data],
+    )
+    relaying.upon(
+        disconnected,
+        enter=done,
+        outputs=[_disconnect],
+    )
+
+    abort.upon(
+        got_data,
+        enter=abort,
+        outputs=[],
+    )
+    abort.upon(
+        disconnected,
+        enter=abort,
+        outputs=[],
+    )
+
+    done.upon(
+        disconnected,
+        enter=done,
+        outputs=[],
+    )
+
+    _dispatch = {
+        'CONNECT': _send_connect_request,
+        'RESOLVE': _send_resolve_request,
+        'RESOLVE_PTR': _send_resolve_ptr_request,
+    }
+
+
+class _TorSocksProtocol(Protocol):
+
+    def __init__(self, host, port, socks_method, factory):
+        self._machine = _SocksMachine(
+            req_type=socks_method,
+            host=host,  # noqa unicode() on py3, py2? we want idna, actually?
+            port=port,
+            on_disconnect=self._on_disconnect,
+            on_data=self._on_data,
+            create_connection=self._create_connection,
+        )
+        self._factory = factory
+
+    def when_done(self):
+        return self._machine.when_done()
+
+    def connectionMade(self):
+        self._machine.connection()
+        # we notify via the factory that we have teh
+        # locally-connecting host -- this is e.g. used by the "stream
+        # over one particular circuit" code to determine the local
+        # port that "our" SOCKS connection went to
+        self.factory._did_connect(self.transport.getHost())
+
+    def connectionLost(self, reason):
+        self._machine.disconnected(SocksError(reason))
+
+    def dataReceived(self, data):
+        self._machine.feed_data(data)
+
+    def _on_data(self, data):
+        self.transport.write(data)
+
+    def _create_connection(self, addr, port):
+        addr = IPv4Address('TCP', addr, port)
+        sender = self._factory.buildProtocol(addr)
+        client_proxy = portforward.ProxyClient()
+        sender.makeConnection(self.transport)
+        # portforward.ProxyClient is going to call setPeer but this
+        # probably doesn't have it...
+        setattr(sender, 'setPeer', lambda _: None)
+        client_proxy.setPeer(sender)
+        self._sender = sender
+        return sender
+
+    def _on_disconnect(self, error_message):
+        self.transport.loseConnection()
+        # self.transport.abortConnection()#SocksError(error_message)) ?
+
+
+class _TorSocksFactory(Factory):
+    protocol = _TorSocksProtocol
+
+    def __init__(self, *args, **kw):
+        self._args = args
+        self._kw = kw
+        self._host = None
+        self._when_connected = util.SingleObserver()
+
+    def _get_address(self):
+        """
+        Returns a Deferred that fires with the transport's getHost()
+        when this SOCKS protocol becomes connected.
+        """
+        return self._when_connected.when_fired()
+
+    def _did_connect(self, host):
+        self._host = host
+        self._when_connected.fire(host)
+
+    def buildProtocol(self, addr):
+        p = self.protocol(*self._args, **self._kw)
+        p.factory = self
+        return p
+
+
+class SocksError(Exception):
+    code = None
+    message = ''
+
+    def __init__(self, message='', code=None):
+        super(SocksError, self).__init__(message or self.message)
+        self.message = message or self.message
+        self.code = code or self.code
+
+
+class GeneralServerFailureError(SocksError):
+    code = 0x01
+    message = 'general SOCKS server failure'
+
+
+class ConnectionNotAllowedError(SocksError):
+    code = 0x02
+    message = 'connection not allowed by ruleset'
+
+
+class NetworkUnreachableError(SocksError):
+    code = 0x03
+    message = 'Network unreachable'
+
+
+class HostUnreachableError(SocksError):
+    code = 0x04
+    message = 'Host unreachable'
+
+
+class ConnectionRefusedError(SocksError):
+    code = 0x05
+    message = 'Connection refused'
+
+
+class TtlExpiredError(SocksError):
+    code = 0x06
+    message = 'TTL expired'
+
+
+class CommandNotSupportedError(SocksError):
+    code = 0x07
+    message = 'Command not supported'
+
+
+class AddressTypeNotSupportedError(SocksError):
+    code = 0x08
+    message = 'Address type not supported'
+
+
+_socks_errors = {cls.code: cls for cls in SocksError.__subclasses__()}
+
+
+def _create_socks_error(code):
+    try:
+        return _socks_errors[code]()
+    except KeyError:
+        return SocksError("Unknown SOCKS error-code {}".format(code),
+                          code=code)
+
+
+ at inlineCallbacks
+def resolve(tor_endpoint, hostname):
+    """
+    This is easier to use via :meth:`txtorcon.Tor.dns_resolve`
+
+    :param tor_endpoint: the Tor SOCKS endpoint to use.
+
+    :param hostname: the hostname to look up.
+    """
+    if six.PY2 and isinstance(hostname, str):
+        hostname = unicode(hostname)  # noqa
+    elif six.PY3 and isinstance(hostname, bytes):
+        hostname = hostname.decode('ascii')
+    factory = _TorSocksFactory(
+        hostname, 0, 'RESOLVE', None,
+    )
+    proto = yield tor_endpoint.connect(factory)
+    result = yield proto.when_done()
+    returnValue(result)
+
+
+ at inlineCallbacks
+def resolve_ptr(tor_endpoint, ip):
+    """
+    This is easier to use via :meth:`txtorcon.Tor.dns_resolve_ptr`
+
+    :param tor_endpoint: the Tor SOCKS endpoint to use.
+
+    :param ip: the IP address to look up.
+    """
+    if six.PY2 and isinstance(ip, str):
+        ip = unicode(ip)  # noqa
+    elif six.PY3 and isinstance(ip, bytes):
+        ip = ip.decode('ascii')
+    factory = _TorSocksFactory(
+        ip, 0, 'RESOLVE_PTR', None,
+    )
+    proto = yield tor_endpoint.connect(factory)
+    result = yield proto.when_done()
+    returnValue(result)
+
+
+ at implementer(IStreamClientEndpoint)
+class TorSocksEndpoint(object):
+    """
+    Represents an endpoint which will talk to a Tor SOCKS port.
+
+    These should usually not be instantiated directly, instead use
+    :meth:`txtorcon.TorConfig.socks_endpoint`.
+    """
+    # XXX host, port args should be (host, port) tuple, or
+    # IAddress-implementer?
+    def __init__(self, socks_endpoint, host, port, tls=False):
+        self._proxy_ep = socks_endpoint  # can be Deferred
+        if six.PY2 and isinstance(host, str):
+            host = unicode(host)  # noqa
+        if six.PY3 and isinstance(host, bytes):
+            host = host.decode('ascii')
+        self._host = host
+        self._port = port
+        self._tls = tls
+        self._socks_factory = None
+        self._when_address = util.SingleObserver()
+
+    def _get_address(self):
+        """
+        Returns a Deferred that fires with the source IAddress of the
+        underlying SOCKS connection (i.e. usually a
+        twisted.internet.address.IPv4Address)
+
+        circuit.py uses this; better suggestions welcome!
+        """
+        return self._when_address.when_fired()
+
+    @inlineCallbacks
+    def connect(self, factory):
+        # further wrap the protocol if we're doing TLS.
+        # "pray i do not wrap the protocol further".
+        if self._tls:
+            # XXX requires Twisted 14+
+            from twisted.internet.ssl import optionsForClientTLS
+            context = optionsForClientTLS(self._host)
+            tls_factory = tls.TLSMemoryBIOFactory(context, True, factory)
+            socks_factory = _TorSocksFactory(
+                self._host, self._port, 'CONNECT', tls_factory,
+            )
+        else:
+            socks_factory = _TorSocksFactory(
+                self._host, self._port, 'CONNECT', factory,
+            )
+
+        self._socks_factory = socks_factory
+        # forward our address (when we get it) to any listeners
+        self._socks_factory._get_address().addBoth(self._when_address.fire)
+        # XXX isn't this just maybeDeferred()
+        if isinstance(self._proxy_ep, Deferred):
+            proxy_ep = yield self._proxy_ep
+        else:
+            proxy_ep = self._proxy_ep
+
+        # socks_proto = yield proxy_ep.connect(socks_factory)
+        proto = yield proxy_ep.connect(socks_factory)
+        wrapped_proto = yield proto.when_done()
+        if self._tls:
+            returnValue(wrapped_proto.wrappedProtocol)
+        else:
+            returnValue(wrapped_proto)
diff --git a/txtorcon/stream.py b/txtorcon/stream.py
index 1d45904..750b96e 100644
--- a/txtorcon/stream.py
+++ b/txtorcon/stream.py
@@ -60,7 +60,7 @@ class Stream(object):
     def __init__(self, circuitcontainer, addrmap=None):
         """
         :param circuitcontainer: an object which implements
-        :class:`interface.ICircuitContainer`
+            :class:`interface.ICircuitContainer`
         """
 
         self.circuit_container = ICircuitContainer(circuitcontainer)
@@ -121,7 +121,7 @@ class Stream(object):
         listen to all streams.
 
         :param listen: something that knows
-        :class:`txtorcon.interface.IStreamListener`
+            :class:`txtorcon.interface.IStreamListener`
         """
 
         listener = IStreamListener(listen)
@@ -167,8 +167,6 @@ class Stream(object):
         return flags
 
     def update(self, args):
-        # print "update",self.id,args
-
         if self.id is None:
             self.id = int(args[0])
         else:
diff --git a/txtorcon/torconfig.py b/txtorcon/torconfig.py
index 66b0f31..b2a985c 100644
--- a/txtorcon/torconfig.py
+++ b/txtorcon/torconfig.py
@@ -5,293 +5,47 @@ from __future__ import print_function
 from __future__ import with_statement
 
 import os
-import sys
-import types
+import re
+import six
 import functools
-import tempfile
 import warnings
 from io import StringIO
-import shlex
+from collections import OrderedDict
 
 from twisted.python import log
-from twisted.internet import defer, error, protocol
-from twisted.internet.interfaces import IReactorTime
-from twisted.internet.endpoints import TCP4ClientEndpoint
-from twisted.internet.endpoints import UNIXClientEndpoint
-
-from txtorcon.torcontrolprotocol import parse_keywords, TorProtocolFactory, DEFAULT_VALUE
-from txtorcon.util import delete_file_or_tree, find_keywords, find_tor_binary
-from txtorcon.log import txtorlog
+from twisted.python.compat import nativeString
+from twisted.python.deprecate import deprecated
+from twisted.internet import defer
+from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint
+
+from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE
+from txtorcon.torcontrolprotocol import TorProtocolError
 from txtorcon.interface import ITorControlProtocol
-if sys.platform in ('linux2', 'darwin'):
-    import pwd
+from txtorcon.util import find_keywords
 
 
-class TorNotFound(RuntimeError):
+class _Version(object):
     """
-    Raised by launch_tor() in case the tor binary was unspecified and could
-    not be found by consulting the shell.
+    Replacement for incremental.Version until
+    https://github.com/meejah/txtorcon/issues/233 and/or
+    https://github.com/hawkowl/incremental/issues/31 is fixed.
     """
+    # as of latest incremental, it should only access .package and
+    # .short() via the getVersionString() method that Twisted's
+    # deprecated() uses...
 
+    def __init__(self, package, major, minor, patch):
+        self.package = package
+        self.major = major
+        self.minor = minor
+        self.patch = patch
 
-class TorProcessProtocol(protocol.ProcessProtocol):
-
-    def __init__(self, connection_creator, progress_updates=None, config=None,
-                 ireactortime=None, timeout=None, kill_on_stderr=True,
-                 stdout=None, stderr=None):
-        """
-        This will read the output from a Tor process and attempt a
-        connection to its control port when it sees any 'Bootstrapped'
-        message on stdout. You probably don't need to use this
-        directly except as the return value from the
-        :func:`txtorcon.launch_tor` method. tor_protocol contains a
-        valid :class:`txtorcon.TorControlProtocol` instance by that
-        point.
-
-        connection_creator is a callable that should return a Deferred
-        that callbacks with a :class:`txtorcon.TorControlProtocol`;
-        see :func:`txtorcon.launch_tor` for the default one which is a
-        functools.partial that will call
-        ``connect(TorProtocolFactory())`` on an appropriate
-        :api:`twisted.internet.endpoints.TCP4ClientEndpoint`
-
-        :param connection_creator: A no-parameter callable which
-            returns a Deferred which promises a
-            :api:`twisted.internet.interfaces.IStreamClientEndpoint
-            <IStreamClientEndpoint>`. If this is None, we do NOT
-            attempt to connect to the underlying Tor process.
-
-        :param progress_updates: A callback which received progress
-            updates with three args: percent, tag, summary
-
-        :param config: a TorConfig object to connect to the
-            TorControlProtocl from the launched tor (should it succeed)
-
-        :param ireactortime:
-            An object implementing IReactorTime (i.e. a reactor) which
-            needs to be supplied if you pass a timeout.
-
-        :param timeout:
-            An int representing the timeout in seconds. If we are
-            unable to reach 100% by this time we will consider the
-            setting up of Tor to have failed. Must supply ireactortime
-            if you supply this.
-
-        :param kill_on_stderr:
-            When True, kill subprocess if we receive anything on stderr
-
-        :param stdout:
-            Anything subprocess writes to stdout is sent to .write() on this
-
-        :param stderr:
-            Anything subprocess writes to stderr is sent to .write() on this
-
-        :ivar tor_protocol: The TorControlProtocol instance connected
-            to the Tor this :api:`twisted.internet.protocol.ProcessProtocol
-            <ProcessProtocol>`` is speaking to. Will be valid
-            when the `connected_cb` callback runs.
-
-        :ivar connected_cb: Triggered when the Tor process we
-            represent is fully bootstrapped
-
-        """
-
-        self.config = config
-        self.tor_protocol = None
-        self.progress_updates = progress_updates
-
-        if connection_creator:
-            self.connection_creator = connection_creator
-            self.connected_cb = defer.Deferred()
-        else:
-            self.connection_creator = None
-            self.connected_cb = None
-
-        self.attempted_connect = False
-        self.to_delete = []
-        self.kill_on_stderr = kill_on_stderr
-        self.stderr = stderr
-        self.stdout = stdout
-
-        self._setup_complete = False
-        self._did_timeout = False
-        self._timeout_delayed_call = None
-        self._on_exit = []  # Deferred's we owe a call/errback to when we exit
-        if timeout:
-            if not ireactortime:
-                raise RuntimeError(
-                    'Must supply an IReactorTime object when supplying a '
-                    'timeout')
-            ireactortime = IReactorTime(ireactortime)
-            self._timeout_delayed_call = ireactortime.callLater(
-                timeout, self.timeout_expired)
-
-    def quit(self):
-        """
-        This will terminate (with SIGTERM) the underlying Tor process.
-
-        :returns: a Deferred that callback()'s (with None) when the
-            process has actually exited.
-        """
-
-        try:
-            self.transport.signalProcess('TERM')
-            d = defer.Deferred()
-            self._on_exit.append(d)
-
-        except error.ProcessExitedAlready:
-            self.transport.loseConnection()
-            d = defer.succeed(None)
-        return d
-
-    def _signal_on_exit(self, reason):
-        to_notify = self._on_exit
-        self._on_exit = []
-        for d in to_notify:
-            d.callback(None)
-
-    def outReceived(self, data):
-        """
-        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
-        """
-
-        if self.stdout:
-            self.stdout.write(data)
-
-        # minor hack: we can't try this in connectionMade because
-        # that's when the process first starts up so Tor hasn't
-        # opened any ports properly yet. So, we presume that after
-        # its first output we're good-to-go. If this fails, we'll
-        # reset and try again at the next output (see this class'
-        # tor_connection_failed)
-
-        txtorlog.msg(data)
-        if not self.attempted_connect and self.connection_creator \
-                and 'Bootstrap' in data:
-            self.attempted_connect = True
-            d = self.connection_creator()
-            d.addCallback(self.tor_connected)
-            d.addErrback(self.tor_connection_failed)
-
-    def timeout_expired(self):
-        """
-        A timeout was supplied during setup, and the time has run out.
-        """
-
-        try:
-            self.transport.signalProcess('TERM')
-        except error.ProcessExitedAlready:
-            self.transport.loseConnection()
-        self._did_timeout = True
-
-    def errReceived(self, data):
-        """
-        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
-        """
-
-        if self.stderr:
-            self.stderr.write(data)
-
-        if self.kill_on_stderr:
-            self.transport.loseConnection()
-            raise RuntimeError(
-                "Received stderr output from slave Tor process: " + data)
-
-    def cleanup(self):
-        """
-        Clean up my temporary files.
-        """
-
-        all([delete_file_or_tree(f) for f in self.to_delete])
-        self.to_delete = []
-
-    def processExited(self, reason):
-        self._signal_on_exit(reason)
-
-    def processEnded(self, status):
-        """
-        :api:`twisted.internet.protocol.ProcessProtocol <ProcessProtocol>` API
-        """
-
-        self.cleanup()
-
-        if status.value.exitCode is None:
-            if self._did_timeout:
-                err = RuntimeError("Timeout waiting for Tor launch..")
-            else:
-                err = RuntimeError(
-                    "Tor was killed (%s)." % status.value.signal)
-        else:
-            err = RuntimeError(
-                "Tor exited with error-code %d" % status.value.exitCode
-            )
-
-        log.err(err)
-        if self.connected_cb:
-            self.connected_cb.errback(err)
-            self.connected_cb = None
-        self._signal_on_exit(status)
-
-    def progress(self, percent, tag, summary):
-        """
-        Can be overridden or monkey-patched if you want to get
-        progress updates yourself.
-        """
-
-        if self.progress_updates:
-            self.progress_updates(percent, tag, summary)
-
-    # the below are all callbacks
-
-    def tor_connection_failed(self, failure):
-        # FIXME more robust error-handling please, like a timeout so
-        # we don't just wait forever after 100% bootstrapped (that
-        # is, we're ignoring these errors, but shouldn't do so after
-        # we'll stop trying)
-        self.attempted_connect = False
-
-    def status_client(self, arg):
-        args = shlex.split(arg)
-        if args[1] != 'BOOTSTRAP':
-            return
-
-        kw = find_keywords(args)
-        prog = int(kw['PROGRESS'])
-        tag = kw['TAG']
-        summary = kw['SUMMARY']
-        self.progress(prog, tag, summary)
-
-        if prog == 100:
-            if self._timeout_delayed_call:
-                self._timeout_delayed_call.cancel()
-                self._timeout_delayed_call = None
-            if self.connected_cb:
-                self.connected_cb.callback(self)
-                self.connected_cb = None
-
-    def tor_connected(self, proto):
-        txtorlog.msg("tor_connected %s" % proto)
-
-        self.tor_protocol = proto
-        if self.config is not None:
-            self.config._update_proto(proto)
-        self.tor_protocol.is_owned = self.transport.pid
-        self.tor_protocol.post_bootstrap.addCallback(
-            self.protocol_bootstrapped).addErrback(
-                self.tor_connection_failed)
-
-    def protocol_bootstrapped(self, proto):
-        txtorlog.msg("Protocol is bootstrapped")
-
-        self.tor_protocol.add_event_listener(
-            'STATUS_CLIENT', self.status_client)
-
-        # FIXME: should really listen for these to complete as well
-        # as bootstrap etc. For now, we'll be optimistic.
-        self.tor_protocol.queue_command('TAKEOWNERSHIP')
-        self.tor_protocol.queue_command('RESETCONF __OwningControllerProcess')
+    def short(self):
+        return '{}.{}.{}'.format(self.major, self.minor, self.patch)
 
 
+ at defer.inlineCallbacks
+ at deprecated(_Version("txtorcon", 0, 18, 0))
 def launch_tor(config, reactor,
                tor_binary=None,
                progress_updates=None,
@@ -299,219 +53,27 @@ def launch_tor(config, reactor,
                timeout=None,
                kill_on_stderr=True,
                stdout=None, stderr=None):
-    """launches a new Tor process with the given config.
-
-    There may seem to be a ton of options, but don't panic: this
-    method should be easy to use and most options can be ignored
-    except for advanced use-cases. Calling with a completely empty
-    TorConfig should Just Work::
-
-        config = TorConfig()
-        d = launch_tor(config, reactor)
-        d.addCallback(...)
-
-    Note that the incoming TorConfig instance is examined and several
-    config options are acted upon appropriately:
-
-    ``DataDirectory``: if supplied, a tempdir is not created, and the
-    one supplied is not deleted.
-
-    ``ControlPort``: if 0 (zero), a control connection is NOT
-    established (and ``connection_creator`` is ignored). In this case
-    we can't wait for Tor to bootstrap, and **you must kill the tor**
-    yourself.
-
-    ``User``: if this exists, we attempt to set ownership of the tempdir
-    to this user (but only if our effective UID is 0).
-
-    This method may set the following options on the supplied
-    TorConfig object: ``DataDirectory, ControlPort,
-    CookieAuthentication, __OwningControllerProcess`` and WILL call
-    :meth:`txtorcon.TorConfig.save`
-
-    :param config:
-        an instance of :class:`txtorcon.TorConfig` with any
-        configuration values you want.  If ``ControlPort`` isn't set,
-        9052 is used; if ``DataDirectory`` isn't set, tempdir is used
-        to create one (in this case, it will be deleted upon exit).
-
-    :param reactor: a Twisted IReactorCore implementation (usually
-        twisted.internet.reactor)
-
-    :param tor_binary: path to the Tor binary to run. Tries to find the tor
-        binary if unset.
-
-    :param progress_updates: a callback which gets progress updates; gets as
-         args: percent, tag, summary (FIXME make an interface for this).
-
-    :param kill_on_stderr:
-        When True (the default), if Tor prints anything on stderr we
-        kill off the process, close the TorControlProtocol and raise
-        an exception.
-
-    :param stdout: a file-like object to which we write anything that
-        Tor prints on stdout (just needs to support write()).
-
-    :param stderr: a file-like object to which we write anything that
-        Tor prints on stderr (just needs .write()). Note that we kill Tor
-        off by default if anything appears on stderr; pass "no_kill=True"
-        if you don't like the behavior.
-
-    :param connection_creator: is mostly available to ease testing, so
-        you probably don't want to supply this. If supplied, it is a
-        callable that should return a Deferred that delivers an
-        :api:`twisted.internet.interfaces.IProtocol <IProtocol>` or
-        ConnectError.
-        See :api:`twisted.internet.interfaces.IStreamClientEndpoint`.connect
-        Note that this parameter is ignored if config.ControlPort == 0
-
-    :return: a Deferred which callbacks with a TorProcessProtocol
-        connected to the fully-bootstrapped Tor; this has a
-        :class:`txtorcon.TorControlProtocol` instance as `.tor_protocol`. In
-        Tor, ``__OwningControllerProcess`` will be set and TAKEOWNERSHIP will
-        have been called, so if you close the TorControlProtocol the Tor should
-        exit also (see `control-spec
-        <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
-        3.23). Note that if ControlPort was 0, we don't connect at all
-        and therefore don't wait for Tor to be bootstrapped. In this case, it's
-        up to you to kill off the Tor you created.
-
-    HACKS:
-
-     1. It's hard to know when Tor has both (completely!) written its
-        authentication cookie file AND is listening on the control
-        port. It seems that waiting for the first 'bootstrap' message on
-        stdout is sufficient. Seems fragile...and doesn't work 100% of
-        the time, so FIXME look at Tor source.
     """
+    Deprecated; use launch() instead.
 
-    # We have a slight problem with the approach: we need to pass a
-    # few minimum values to a torrc file so that Tor will start up
-    # enough that we may connect to it. Ideally, we'd be able to
-    # start a Tor up which doesn't really do anything except provide
-    # "AUTHENTICATE" and "GETINFO config/names" so we can do our
-    # config validation.
-
-    # the other option here is to simply write a torrc version of our
-    # config and get Tor to load that...which might be the best
-    # option anyway.
-
-    # actually, can't we pass them all as command-line arguments?
-    # could be pushing some limits for giant configs...
-
-    if tor_binary is None:
-        tor_binary = find_tor_binary()
-    if tor_binary is None:
-        # We fail right here instead of waiting for the reactor to start
-        raise TorNotFound('Tor binary could not be found')
-
-    # make sure we got things that have write() for stderr, stdout
-    # kwargs
-    for arg in [stderr, stdout]:
-        if arg and not getattr(arg, "write", None):
-            raise RuntimeError(
-                'File-like object needed for stdout or stderr args.')
-
-    try:
-        data_directory = config.DataDirectory
-        user_set_data_directory = True
-    except KeyError:
-        user_set_data_directory = False
-        data_directory = tempfile.mkdtemp(prefix='tortmp')
-        config.DataDirectory = data_directory
-
-        # Set ownership on the temp-dir to the user tor will drop privileges to
-        # when executing as root.
-        try:
-            user = config.User
-        except KeyError:
-            pass
-        else:
-            if sys.platform in ('linux2', 'darwin') and os.geteuid() == 0:
-                os.chown(data_directory, pwd.getpwnam(user).pw_uid, -1)
-
-    try:
-        control_port = config.ControlPort
-    except KeyError:
-        control_port = 9052  # FIXME choose a random, unoccupied one?
-        config.ControlPort = control_port
-
-    # so, we support passing in ControlPort=0 -- not really sure if
-    # this is a good idea (since then the caller has to kill the tor
-    # off, etc), but at least one person has requested it :/
-    if control_port != 0:
-        config.CookieAuthentication = 1
-        config.__OwningControllerProcess = os.getpid()
-        if connection_creator is None:
-            if str(control_port).startswith('unix:'):
-                connection_creator = functools.partial(
-                    UNIXClientEndpoint(reactor, control_port[5:]).connect,
-                    TorProtocolFactory()
-                )
-            else:
-                connection_creator = functools.partial(
-                    TCP4ClientEndpoint(reactor, 'localhost', control_port).connect,
-                    TorProtocolFactory()
-                )
-    else:
-        connection_creator = None
-
-    # NOTE well, that if we don't pass "-f" then Tor will merrily load
-    # it's default torrc, and apply our options over top... :/
-    config_args = ['-f', '/non-existant', '--ignore-missing-torrc']
-
-    # ...now add all our config options on the command-line. This
-    # avoids writing a temporary torrc.
-    for (k, v) in config.config_args():
-        config_args.append(k)
-        config_args.append(v)
-
-    # txtorlog.msg('Running with config:\n', ' '.join(config_args))
-
-    process_protocol = TorProcessProtocol(
-        connection_creator,
-        progress_updates,
-        config, reactor,
-        timeout,
-        kill_on_stderr,
-        stdout,
-        stderr
+    See also controller.py
+    """
+    from .controller import launch
+    # XXX FIXME are we dealing with options in the config "properly"
+    # as far as translating semantics from the old launch_tor to
+    # launch()? DataDirectory, User, ControlPort, ...?
+    tor = yield launch(
+        reactor,
+        stdout=stdout,
+        stderr=stderr,
+        progress_updates=progress_updates,
+        tor_binary=tor_binary,
+        connection_creator=connection_creator,
+        timeout=timeout,
+        kill_on_stderr=kill_on_stderr,
+        _tor_config=config,
     )
-
-    # we set both to_delete and the shutdown events because this
-    # process might be shut down way before the reactor, but if the
-    # reactor bombs out without the subprocess getting closed cleanly,
-    # we'll want the system shutdown events triggered so the temporary
-    # files get cleaned up either way
-
-    # we don't want to delete the user's directories, just temporary
-    # ones this method created.
-    if not user_set_data_directory:
-        process_protocol.to_delete = [data_directory]
-        reactor.addSystemEventTrigger(
-            'before', 'shutdown',
-            functools.partial(delete_file_or_tree, data_directory)
-        )
-
-    try:
-        log.msg('Spawning tor process with DataDirectory', data_directory)
-        args = [tor_binary] + config_args
-        transport = reactor.spawnProcess(
-            process_protocol,
-            tor_binary,
-            args=args,
-            env={'HOME': data_directory},
-            path=data_directory if os.path.exists(data_directory) else None,
-        )
-        # FIXME? don't need rest of the args: uid, gid, usePTY, childFDs)
-        transport.closeStdin()
-
-    except RuntimeError as e:
-        return defer.fail(e)
-
-    if process_protocol.connected_cb:
-        return process_protocol.connected_cb
-    return defer.succeed(process_protocol)
+    defer.returnValue(tor.process)
 
 
 class TorConfigType(object):
@@ -649,6 +211,7 @@ class LineList(TorConfigType):
         return _ListWrapper(
             obj, functools.partial(instance.mark_unsaved, name))
 
+
 config_types = [Boolean, Boolean_Auto, LineList, Integer, SignedInteger, Port,
                 TimeInterval, TimeMsecInterval,
                 DataSize, Float, Time, CommaList, String, LineList, Filename,
@@ -689,7 +252,6 @@ class _ListWrapper(list):
         self.on_modify = on_modify_cb
 
     __setitem__ = _wrapture(list.__setitem__)
-    __setslice__ = _wrapture(list.__setslice__)
     append = _wrapture(list.append)
     extend = _wrapture(list.extend)
     insert = _wrapture(list.insert)
@@ -700,6 +262,10 @@ class _ListWrapper(list):
         return '_ListWrapper' + super(_ListWrapper, self).__repr__()
 
 
+if six.PY2:
+    setattr(_ListWrapper, '__setslice__', _wrapture(list.__setslice__))
+
+
 class HiddenServiceClientAuth(object):
     """
     Encapsulates a single client-authorization, as parsed from a
@@ -878,6 +444,15 @@ class EphemeralHiddenService(object):
     https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1295
     '''
 
+    @classmethod
+    def _is_valid_keyblob(cls, key_blob_or_type):
+        try:
+            key_blob_or_type = nativeString(key_blob_or_type)
+        except (UnicodeError, TypeError):
+            return False
+        else:
+            return re.match(r'[^ :]+:[^ :]+$', key_blob_or_type)
+
     # XXX the "ports" stuff is still kind of an awkward API, especialy
     # making the actual list public (since it'll have
     # "80,127.0.0.1:80" instead of with a space
@@ -888,21 +463,22 @@ class EphemeralHiddenService(object):
     # XXX "auth" is unused (also, no Tor support I don't think?)
 
     def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2):
-        if not isinstance(ports, types.ListType):
+        if not isinstance(ports, list):
             ports = [ports]
         # for "normal" HSes the port-config bit looks like "80
         # 127.0.0.1:1234" whereas this one wants a comma, so we leave
         # the public API the same and fix up the space. Or of course
         # you can just use the "real" comma-syntax if you wanted.
-        self._ports = map(lambda x: x.replace(' ', ','), ports)
-        self._key_blob = key_blob_or_type
+        self._ports = [x.replace(' ', ',') for x in ports]
+        if EphemeralHiddenService._is_valid_keyblob(key_blob_or_type):
+            self._key_blob = nativeString(key_blob_or_type)
+        else:
+            raise ValueError(
+                'key_blob_or_type must be a string in the formats '
+                '"NEW:<ALGORITHM>" or "<ALGORITHM>:<KEY>"')
         self.auth = auth  # FIXME ununsed
         # FIXME nicer than assert, plz
-        assert ' ' not in self._key_blob
-        assert isinstance(ports, types.ListType)
-        if not key_blob_or_type.startswith('NEW:') \
-           and (len(key_blob_or_type) > 825 or len(key_blob_or_type) < 820):
-            raise RuntimeError('Wrong size key-blob')
+        assert isinstance(ports, list)
 
     @defer.inlineCallbacks
     def add_to_tor(self, protocol):
@@ -916,8 +492,10 @@ class EphemeralHiddenService(object):
         ans = yield protocol.queue_command(cmd)
         ans = find_keywords(ans.split('\n'))
         self.hostname = ans['ServiceID'] + '.onion'
-        if self._key_blob == 'NEW:BEST':
+        if self._key_blob.startswith('NEW:'):
             self.private_key = ans['PrivateKey']
+        else:
+            self.private_key = self._key_blob
 
         log.msg('Created hidden-service at', self.hostname)
 
@@ -1070,6 +648,32 @@ def parse_client_keys(stream):
     return parser_state.keys
 
 
+def _endpoint_from_socksport_line(reactor, socks_config):
+    """
+    Internal helper.
+
+    Returns an IStreamClientEndpoint for the given config, which is of
+    the same format expected by the SOCKSPort option in Tor.
+    """
+    if socks_config.startswith('unix:'):
+        # XXX wait, can SOCKSPort lines with "unix:/path" still
+        # include options afterwards? What about if the path has a
+        # space in it?
+        return UNIXClientEndpoint(reactor, socks_config[5:])
+
+    # options like KeepAliveIsolateSOCKSAuth can be appended
+    # to a SocksPort line...
+    if ' ' in socks_config:
+        socks_config = socks_config.split()[0]
+    if ':' in socks_config:
+        host, port = socks_config.split(':', 1)
+        port = int(port)
+    else:
+        host = '127.0.0.1'
+        port = int(socks_config)
+    return TCP4ClientEndpoint(reactor, host, port)
+
+
 class TorConfig(object):
     """This class abstracts out Tor's config, and can be used both to
     create torrc files from nothing and track live configuration of a Tor
@@ -1109,19 +713,19 @@ class TorConfig(object):
     FIXME:
 
         - HiddenServiceOptions is special: GETCONF on it returns
-        several (well, two) values. Besides adding the two keys 'by
-        hand' do we need to do anything special? Can't we just depend
-        on users doing 'conf.hiddenservicedir = foo' AND
-        'conf.hiddenserviceport = bar' before a save() ?
+          several (well, two) values. Besides adding the two keys 'by
+          hand' do we need to do anything special? Can't we just depend
+          on users doing 'conf.hiddenservicedir = foo' AND
+          'conf.hiddenserviceport = bar' before a save() ?
 
         - once I determine a value is default, is there any way to
           actually get what this value is?
 
     """
 
-    @classmethod
+    @staticmethod
     @defer.inlineCallbacks
-    def from_protocol(cls, proto):
+    def from_protocol(proto):
         """
         This creates and returns a ready-to-go TorConfig instance from the
         given protocol, which should be an instance of
@@ -1142,7 +746,7 @@ class TorConfig(object):
         else:
             self._protocol = ITorControlProtocol(control)
 
-        self.unsaved = {}
+        self.unsaved = OrderedDict()
         '''Configuration that has been changed since last save().'''
 
         self.parsers = {}
@@ -1163,7 +767,7 @@ class TorConfig(object):
         if self.protocol:
             if self.protocol.post_bootstrap:
                 self.protocol.post_bootstrap.addCallback(
-                    self.bootstrap).addErrback(log.err)
+                    self.bootstrap).addErrback(self.post_bootstrap.errback)
             else:
                 self.bootstrap()
 
@@ -1172,6 +776,107 @@ class TorConfig(object):
 
         self.__dict__['_setup_'] = None
 
+    def socks_endpoint(self, reactor, port=None):
+        """
+        Returns a TorSocksEndpoint configured to use an already-configured
+        SOCKSPort from the Tor we're connected to. By default, this
+        will be the very first SOCKSPort.
+
+        :param port: a str, the first part of the SOCKSPort line (that
+            is, a port like "9151" or a Unix socket config like
+            "unix:/path". You may also specify a port as an int.
+
+        If you need to use a particular port that may or may not
+        already be configured, see the async method
+        :meth:`txtorcon.TorConfig.create_socks_endpoint`
+        """
+
+        if len(self.SocksPort) == 0:
+            raise RuntimeError(
+                "No SOCKS ports configured"
+            )
+
+        socks_config = None
+        if port is None:
+            socks_config = self.SocksPort[0]
+        else:
+            port = str(port)  # in case e.g. an int passed in
+            if ' ' in port:
+                raise ValueError(
+                    "Can't specify options; use create_socks_endpoint instead"
+                )
+
+            for idx, port_config in enumerate(self.SocksPort):
+                # "SOCKSPort" is a gnarly beast that can have a bunch
+                # of options appended, so we have to split off the
+                # first thing which *should* be the port (or can be a
+                # string like 'unix:')
+                if port_config.split()[0] == port:
+                    socks_config = port_config
+                    break
+        if socks_config is None:
+            raise RuntimeError(
+                "No SOCKSPort configured for port {}".format(port)
+            )
+
+        return _endpoint_from_socksport_line(reactor, socks_config)
+
+    @defer.inlineCallbacks
+    def create_socks_endpoint(self, reactor, socks_config):
+        """
+        Creates a new TorSocksEndpoint instance given a valid
+        configuration line for ``SocksPort``; if this configuration
+        isn't already in the underlying tor, we add it. Note that this
+        method may call :meth:`txtorcon.TorConfig.save()` on this instance.
+
+        Note that calling this with `socks_config=None` is equivalent
+        to calling `.socks_endpoint` (which is not async).
+
+        XXX socks_config should be .. i dunno, but there's fucking
+        options and craziness, e.g. default Tor Browser Bundle is:
+        ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth',
+        '9155']
+
+        XXX maybe we should say "socks_port" as the 3rd arg, insist
+        it's an int, and then allow/support all the other options
+        (e.g. via kwargs)
+
+        XXX we could avoid the "maybe call .save()" thing; worth it?
+        (actually, no we can't or the Tor won't have it config'd)
+        """
+
+        yield self.post_bootstrap
+
+        if socks_config is None:
+            if len(self.SocksPort) == 0:
+                raise RuntimeError(
+                    "socks_port is None and Tor has no SocksPorts configured"
+                )
+            socks_config = self.SocksPort[0]
+        else:
+            if not any([socks_config in port for port in self.SocksPort]):
+                # need to configure Tor
+                self.SocksPort.append(socks_config)
+                try:
+                    yield self.save()
+                except TorProtocolError as e:
+                    extra = ''
+                    if socks_config.startswith('unix:'):
+                        # XXX so why don't we check this for the
+                        # caller, earlier on?
+                        extra = '\nNote Tor has specific ownership/permissions ' +\
+                                'requirements for unix sockets and parent dir.'
+                    raise RuntimeError(
+                        "While configuring SOCKSPort to '{}', error from"
+                        " Tor: {}{}".format(
+                            socks_config, e, extra
+                        )
+                    )
+
+        defer.returnValue(
+            _endpoint_from_socksport_line(reactor, socks_config)
+        )
+
     # FIXME should re-name this to "tor_protocol" to be consistent
     # with other things? Or rename the other things?
     """
@@ -1182,6 +887,7 @@ class TorConfig(object):
     def _get_protocol(self):
         return self.__dict__['_protocol']
     protocol = property(_get_protocol)
+    tor_protocol = property(_get_protocol)
 
     def attach_protocol(self, proto):
         """
@@ -1201,13 +907,6 @@ class TorConfig(object):
             proto.post_bootstrap.addCallback(self.bootstrap)
         return self.__dict__['post_bootstrap']
 
-    def _update_proto(self, proto):
-        """
-        internal method, used by launch_tor to update the protocol after we're
-        set up.
-        """
-        self.__dict__['_protocol'] = proto
-
     def __setattr__(self, name, value):
         """
         we override this so that we can provide direct attribute
@@ -1440,7 +1139,9 @@ class TorConfig(object):
                 self._setup_hidden_services(servicelines)
                 continue
 
-            if value == 'Dependant':
+            # XXX for Virtual check that it's one of the *Ports things
+            # (because if not it should be an error)
+            if value in ('Dependant', 'Dependent', 'Virtual'):
                 continue
 
             # there's a thing called "Boolean+Auto" which is -1 for
@@ -1537,18 +1238,20 @@ class TorConfig(object):
 
     def config_args(self):
         '''
-        Returns an iterator of 2-tuples (config_name, value), one for
-        each configuration option in this config. This is more-or-less
-        and internal method, but see, e.g., launch_tor()'s
-        implementation if you thing you need to use this for
-        something.
+        Returns an iterator of 2-tuples (config_name, value), one for each
+        configuration option in this config. This is more-or-less an
+        internal method, but see, e.g., launch_tor()'s implementation
+        if you think you need to use this for something.
 
         See :meth:`txtorcon.TorConfig.create_torrc` which returns a
         string which is also a valid ``torrc`` file
-
         '''
 
-        for (k, v) in list(self.config.items()) + list(self.unsaved.items()):
+        everything = dict()
+        everything.update(self.config)
+        everything.update(self.unsaved)
+
+        for (k, v) in list(everything.items()):
             if type(v) is _ListWrapper:
                 if k.lower() == 'hiddenservices':
                     for x in v:
diff --git a/txtorcon/torcontrolprotocol.py b/txtorcon/torcontrolprotocol.py
index 08c49ba..48a808c 100644
--- a/txtorcon/torcontrolprotocol.py
+++ b/txtorcon/torcontrolprotocol.py
@@ -4,11 +4,14 @@ from __future__ import absolute_import
 from __future__ import print_function
 from __future__ import with_statement
 
+from binascii import b2a_hex, hexlify
+
 from twisted.python import log
 from twisted.internet import defer
 from twisted.internet.interfaces import IProtocolFactory
 from twisted.internet.error import ConnectionDone
 from twisted.protocols.basic import LineOnlyReceiver
+from twisted.python.failure import Failure
 
 from zope.interface import implementer
 
@@ -17,6 +20,7 @@ from txtorcon.log import txtorlog
 
 from txtorcon.interface import ITorControlProtocol
 from .spaghetti import FSM, State, Transition
+from .util import maybe_coroutine
 
 import os
 import re
@@ -25,39 +29,6 @@ import base64
 DEFAULT_VALUE = 'DEFAULT'
 
 
- at defer.inlineCallbacks
-def connect(endpoint, password_function=lambda: None):
-    """
-    (experimental; details may change) This connects to the given
-    endpoint, presuming it is a Tor control connection and gives back
-    a TorControlProtocol instance. This does *not* reconnect if the
-    connection is dropped. This is the preferred entry point to
-    control a running Tor; if you need to *start* a Tor see
-    :ref:`launching_tor`.
-
-    See :meth:`txtorcon.TorState.from_protocol` to create a valid
-    :class:`txtorcon.TorState` instance from the connection.
-
-    :param endpoint: A Twisted IEndpoint, in practice either a
-        TCP4ClientEndpoint or a UnixClientEndpoint. By default, Tor
-        uses "tcp:localhost:9051" or "unix:/var/run/tor/control".
-
-    :param password_function: This is only consulted if the Tor to
-        which we connect does not have cookie authentication enabled. In
-        that case, this function is called to request a password. It may
-        return a Deferred.
-
-    :returns: A Deferred that fires with a ready-to-use
-        TorControlProtocol instance is returned, or Failure if a
-        connection can't be established.
-    """
-
-    factory = TorProtocolFactory(password_function=password_function)
-    proto = yield endpoint.connect(factory)
-    yield proto.post_bootstrap
-    defer.returnValue(proto)
-
-
 class TorProtocolError(RuntimeError):
     """
     Happens on 500-level responses in the protocol, almost certainly
@@ -139,9 +110,18 @@ class Event(object):
         self.callbacks.remove(cb)
 
     def got_update(self, data):
-        # print self.name,"got_update:",data
         for cb in self.callbacks:
-            cb(data)
+            try:
+                cb(data)
+            except Exception as e:
+                log.err(Failure())
+                log.err(
+                    "Notifying '{callback}' for '{name}' failed: {e}".format(
+                        callback=cb,
+                        name=self.name,
+                        e=e,
+                    )
+                )
 
 
 def unquote(word):
@@ -367,8 +347,7 @@ class TorControlProtocol(LineOnlyReceiver):
         GETINFO command. See :meth:`getinfo
         <txtorcon.TorControlProtocol.get_info>`
         """
-        info = ' '.join([str(x) for x in list(args)])
-        return self.queue_command('GETINFO %s' % info)
+        return self.queue_command('GETINFO %s' % ' '.join(args))
 
     def get_info_incremental(self, key, line_cb):
         """
@@ -482,16 +461,18 @@ class TorControlProtocol(LineOnlyReceiver):
         return self.queue_command('SIGNAL %s' % nm)
 
     def add_event_listener(self, evt, callback):
-        """:param evt: event name, see also
-        :var:`txtorcon.TorControlProtocol.events` .keys()
-
+        """
         Add a listener to an Event object. This may be called multiple
         times for the same event. If it's the first listener, a new
         SETEVENTS call will be initiated to Tor.
 
-        Currently the callback is any callable that takes a single
-        argument, that is the text collected for the event from the
-        tor control protocol.
+        :param evt: event name, see also
+            :attr:`txtorcon.TorControlProtocol.events` .keys(). These
+            event names are queried from Tor (with `GETINFO events/names`)
+
+        :param callback: any callable that takes a single argument
+             which receives the text collected for the event from the
+             tor control protocol.
 
         For more information on the events supported, see
         `control-spec section 4.1
@@ -505,9 +486,8 @@ class TorControlProtocol(LineOnlyReceiver):
         :Return: ``None``
 
         .. todo::
-            need an interface for the callback
-            show how to tie in Stem parsing if you want
-
+            - should have an interface for the callback
+            - show how to tie in Stem parsing if you want
         """
 
         if evt not in self.valid_events.values():
@@ -523,6 +503,13 @@ class TorControlProtocol(LineOnlyReceiver):
         return None
 
     def remove_event_listener(self, evt, cb):
+        """
+        The opposite of :meth:`TorControlProtocol.add_event_listener`
+
+        :param evt: the event name (or an Event object)
+
+        :param cb: the callback object to remove
+        """
         if evt not in self.valid_events.values():
             # this lets us pass a string or a real event-object
             try:
@@ -546,8 +533,18 @@ class TorControlProtocol(LineOnlyReceiver):
         return self.queue_command("PROTOCOLINFO 1")
 
     def authenticate(self, passphrase):
-        """Call the AUTHENTICATE command."""
-        return self.queue_command('AUTHENTICATE ' + passphrase.encode("hex"))
+        """
+        Call the AUTHENTICATE command.
+
+        Quoting torspec/control-spec.txt: "The authentication token
+        can be specified as either a quoted ASCII string, or as an
+        unquoted hexadecimal encoding of that same string (to avoid
+        escaping issues)."
+        """
+        if not isinstance(passphrase, bytes):
+            passphrase = passphrase.encode()
+        phrase = b2a_hex(passphrase)
+        return self.queue_command(b'AUTHENTICATE ' + phrase)
 
     def quit(self):
         """
@@ -569,6 +566,8 @@ class TorControlProtocol(LineOnlyReceiver):
         through this command.
         """
 
+        if not isinstance(cmd, bytes):
+            cmd = cmd.encode('ascii')
         d = defer.Deferred()
         self.commands.append((d, cmd, arg))
         self._maybe_issue_command()
@@ -583,9 +582,9 @@ class TorControlProtocol(LineOnlyReceiver):
         :api:`twisted.protocols.basic.LineOnlyReceiver` API
         """
 
-        self.debuglog.write(line + '\n')
+        self.debuglog.write(line + b'\n')
         self.debuglog.flush()
-        self.fsm.process(line)
+        self.fsm.process(line.decode('ascii'))
 
     def connectionMade(self):
         "Protocol API"
@@ -612,8 +611,9 @@ class TorControlProtocol(LineOnlyReceiver):
 
         firstline = rest[:rest.find('\n')]
         args = firstline.split()
-        if args[0] in self.events:
-            self.events[args[0]].got_update(rest[len(args[0]) + 1:])
+        name = args[0]
+        if name in self.events:
+            self.events[name].got_update(rest[len(name) + 1:])
             return
         # not considering this an error, as there's a slight window
         # after remove_event_listener is called (so the handler is
@@ -633,12 +633,12 @@ class TorControlProtocol(LineOnlyReceiver):
             (d, cmd, cmd_arg) = self.command
             self.defer = d
 
-            self.debuglog.write(cmd + '\n')
+            self.debuglog.write(cmd + b'\n')
             self.debuglog.flush()
 
-            data = cmd + '\r\n'
+            data = cmd + b'\r\n'
             txtorlog.msg("cmd: {}".format(data.strip()))
-            self.transport.write(data.encode('utf8'))
+            self.transport.write(data)
 
     def _auth_failed(self, fail):
         """
@@ -660,8 +660,8 @@ class TorControlProtocol(LineOnlyReceiver):
         server_nonce = base64.b16decode(kw['SERVERNONCE'])
         # FIXME put string in global. or something.
         expected_server_hash = hmac_sha256(
-            "Tor safe cookie authentication server-to-controller hash",
-            self._cookie_data + self.client_nonce + server_nonce
+            b"Tor safe cookie authentication server-to-controller hash",
+            self._cookie_data + self.client_nonce + server_nonce,
         )
 
         if not compare_via_hash(expected_server_hash, server_hash):
@@ -672,11 +672,11 @@ class TorControlProtocol(LineOnlyReceiver):
             )
 
         client_hash = hmac_sha256(
-            "Tor safe cookie authentication controller-to-server hash",
+            b"Tor safe cookie authentication controller-to-server hash",
             self._cookie_data + self.client_nonce + server_nonce
         )
         client_hash_hex = base64.b16encode(client_hash)
-        return self.queue_command('AUTHENTICATE %s' % client_hash_hex)
+        return self.queue_command(b'AUTHENTICATE ' + client_hash_hex)
 
     def _read_cookie(self, cookiefile):
         """
@@ -715,13 +715,24 @@ class TorControlProtocol(LineOnlyReceiver):
                 cookiefile = unescape_quoted_string(cookiefile)
                 try:
                     self._read_cookie(cookiefile)
+                    cookie_auth = True
                 except IOError as why:
                     txtorlog.msg("Reading COOKIEFILE failed: " + str(why))
-                    cookie_auth = False
-                else:
-                    cookie_auth = True
+                    if self.password_function and 'HASHEDPASSWORD' in methods:
+                        txtorlog.msg("Falling back to password")
+                    else:
+                        raise RuntimeError(
+                            "Failed to read COOKIEFILE '{fname}': {msg}\n".format(
+                                fname=cookiefile,
+                                msg=str(why),
+                            )
+                            # "On Debian, join the debian-tor group"
+                        )
             else:
                 txtorlog.msg("Didn't get COOKIEFILE")
+                raise RuntimeError(
+                    "Got 'COOKIE' or 'SAFECOOKIE' method, but no 'COOKIEFILE'"
+                )
 
         if cookie_auth:
             if 'SAFECOOKIE' in methods:
@@ -729,8 +740,8 @@ class TorControlProtocol(LineOnlyReceiver):
                              len(self._cookie_data), "bytes")
                 self.client_nonce = os.urandom(32)
 
-                cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \
-                      base64.b16encode(self.client_nonce)
+                cmd = b'AUTHCHALLENGE SAFECOOKIE ' + \
+                      hexlify(self.client_nonce)
                 d = self.queue_command(cmd)
                 d.addCallback(self._safecookie_authchallenge)
                 d.addCallback(self._bootstrap)
@@ -747,6 +758,7 @@ class TorControlProtocol(LineOnlyReceiver):
 
         if self.password_function and 'HASHEDPASSWORD' in methods:
             d = defer.maybeDeferred(self.password_function)
+            d.addCallback(maybe_coroutine)
             d.addCallback(self._do_password_authentication)
             d.addErrback(self._auth_failed)
             return
@@ -759,7 +771,8 @@ class TorControlProtocol(LineOnlyReceiver):
 
         raise RuntimeError(
             "The Tor I connected to doesn't support SAFECOOKIE nor COOKIE"
-            " authentication and I have no password_function specified."
+            " authentication (or we can't read the cookie files) and I have"
+            " no password_function specified."
         )
 
     def _do_password_authentication(self, passwd):
@@ -842,7 +855,6 @@ class TorControlProtocol(LineOnlyReceiver):
 
     def _is_continuation_line(self, line):
         "for FSM"
-        # print "isContinuationLine",self.code,line
         code = int(line[:3])
         if self.code and self.code != code:
             raise RuntimeError("Unexpected code %d, wanted %d" % (code,
@@ -851,7 +863,6 @@ class TorControlProtocol(LineOnlyReceiver):
 
     def _is_multi_line(self, line):
         "for FSM"
-        # print "isMultiLine",self.code,line,line[3] == '+'
         code = int(line[:3])
         if self.code and self.code != code:
             raise RuntimeError("Unexpected code %d, wanted %d" % (code,
@@ -889,7 +900,6 @@ class TorControlProtocol(LineOnlyReceiver):
 
     def _broadcast_response(self, line):
         "for FSM"
-        # print "BCAST",line
         if len(line) > 3:
             if self.code >= 200 and self.code < 300 and \
                self.command and self.command[2] is not None:
@@ -901,7 +911,9 @@ class TorControlProtocol(LineOnlyReceiver):
         else:
             resp = self.response
         self.response = ''
-        if self.code >= 200 and self.code < 300:
+        if self.code is None:
+            raise RuntimeError("No code set yet in broadcast response.")
+        elif self.code >= 200 and self.code < 300:
             if self.defer is None:
                 raise RuntimeError(
                     'Got a response, but didn\'t issue a command: "%s"' % resp
@@ -916,8 +928,6 @@ class TorControlProtocol(LineOnlyReceiver):
             self._handle_notify(self.code, resp)
             self.code = None
             return
-        elif self.code is None:
-            raise RuntimeError("No code set yet in broadcast response.")
         else:
             raise RuntimeError(
                 "Unknown code in broadcast response %d." % self.code
diff --git a/txtorcon/torstate.py b/txtorcon/torstate.py
index c18a577..07b8f08 100644
--- a/txtorcon/torstate.py
+++ b/txtorcon/torstate.py
@@ -11,13 +11,14 @@ import types
 import warnings
 
 from twisted.internet import defer
+from twisted.python.failure import Failure
 from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.internet.endpoints import UNIXClientEndpoint
 from twisted.internet.interfaces import IReactorCore
 from twisted.internet.interfaces import IStreamClientEndpoint
 from zope.interface import implementer
 
-from txtorcon import TorProtocolFactory
+from txtorcon.torcontrolprotocol import TorProtocolFactory
 from txtorcon.stream import Stream
 from txtorcon.circuit import Circuit
 from txtorcon.router import Router, hashFromHexId
@@ -32,8 +33,9 @@ from txtorcon.interface import ICircuitListener
 from txtorcon.interface import ICircuitContainer
 from txtorcon.interface import IStreamListener
 from txtorcon.interface import IStreamAttacher
-from .spaghetti import FSM, State, Transition
-from .util import basestring
+from ._microdesc_parser import MicrodescriptorParser
+from .router import hexIdFromHash
+from .util import maybe_coroutine
 
 
 def _build_state(proto):
@@ -164,6 +166,24 @@ def flags_from_dict(kw):
     return flags
 
 
+def _extract_reason(kw):
+    """
+    Internal helper. Extracts a reason (possibly both reasons!) from
+    the kwargs for a circuit failed or closed event.
+    """
+    try:
+        # we "often" have a REASON
+        reason = kw['REASON']
+        try:
+            # ...and sometimes even have a REMOTE_REASON
+            reason = '{}, {}'.format(reason, kw['REMOTE_REASON'])
+        except KeyError:
+            pass  # should still be the 'REASON' error if we had it
+    except KeyError:
+        reason = "unknown"
+    return reason
+
+
 @implementer(ICircuitListener)
 @implementer(ICircuitContainer)
 @implementer(IRouterContainer)
@@ -214,7 +234,7 @@ class TorState(object):
         self.circuit_factory = Circuit
         self.stream_factory = Stream
 
-        self.attacher = None
+        self._attacher = None
         """If set, provides
         :class:`txtorcon.interface.IStreamAttacher` to attach new
         streams we hear about."""
@@ -258,127 +278,77 @@ class TorState(object):
         self.authorities = {}
 
         #: see set_attacher
-        self.cleanup = None
-
-        class die(object):
-            __name__ = 'die'  # FIXME? just to ease spagetti.py:82's pain
-
-            def __init__(self, msg):
-                self.msg = msg
-
-            def __call__(self, *args):
-                raise RuntimeError(self.msg % tuple(args))
+        self._cleanup = None
 
-        waiting_r = State("waiting_r")
-        waiting_w = State("waiting_w")
-        waiting_p = State("waiting_p")
-        waiting_s = State("waiting_s")
-
-        def ignorable_line(x):
-            x = x.strip()
-            return x in ['.', 'OK', ''] or x.startswith('ns/')
-
-        waiting_r.add_transition(Transition(waiting_r, ignorable_line, None))
-        waiting_r.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))
-        # FIXME use better method/func than die!!
-        waiting_r.add_transition(Transition(waiting_r, lambda x: not x.startswith('r '), die('Expected "r " while parsing routers not "%s"')))
-
-        waiting_s.add_transition(Transition(waiting_w, lambda x: x.startswith('s '), self._router_flags))
-        waiting_s.add_transition(Transition(waiting_s, lambda x: x.startswith('a '), self._router_address))
-        waiting_s.add_transition(Transition(waiting_r, ignorable_line, None))
-        waiting_s.add_transition(Transition(waiting_r, lambda x: not x.startswith('s ') and not x.startswith('a '), die('Expected "s " while parsing routers not "%s"')))
-        waiting_s.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
-
-        waiting_w.add_transition(Transition(waiting_p, lambda x: x.startswith('w '), self._router_bandwidth))
-        waiting_w.add_transition(Transition(waiting_r, ignorable_line, None))
-        waiting_w.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))  # "w" lines are optional
-        waiting_w.add_transition(Transition(waiting_r, lambda x: not x.startswith('w '), die('Expected "w " while parsing routers not "%s"')))
-        waiting_w.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
-
-        waiting_p.add_transition(Transition(waiting_r, lambda x: x.startswith('p '), self._router_policy))
-        waiting_p.add_transition(Transition(waiting_r, ignorable_line, None))
-        waiting_p.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin))  # "p" lines are optional
-        waiting_p.add_transition(Transition(waiting_r, lambda x: x[:2] != 'p ', die('Expected "p " while parsing routers not "%s"')))
-        waiting_p.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None))
-
-        self._network_status_parser = FSM([waiting_r, waiting_s, waiting_w, waiting_p])
+        self._network_status_parser = MicrodescriptorParser(self._create_router)
 
         self.post_bootstrap = defer.Deferred()
         if bootstrap:
             self.protocol.post_bootstrap.addCallback(self._bootstrap)
             self.protocol.post_bootstrap.addErrback(self.post_bootstrap.errback)
 
-    def _router_begin(self, data):
-        args = data.split()
-        self._router = Router(self.protocol)
-        self._router.from_consensus = True
-        self._router.update(
-            args[1],                  # nickname
-            args[2],                  # idhash
-            args[3],                  # orhash
-            args[4] + ' ' + args[5],  # modified (like '%Y-%m-%f %H:%M:%S')
-            args[6],                  # ip address
-            args[7],                  # ORPort
-            args[8],                  # DirPort
+    def _create_router(self, **kw):
+        id_hex = hexIdFromHash(kw['idhash'])
+        try:
+            router = self.routers[id_hex]
+        except KeyError:
+            router = Router(self.protocol)
+            self.routers[router.id_hex] = router
+        router.from_consensus = True
+        router.update(
+            kw['nickname'],
+            kw['idhash'],
+            kw['orhash'],
+            kw['modified'],
+            kw['ip'],
+            kw['orport'],
+            kw['dirport'],
         )
-
-        if self._router.id_hex in self.routers:
+        router.flags = kw.get('flags', [])
+        if 'bandwidth' in kw:
+            router.bandwidth = kw['bandwidth']
+        if 'ip_v6' in kw:
+            router.ip_v6.extend(kw['ip_v6'])
+
+        if 'guard' in router.flags:
+            self.guards[router.id_hex] = router
+        if 'authority' in router.flags:
+            self.authorities[router.name] = router
+
+        if router.id_hex in self.routers:
             # FIXME should I do an update() on this one??
-            self._router = self.routers[self._router.id_hex]
-            return
-
-        if self._router.name in self.routers_by_name:
-            self.routers_by_name[self._router.name].append(self._router)
-
+            router = self.routers[router.id_hex]
         else:
-            self.routers_by_name[self._router.name] = [self._router]
+            if router.name in self.routers:
+                self.routers[router.name] = None
+
+            else:
+                self.routers[router.name] = router
 
-        if self._router.name in self.routers:
-            self.routers[self._router.name] = None
+        if router.name in self.routers_by_name:
+            self.routers_by_name[router.name].append(router)
 
         else:
-            self.routers[self._router.name] = self._router
-        self.routers[self._router.id_hex] = self._router
-        self.routers_by_hash[self._router.id_hex] = self._router
-        self.all_routers.add(self._router)
-
-    def _router_flags(self, data):
-        args = data.split()
-        self._router.flags = args[1:]
-        if 'guard' in self._router.flags:
-            self.guards[self._router.id_hex] = self._router
-        if 'authority' in self._router.flags:
-            self.authorities[self._router.name] = self._router
-
-    def _router_address(self, data):
-        """only for IPv6 addresses"""
-        self._router.ip_v6.append(data.split()[1].strip())
-
-    def _router_bandwidth(self, data):
-        args = data.split()
-        self._router.bandwidth = int(args[1].split('=')[1])
-
-    def _router_policy(self, data):
-        args = data.split()
-        self._router.policy = args[1:]
-        self._router = None
+            self.routers_by_name[router.name] = [router]
+
+        self.routers[router.id_hex] = router
+        self.routers_by_hash[router.id_hex] = router
+        self.all_routers.add(router)
 
     @defer.inlineCallbacks
     def _bootstrap(self, arg=None):
         "This takes an arg so we can use it as a callback (see __init__)."
 
         # update list of routers (must be before we do the
-        # circuit-status) note that we're feeding each line
-        # incrementally to a state-machine called
-        # _network_status_parser, set up in constructor. "ns" should
-        # be the empty string, but we call _update_network_status for
-        # the de-duplication of named routers
+        # circuit-status)
 
-        ns = yield self.protocol.get_info_incremental(
+        # look out! we're depending on get_info_incremental returning
+        # *lines*, which isn't documented -- but will be true because
+        # TorControlProtocol is a LineReceiver...
+        yield self.protocol.get_info_incremental(
             'ns/all',
-            self._network_status_parser.process
+            self._network_status_parser.feed_line,
         )
-        self._update_network_status(ns)
 
         # update list of existing circuits
         cs = yield self.protocol.get_info_raw('circuit-status')
@@ -453,28 +423,43 @@ class TorState(object):
     def set_attacher(self, attacher, myreactor):
         """
         Provide an :class:`txtorcon.interface.IStreamAttacher` to
-        associate streams to circuits. This won't get turned on until
-        after bootstrapping is completed. ('__LeaveStreamsUnattached'
-        needs to be set to '1' and the existing circuits list needs to
-        be populated).
+        associate streams to circuits.
+
+        You are Strongly Encouraged to **not** use this API directly,
+        and instead use :meth:`txtorcon.Circuit.stream_via` or
+        :meth:`txtorcon.Circuit.web_agent` instead. If you do need to
+        use this API, it's an error if you call either of the other
+        two methods.
+
+        This won't get turned on until after bootstrapping is
+        completed. ('__LeaveStreamsUnattached' needs to be set to '1'
+        and the existing circuits list needs to be populated).
         """
 
         react = IReactorCore(myreactor)
         if attacher:
-            self.attacher = IStreamAttacher(attacher)
+            if self._attacher is attacher:
+                return
+            if self._attacher is not None:
+                raise RuntimeError(
+                    "set_attacher called but we already have an attacher"
+                )
+            self._attacher = IStreamAttacher(attacher)
         else:
-            self.attacher = None
+            self._attacher = None
 
-        if self.attacher is None:
+        if self._attacher is None:
             d = self.undo_attacher()
-            if self.cleanup:
-                react.removeSystemEventTrigger(self.cleanup)
-                self.cleanup = None
+            if self._cleanup:
+                react.removeSystemEventTrigger(self._cleanup)
+                self._cleanup = None
 
         else:
             d = self.protocol.set_conf("__LeaveStreamsUnattached", "1")
-            self.cleanup = react.addSystemEventTrigger('before', 'shutdown',
-                                                       self.undo_attacher)
+            self._cleanup = react.addSystemEventTrigger(
+                'before', 'shutdown',
+                self.undo_attacher,
+            )
         return d
 
     # noqa
@@ -603,9 +588,10 @@ class TorState(object):
                     first = False
                 else:
                     cmd += ','
-                if isinstance(router, basestring) and len(router) == 40 \
+                # XXX should we really accept bytes here?
+                if isinstance(router, bytes) and len(router) == 40 \
                    and hashFromHexId(router):
-                    cmd += router
+                    cmd += router.decode('utf8')
                 else:
                     cmd += router.id_hex[1:]
         d = self.protocol.queue_command(cmd)
@@ -629,7 +615,7 @@ class TorState(object):
         (neither attaching it, nor telling Tor to attach it).
         """
 
-        if self.attacher is None:
+        if self._attacher is None:
             return None
 
         if stream.target_host is not None \
@@ -643,9 +629,10 @@ class TorState(object):
 
         # handle async or sync .attach() the same
         circ_d = defer.maybeDeferred(
-            self.attacher.attach_stream,
+            self._attacher.attach_stream,
             stream, self.circuits,
         )
+        circ_d.addCallback(maybe_coroutine)
 
         # actually do the attachment logic; .attach() can return 3 things:
         #    1. None: let Tor do whatever it wants
@@ -653,13 +640,9 @@ class TorState(object):
         #    3. Circuit instance: attach to the provided circuit
         def issue_stream_attach(circ):
             txtorlog.msg("circuit:", circ)
-            if circ is None:
+            if circ is None or circ is TorState.DO_NOT_ATTACH:
                 # tell Tor to do what it likes
-                return self.protocol.queue_command("ATTACHSTREAM %d 0" % stream.id)
-
-            elif circ is self.DO_NOT_ATTACH:
-                # do nothing; don't attach the stream
-                return
+                return self.protocol.queue_command(b"ATTACHSTREAM %d 0" % stream.id)
 
             else:
                 # should get a Circuit instance; check it for suitability
@@ -679,7 +662,7 @@ class TorState(object):
                     )
                 # we've got a valid Circuit instance; issue the command
                 return self.protocol.queue_command(
-                    "ATTACHSTREAM %d %d" % (stream.id, circ.id)
+                    b"ATTACHSTREAM %d %d" % (stream.id, circ.id)
                 )
 
         circ_d.addCallback(issue_stream_attach)
@@ -731,19 +714,26 @@ class TorState(object):
     def _update_network_status(self, data):
         """
         Used internally as a callback for updating Router information
-        from NS and NEWCONSENSUS events.
+        from NEWCONSENSUS events.
         """
 
-        self.all_routers = set()
-        for line in data.split('\n'):
-            self._network_status_parser.process(line)
+        # XXX why are we getting this with 0 data?
+        if len(data):
+            self.all_routers = set()
+            for line in data.split('\n'):
+                self._network_status_parser.feed_line(line)
+            self._network_status_parser.done()
 
         txtorlog.msg(len(self.routers_by_name), "named routers found.")
         # remove any names we added that turned out to have dups
+        remove_keys = set()
         for (k, v) in self.routers.items():
             if v is None:
                 txtorlog.msg(len(self.routers_by_name[k]), "dups:", k)
-                del self.routers[k]
+                remove_keys.add(k)
+
+        for k in remove_keys:
+            del self.routers[k]
 
         txtorlog.msg(len(self.guards), "GUARDs")
 
@@ -777,7 +767,6 @@ class TorState(object):
         from STREAM events.
         """
 
-        # print("stream_update", line)
         if line.strip() == 'stream-status=':
             # this happens if there are no active streams
             return
@@ -809,7 +798,6 @@ class TorState(object):
 
     event_map = {'STREAM': _stream_update,
                  'CIRC': _circuit_update,
-                 'NS': _update_network_status,
                  'NEWCONSENSUS': _update_network_status,
                  'ADDRMAP': _addr_map}
     """event_map used by add_events to map event_name -> unbound method"""
@@ -822,9 +810,14 @@ class TorState(object):
         for (event, func) in self.event_map.items():
             # the map contains unbound methods, so we bind them
             # to self so they call the right thing
+            try:
+                bound = types.MethodType(func, self, TorState)
+            except TypeError:
+                # python3
+                bound = types.MethodType(func, self)
             yield self.protocol.add_event_listener(
                 event,
-                types.MethodType(func, self, TorState)
+                bound,
             )
 
     # ICircuitContainer
@@ -928,16 +921,23 @@ class TorState(object):
     def circuit_destroy(self, circuit):
         "Used by circuit_closed and circuit_failed (below)"
         txtorlog.msg("circuit_destroy:", circuit.id)
-        for d in circuit._when_built:
-            d.errback(Exception("Destroying circuit; will never hit BUILT"))
+        circuit._when_built.fire(
+            Failure(Exception("Destroying circuit; will never hit BUILT"))
+        )
         del self.circuits[circuit.id]
 
     def circuit_closed(self, circuit, **kw):
         "ICircuitListener API"
         txtorlog.msg("circuit_closed", circuit)
+        circuit._when_built.fire(
+            Failure(Exception("Circuit closed ('{}')".format(_extract_reason(kw))))
+        )
         self.circuit_destroy(circuit)
 
     def circuit_failed(self, circuit, **kw):
         "ICircuitListener API"
         txtorlog.msg("circuit_failed", circuit, str(kw))
+        circuit._when_built.fire(
+            Failure(Exception("Circuit failed ('{}')".format(_extract_reason(kw))))
+        )
         self.circuit_destroy(circuit)
diff --git a/txtorcon/util.py b/txtorcon/util.py
index fd34758..a79fada 100644
--- a/txtorcon/util.py
+++ b/txtorcon/util.py
@@ -14,13 +14,18 @@ import subprocess
 import ipaddress
 import struct
 import re
+import six
 
 from twisted.internet import defer
 from twisted.internet.interfaces import IProtocolFactory
-
 from twisted.internet.endpoints import serverFromString
+from twisted.web.http_headers import Headers
 
 from zope.interface import implementer
+from zope.interface import Interface
+
+if six.PY3:
+    import asyncio
 
 try:
     import GeoIP as _GeoIP
@@ -32,15 +37,35 @@ city = None
 country = None
 asn = None
 
-# XXX probably better to depend on and use "six" for py2/3 stuff?
-try:
-    unicode
-except NameError:
-    py3k = True
-    basestring = str
-else:
-    py3k = False
-    basestring = basestring
+
+def create_tbb_web_headers():
+    """
+    Returns a new `twisted.web.http_headers.Headers` instance
+    populated with tags to mimic Tor Browser. These include values for
+    `User-Agent`, `Accept`, `Accept-Language` and `Accept-Encoding`.
+    """
+    return Headers({
+        b"User-Agent": [b"Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0"],
+        b"Accept": [b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],
+        b"Accept-Language": [b"en-US,en;q=0.5"],
+        b"Accept-Encoding": [b"gzip, deflate"],
+    })
+
+
+def version_at_least(version_string, major, minor, micro, patch):
+    """
+    This returns True if the version_string represents a Tor version
+    of at least ``major``.``minor``.``micro``.``patch`` version,
+    ignoring any trailing specifiers.
+    """
+    parts = re.match(
+        r'^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+).*$',
+        version_string,
+    )
+    for ver, gold in zip(parts.group(1, 2, 3, 4), (major, minor, micro, patch)):
+        if int(ver) < int(gold):
+            return False
+    return True
 
 
 def create_geoip(fname):
@@ -123,10 +148,12 @@ def maybe_ip_addr(addr):
     TODO consider explicitly checking for .exit or .onion at the end?
     """
 
+    if six.PY2 and isinstance(addr, str):
+        addr = unicode(addr)  # noqa
     try:
         return ipaddress.ip_address(addr)
     except ValueError:
-            pass
+        pass
     return str(addr)
 
 
@@ -190,7 +217,7 @@ def process_from_address(addr, port, torstate=None):
     proc = subprocess.Popen(['lsof', '-i', '4tcp@%s:%s' % (addr, port)],
                             stdout=subprocess.PIPE)
     (stdout, stderr) = proc.communicate()
-    lines = stdout.split('\n')
+    lines = stdout.split(b'\n')
     if len(lines) > 1:
         return int(lines[1].split()[1])
 
@@ -217,7 +244,7 @@ def compare_via_hash(x, y):
             hmac_sha256(CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE, y))
 
 
-class NetLocation:
+class NetLocation(object):
     """
     Represents the location of an IP address, either city or country
     level resolution depending on what GeoIP database was loaded. If
@@ -315,4 +342,155 @@ def unescape_quoted_string(string):
     # handeled as escape codes by string.decode('string-escape').
     # This is needed so e.g. '\x00' is not unescaped as '\0'
     string = re.sub(r'((?:^|[^\\])(?:\\\\)*)\\([^ntr0-7\\])', r'\1\2', string)
+    if six.PY3:
+        # XXX hmmm?
+        return bytes(string, 'ascii').decode('unicode-escape')
     return string.decode('string-escape')
+
+
+def default_control_port():
+    """
+    This returns a default control port, which respects an environment
+    variable `TX_CONTROL_PORT`. Without the environment variable, this
+    returns 9151 (the Tor Browser Bundle default).
+
+    You shouldn't use this in "normal" code, this is a convenience for
+    the examples.
+    """
+    try:
+        return int(os.environ['TX_CONTROL_PORT'])
+    except KeyError:
+        return 9151
+
+
+class IListener(Interface):
+    def add(callback):
+        """
+        Add a listener. The arguments to the callback are determined by whomever calls notify()
+        """
+
+    def remove(callback):
+        """
+        Add a listener. The arguments to the callback are determined by whomever calls notify()
+        """
+
+    def notify(*args, **kw):
+        """
+        Calls every listener with the given args and keyword-args.
+
+        XXX errors? just log?
+        """
+
+
+def maybe_coroutine(obj):
+    """
+    If 'obj' is a coroutine and we're using Python3, wrap it in
+    ensureDeferred. Otherwise return the original object.
+
+    (This is to insert in all callback chains from user code, in case
+    that user code is Python3 and used 'async def')
+    """
+    if six.PY3 and asyncio.iscoroutine(obj):
+        return defer.ensureDeferred(obj)
+    return obj
+
+
+ at implementer(IListener)
+class _Listener(object):
+    """
+    Internal helper.
+    """
+
+    def __init__(self):
+        self._listeners = set()
+
+    def add(self, callback):
+        """
+        Add a callback to this listener
+        """
+        self._listeners.add(callback)
+
+    __call__ = add  #: alias for "add"
+
+    def remove(self, callback):
+        """
+        Remove a callback from this listener
+        """
+        self._listeners.remove(callback)
+
+    def notify(self, *args, **kw):
+        """
+        Calls all listeners with the specified args.
+
+        Returns a Deferred which callbacks when all the listeners
+        which return Deferreds have themselves completed.
+        """
+        calls = []
+
+        def failed(fail):
+            # XXX use logger
+            fail.printTraceback()
+
+        for cb in self._listeners:
+            d = defer.maybeDeferred(cb, *args, **kw)
+            d.addCallback(maybe_coroutine)
+            d.addErrback(failed)
+            calls.append(d)
+        return defer.DeferredList(calls)
+
+
+class _ListenerCollection(object):
+    """
+    Internal helper.
+
+    This collects all your valid event listeners together in one
+    object if you want.
+    """
+    def __init__(self, valid_events):
+        self._valid_events = valid_events
+        for e in valid_events:
+            setattr(self, e, _Listener())
+
+    def __call__(self, event, callback):
+        if event not in self._valid_events:
+            raise Exception("Invalid event '{}'".format(event))
+        getattr(self, event).add(callback)
+
+    def remove(self, event, callback):
+        if event not in self._valid_events:
+            raise Exception("Invalid event '{}'".format(event))
+        getattr(self, event).remove(callback)
+
+    def notify(self, event, *args, **kw):
+        if event not in self._valid_events:
+            raise Exception("Invalid event '{}'".format(event))
+        getattr(self, event).notify(*args, **kw)
+
+
+# similar to OneShotObserverList in Tahoe-LAFS
+class SingleObserver(object):
+    """
+    A helper for ".when_*()" sort of functions.
+    """
+    _NotFired = object()
+
+    def __init__(self):
+        self._observers = []
+        self._fired = self._NotFired
+
+    def when_fired(self):
+        d = defer.Deferred()
+        if self._fired is not self._NotFired:
+            d.callback(self._fired)
+        else:
+            self._observers.append(d)
+        return d
+
+    def fire(self, value):
+        if self._observers is None:
+            return  # raise RuntimeError("already fired") ?
+        self._fired = value
+        for d in self._observers:
+            d.callback(self._fired)
+        self._observers = None
+        return value  # so we're transparent if used as a callback
diff --git a/txtorcon/web.py b/txtorcon/web.py
new file mode 100644
index 0000000..fd03196
--- /dev/null
+++ b/txtorcon/web.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import with_statement
+
+from twisted.web.iweb import IAgentEndpointFactory
+from twisted.web.client import Agent
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint
+
+from zope.interface import implementer
+
+from txtorcon.socks import TorSocksEndpoint
+from txtorcon.log import txtorlog
+
+
+ at implementer(IAgentEndpointFactory)
+class _AgentEndpointFactoryUsingTor(object):
+    def __init__(self, reactor, tor_socks_endpoint):
+        self._reactor = reactor
+        self._proxy_ep = tor_socks_endpoint
+        # XXX could accept optional "tls=" to do something besides
+        # optionsForClientTLS(hostname)?
+
+    def endpointForURI(self, uri):
+        return TorSocksEndpoint(
+            self._proxy_ep,
+            uri.host,
+            uri.port,
+            tls=(uri.scheme == b'https'),
+        )
+
+
+ at implementer(IAgentEndpointFactory)
+class _AgentEndpointFactoryForCircuit(object):
+    def __init__(self, reactor, tor_socks_endpoint, circ):
+        self._reactor = reactor
+        self._socks_ep = tor_socks_endpoint
+        self._circ = circ
+
+    def endpointForURI(self, uri):
+        """IAgentEndpointFactory API"""
+        torsocks = TorSocksEndpoint(
+            self._socks_ep,
+            uri.host, uri.port,
+            tls=uri.scheme == b'https',
+        )
+        from txtorcon.circuit import TorCircuitEndpoint
+        return TorCircuitEndpoint(
+            self._reactor, self._circ._torstate, self._circ, torsocks,
+        )
+
+
+def tor_agent(reactor, socks_endpoint, circuit=None, pool=None):
+    """
+    This is the low-level method used by
+    :meth:`txtorcon.Tor.web_agent` and
+    :meth:`txtorcon.Circuit.web_agent` -- probably you should call one
+    of those instead.
+
+    :returns: a Deferred that fires with an object that implements
+        :class:`twisted.web.iweb.IAgent` and is thus suitable for passing
+        to ``treq`` as the ``agent=`` kwarg. Of course can be used
+        directly; see `using Twisted web cliet
+        <http://twistedmatrix.com/documents/current/web/howto/client.html>`_.
+
+    :param reactor: the reactor to use
+
+    :param circuit: If supplied, a particular circuit to use
+
+    :param socks_endpoint: Deferred that fires w/
+        IStreamClientEndpoint (or IStreamClientEndpoint instance)
+        which points at a SOCKS5 port of our Tor
+
+    :param pool: passed on to the Agent (as ``pool=``)
+    """
+
+    if socks_endpoint is None:
+        raise ValueError(
+            "Must provide socks_endpoint as Deferred or IStreamClientEndpoint"
+        )
+    if circuit is not None:
+        factory = _AgentEndpointFactoryForCircuit(reactor, socks_endpoint, circuit)
+    else:
+        factory = _AgentEndpointFactoryUsingTor(reactor, socks_endpoint)
+    return Agent.usingEndpointFactory(reactor, factory, pool=pool)
+
+
+ at inlineCallbacks
+def agent_for_socks_port(reactor, torconfig, socks_config, pool=None):
+    """
+    This returns a Deferred that fires with an object that implements
+    :class:`twisted.web.iweb.IAgent` and is thus suitable for passing
+    to ``treq`` as the ``agent=`` kwarg. Of course can be used
+    directly; see `using Twisted web cliet
+    <http://twistedmatrix.com/documents/current/web/howto/client.html>`_. If
+    you have a :class:`txtorcon.Tor` instance already, the preferred
+    API is to call :meth:`txtorcon.Tor.web_agent` on it.
+
+    :param torconfig: a :class:`txtorcon.TorConfig` instance.
+
+    :param socks_config: anything valid for Tor's ``SocksPort``
+        option. This is generally just a TCP port (e.g. ``9050``), but
+        can also be a unix path like so ``unix:/path/to/socket`` (Tor
+        has restrictions on the ownership/permissions of the directory
+        containing ``socket``). If the given SOCKS option is not
+        already available in the underlying Tor instance, it is
+        re-configured to add the SOCKS option.
+    """
+    # :param tls: True (the default) will use Twisted's default options
+    #     with the hostname in the URI -- that is, TLS verification
+    #     similar to a Browser. Otherwise, you can pass whatever Twisted
+    #     returns for `optionsForClientTLS
+    #     <https://twistedmatrix.com/documents/current/api/twisted.internet.ssl.optionsForClientTLS.html>`_
+
+    socks_config = str(socks_config)  # sadly, all lists are lists-of-strings to Tor :/
+    if socks_config not in torconfig.SocksPort:
+        txtorlog.msg("Adding SOCKS port '{}' to Tor".format(socks_config))
+        torconfig.SocksPort.append(socks_config)
+        try:
+            yield torconfig.save()
+        except Exception as e:
+            raise RuntimeError(
+                "Failed to reconfigure Tor with SOCKS port '{}': {}".format(
+                    socks_config, str(e)
+                )
+            )
+
+    if socks_config.startswith('unix:'):
+        socks_ep = UNIXClientEndpoint(reactor, socks_config[5:])
+    else:
+        if ':' in socks_config:
+            host, port = socks_config.split(':', 1)
+        else:
+            host = '127.0.0.1'
+            port = int(socks_config)
+        socks_ep = TCP4ClientEndpoint(reactor, host, port)
+
+    returnValue(
+        Agent.usingEndpointFactory(
+            reactor,
+            _AgentEndpointFactoryUsingTor(reactor, socks_ep),
+            pool=pool,
+        )
+    )

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/txtorcon.git



More information about the Pkg-privacy-commits mailing list