[Pkg-privacy-commits] [obfs4proxy] 83/151: Massive cleanup/code reorg.

Ximin Luo infinity0 at moszumanska.debian.org
Sat Aug 22 12:59:41 UTC 2015


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

infinity0 pushed a commit to branch master
in repository obfs4proxy.

commit 339c63f0c8cd4374f6fa26484498eb6fa91b7bca
Author: Yawning Angel <yawning at torproject.org>
Date:   Sun Aug 17 17:11:03 2014 +0000

    Massive cleanup/code reorg.
    
     * Changed obfs4proxy to be more like obfsproxy in terms of design,
       including being an easy framework for developing new TCP/IP style
       pluggable transports.
     * Added support for also acting as an obfs2/obfs3 client or bridge
       as a transition measure (and because the code itself is trivial).
     * Massively cleaned up the obfs4 and related code to be easier to
       read, and more idiomatic Go-like in style.
     * To ease deployment, obfs4proxy will now autogenerate the node-id,
       curve25519 keypair, and drbg seed if none are specified, and save
       them to a JSON file in the pt_state directory (Fixes Tor bug #12605).
---
 README.md                                          |  23 +-
 {csrand => common/csrand}/csrand.go                |  21 +-
 {drbg => common/drbg}/hash_drbg.go                 |  20 +-
 {ntor => common/ntor}/ntor.go                      |   9 +-
 {ntor => common/ntor}/ntor_test.go                 |   2 -
 .../probdist/weighted_dist.go                      |  56 +-
 .../probdist/weighted_dist_test.go                 |  10 +-
 common/replayfilter/replay_filter.go               | 147 ++++
 .../replayfilter/replay_filter_test.go             |  29 +-
 common/uniformdh/uniformdh.go                      | 183 +++++
 common/uniformdh/uniformdh_test.go                 | 220 ++++++
 obfs4.go                                           | 758 ---------------------
 obfs4proxy/obfs4proxy.go                           | 553 +++++++--------
 obfs4proxy/proxy_extras.go                         |  51 --
 obfs4proxy/proxy_http.go                           |   3 -
 obfs4proxy/proxy_socks4.go                         |   2 -
 obfs4proxy/pt_extras.go                            |  23 +-
 replay_filter.go                                   | 145 ----
 transports/base/base.go                            |  88 +++
 transports/obfs2/obfs2.go                          | 367 ++++++++++
 transports/obfs3/obfs3.go                          | 358 ++++++++++
 {framing => transports/obfs4/framing}/framing.go   |  10 +-
 .../obfs4/framing}/framing_test.go                 |   2 -
 .../obfs4/handshake_ntor.go                        |  13 +-
 .../obfs4/handshake_ntor_test.go                   |   5 +-
 transports/obfs4/obfs4.go                          | 579 ++++++++++++++++
 packet.go => transports/obfs4/packet.go            |  77 +--
 transports/obfs4/statefile.go                      | 156 +++++
 transports/transports.go                           |  91 +++
 29 files changed, 2563 insertions(+), 1438 deletions(-)

diff --git a/README.md b/README.md
index c97588a..3ee9c0c 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,6 @@
 ## obfs4 - The obfourscator
 #### Yawning Angel (yawning at torproject dot org)
 
-### WARNING
-
-This is pre-alpha.  Don't expect any security or wire protocol stability yet.
-If you want to use something like this, you should currently probably be looking
-at ScrambleSuit.
-
 ### What?
 
 This is a look-like nothing obfuscation protocol that incorporates ideas and
@@ -22,6 +16,9 @@ The notable differences between ScrambleSuit and obfs4:
    obfuscated via the Elligator 2 mapping.
  * The link layer encryption uses NaCl secret boxes (Poly1305/XSalsa20).
 
+As an added bonus, obfs4proxy also supports acting as an obfs2/3 client and
+bridge to ease the transition to the new protocol.
+
 ### Why not extend ScrambleSuit?
 
 It's my protocol and I'll obfuscate if I want to.
@@ -43,20 +40,6 @@ listed for clarity.
  * SipHash-2-4 (https://github.com/dchest/siphash)
  * goptlib (https://git.torproject.org/pluggable-transports/goptlib.git)
 
-### TODO
-
- * Code cleanups.
- * Write more unit tests.
- * Optimize further.
-
-### WON'T DO
-
- * I do not care that much about standalone mode.  Patches *MAY* be accepted,
-   especially if they are clean and are useful to Tor users.
- * Yes, I use a bunch of code from the borg^w^wGoogle.  If that bothers you
-   feel free to write your own implementation.
- * I do not care about older versions of the go runtime.
-
 ### Thanks
 
  * David Fifield for goptlib.
diff --git a/csrand/csrand.go b/common/csrand/csrand.go
similarity index 86%
rename from csrand/csrand.go
rename to common/csrand/csrand.go
index b059ed0..45849d3 100644
--- a/csrand/csrand.go
+++ b/common/csrand/csrand.go
@@ -29,7 +29,7 @@
 // with some utility functions for common random number/byte related tasks.
 //
 // Not all of the convinience routines are replicated, only those that are
-// useful for obfs4.  The CsRand variable provides access to the full math/rand
+// immediately useful.  The Rand variable provides access to the full math/rand
 // API.
 package csrand
 
@@ -44,8 +44,8 @@ import (
 var (
 	csRandSourceInstance csRandSource
 
-	// CsRand is a math/rand instance backed by crypto/rand CSPRNG.
-	CsRand = rand.New(csRandSourceInstance)
+	// Rand is a math/rand instance backed by crypto/rand CSPRNG.
+	Rand = rand.New(csRandSourceInstance)
 )
 
 type csRandSource struct {
@@ -54,8 +54,7 @@ type csRandSource struct {
 
 func (r csRandSource) Int63() int64 {
 	var src [8]byte
-	err := Bytes(src[:])
-	if err != nil {
+	if err := Bytes(src[:]); err != nil {
 		panic(err)
 	}
 	val := binary.BigEndian.Uint64(src[:])
@@ -70,12 +69,12 @@ func (r csRandSource) Seed(seed int64) {
 
 // Intn returns, as a int, a pseudo random number in [0, n).
 func Intn(n int) int {
-	return CsRand.Intn(n)
+	return Rand.Intn(n)
 }
 
 // Float64 returns, as a float64, a pesudo random number in [0.0,1.0).
 func Float64() float64 {
-	return CsRand.Float64()
+	return Rand.Float64()
 }
 
 // IntRange returns a uniformly distributed int [min, max].
@@ -85,18 +84,18 @@ func IntRange(min, max int) int {
 	}
 
 	r := (max + 1) - min
-	ret := CsRand.Intn(r)
+	ret := Rand.Intn(r)
 	return ret + min
 }
 
 // Bytes fills the slice with random data.
 func Bytes(buf []byte) error {
-	_, err := io.ReadFull(cryptRand.Reader, buf)
-	if err != nil {
+	if _, err := io.ReadFull(cryptRand.Reader, buf); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-/* vim :set ts=4 sw=4 sts=4 noet : */
+// Reader is a alias of rand.Reader.
+var Reader = cryptRand.Reader
diff --git a/drbg/hash_drbg.go b/common/drbg/hash_drbg.go
similarity index 91%
rename from drbg/hash_drbg.go
rename to common/drbg/hash_drbg.go
index c94902a..5329828 100644
--- a/drbg/hash_drbg.go
+++ b/common/drbg/hash_drbg.go
@@ -37,7 +37,7 @@ import (
 
 	"github.com/dchest/siphash"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
 )
 
 // Size is the length of the HashDrbg output.
@@ -63,8 +63,7 @@ func (seed *Seed) Base64() string {
 // NewSeed returns a Seed initialized with the runtime CSPRNG.
 func NewSeed() (seed *Seed, err error) {
 	seed = new(Seed)
-	err = csrand.Bytes(seed.Bytes()[:])
-	if err != nil {
+	if err = csrand.Bytes(seed.Bytes()[:]); err != nil {
 		return nil, err
 	}
 
@@ -88,8 +87,7 @@ func SeedFromBytes(src []byte) (seed *Seed, err error) {
 // SeedLength as appropriate.
 func SeedFromBase64(encoded string) (seed *Seed, err error) {
 	var raw []byte
-	raw, err = base64.StdEncoding.DecodeString(encoded)
-	if err != nil {
+	if raw, err = base64.StdEncoding.DecodeString(encoded); err != nil {
 		return nil, err
 	}
 
@@ -112,12 +110,18 @@ type HashDrbg struct {
 
 // NewHashDrbg makes a HashDrbg instance based off an optional seed.  The seed
 // is truncated to SeedLength.
-func NewHashDrbg(seed *Seed) *HashDrbg {
+func NewHashDrbg(seed *Seed) (*HashDrbg, error) {
 	drbg := new(HashDrbg)
+	if seed == nil {
+		var err error
+		if seed, err = NewSeed(); err != nil {
+			return nil, err
+		}
+	}
 	drbg.sip = siphash.New(seed.Bytes()[:16])
 	copy(drbg.ofb[:], seed.Bytes()[16:])
 
-	return drbg
+	return drbg, nil
 }
 
 // Int63 returns a uniformly distributed random integer [0, 1 << 63).
@@ -143,5 +147,3 @@ func (drbg *HashDrbg) NextBlock() []byte {
 	copy(ret, drbg.ofb[:])
 	return ret
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/ntor/ntor.go b/common/ntor/ntor.go
similarity index 98%
rename from ntor/ntor.go
rename to common/ntor/ntor.go
index b178454..37cfe88 100644
--- a/ntor/ntor.go
+++ b/common/ntor/ntor.go
@@ -25,7 +25,6 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-//
 // Package ntor implements the Tor Project's ntor handshake as defined in
 // proposal 216 "Improved circuit-creation key exchange".  It also supports
 // using Elligator to transform the Curve25519 public keys sent over the wire
@@ -33,7 +32,6 @@
 //
 // Before using this package, it is strongly recommended that the specification
 // is read and understood.
-//
 package ntor
 
 import (
@@ -50,7 +48,7 @@ import (
 
 	"github.com/agl/ed25519/extra25519"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
 )
 
 const (
@@ -267,8 +265,7 @@ func NewKeypair(elligator bool) (*Keypair, error) {
 		// Generate a Curve25519 private key.  Like everyone who does this,
 		// run the CSPRNG output through SHA256 for extra tinfoil hattery.
 		priv := keypair.private.Bytes()[:]
-		err := csrand.Bytes(priv)
-		if err != nil {
+		if err := csrand.Bytes(priv); err != nil {
 			return nil, err
 		}
 		digest := sha256.Sum256(priv)
@@ -433,5 +430,3 @@ func Kdf(keySeed []byte, okmLen int) []byte {
 
 	return okm
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/ntor/ntor_test.go b/common/ntor/ntor_test.go
similarity index 99%
rename from ntor/ntor_test.go
rename to common/ntor/ntor_test.go
index 9d7c687..c92c04e 100644
--- a/ntor/ntor_test.go
+++ b/common/ntor/ntor_test.go
@@ -178,5 +178,3 @@ func BenchmarkHandshake(b *testing.B) {
 		}
 	}
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/weighted_dist.go b/common/probdist/weighted_dist.go
similarity index 77%
rename from weighted_dist.go
rename to common/probdist/weighted_dist.go
index 4f1f2a5..2386bbe 100644
--- a/weighted_dist.go
+++ b/common/probdist/weighted_dist.go
@@ -25,15 +25,18 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-package obfs4
+// Package probdist implements a weighted probability distribution suitable for
+// protocol parameterization.  To allow for easy reproduction of a given
+// distribution, the drbg package is used as the random number source.
+package probdist
 
 import (
 	"container/list"
 	"fmt"
 	"math/rand"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
-	"git.torproject.org/pluggable-transports/obfs4.git/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
 )
 
 const (
@@ -41,10 +44,11 @@ const (
 	maxValues = 100
 )
 
-// wDist is a weighted distribution.
-type wDist struct {
+// WeightedDist is a weighted distribution.
+type WeightedDist struct {
 	minValue int
 	maxValue int
+	biased   bool
 	values   []int
 	weights  []float64
 
@@ -52,23 +56,25 @@ type wDist struct {
 	prob  []float64
 }
 
-// newWDist creates a weighted distribution of values ranging from min to max
-// based on a HashDrbg initialized with seed.
-func newWDist(seed *drbg.Seed, min, max int) (w *wDist) {
-	w = &wDist{minValue: min, maxValue: max}
+// New creates a weighted distribution of values ranging from min to max
+// based on a HashDrbg initialized with seed.  Optionally, bias the weight
+// generation to match the ScrambleSuit non-uniform distribution from
+// obfsproxy.
+func New(seed *drbg.Seed, min, max int, biased bool) (w *WeightedDist) {
+	w = &WeightedDist{minValue: min, maxValue: max, biased: biased}
 
 	if max <= min {
 		panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max))
 	}
 
-	w.reset(seed)
+	w.Reset(seed)
 
 	return
 }
 
 // genValues creates a slice containing a random number of random values
 // that when scaled by adding minValue will fall into [min, max].
-func (w *wDist) genValues(rng *rand.Rand) {
+func (w *WeightedDist) genValues(rng *rand.Rand) {
 	nValues := (w.maxValue + 1) - w.minValue
 	values := rng.Perm(nValues)
 	if nValues < minValues {
@@ -83,11 +89,11 @@ func (w *wDist) genValues(rng *rand.Rand) {
 
 // genBiasedWeights generates a non-uniform weight list, similar to the
 // ScrambleSuit prob_dist module.
-func (w *wDist) genBiasedWeights(rng *rand.Rand) {
+func (w *WeightedDist) genBiasedWeights(rng *rand.Rand) {
 	w.weights = make([]float64, len(w.values))
 
 	culmProb := 0.0
-	for i := range w.values {
+	for i := range w.weights {
 		p := (1.0 - culmProb) * rng.Float64()
 		w.weights[i] = p
 		culmProb += p
@@ -95,7 +101,7 @@ func (w *wDist) genBiasedWeights(rng *rand.Rand) {
 }
 
 // genUniformWeights generates a uniform weight list.
-func (w *wDist) genUniformWeights(rng *rand.Rand) {
+func (w *WeightedDist) genUniformWeights(rng *rand.Rand) {
 	w.weights = make([]float64, len(w.values))
 	for i := range w.weights {
 		w.weights[i] = rng.Float64()
@@ -104,7 +110,7 @@ func (w *wDist) genUniformWeights(rng *rand.Rand) {
 
 // genTables calculates the alias and prob tables used for Vose's Alias method.
 // Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/
-func (w *wDist) genTables() {
+func (w *WeightedDist) genTables() {
 	n := len(w.weights)
 	var sum float64
 	for _, weight := range w.weights {
@@ -179,20 +185,24 @@ func (w *wDist) genTables() {
 	w.alias = alias
 }
 
-// reset generates a new distribution with the same min/max based on a new seed.
-func (w *wDist) reset(seed *drbg.Seed) {
+// Reset generates a new distribution with the same min/max based on a new
+// seed.
+func (w *WeightedDist) Reset(seed *drbg.Seed) {
 	// Initialize the deterministic random number generator.
-	drbg := drbg.NewHashDrbg(seed)
+	drbg, _ := drbg.NewHashDrbg(seed)
 	rng := rand.New(drbg)
 
 	w.genValues(rng)
-	//w.genBiasedWeights(rng)
-	w.genUniformWeights(rng)
+	if w.biased {
+		w.genBiasedWeights(rng)
+	} else {
+		w.genUniformWeights(rng)
+	}
 	w.genTables()
 }
 
-// sample generates a random value according to the distribution.
-func (w *wDist) sample() int {
+// Sample generates a random value according to the distribution.
+func (w *WeightedDist) Sample() int {
 	var idx int
 
 	// Generate a fair die roll from an $n$-sided die; call the side $i$.
@@ -208,5 +218,3 @@ func (w *wDist) sample() int {
 
 	return w.minValue + w.values[idx]
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/weighted_dist_test.go b/common/probdist/weighted_dist_test.go
similarity index 93%
rename from weighted_dist_test.go
rename to common/probdist/weighted_dist_test.go
index 16b93c4..b705add 100644
--- a/weighted_dist_test.go
+++ b/common/probdist/weighted_dist_test.go
@@ -25,13 +25,13 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-package obfs4
+package probdist
 
 import (
 	"fmt"
 	"testing"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
 )
 
 const debug = false
@@ -46,7 +46,7 @@ func TestWeightedDist(t *testing.T) {
 
 	hist := make([]int, 1000)
 
-	w := newWDist(seed, 0, 999)
+	w := New(seed, 0, 999, true)
 	if debug {
 		// Dump a string representation of the probability table.
 		fmt.Println("Table:")
@@ -64,7 +64,7 @@ func TestWeightedDist(t *testing.T) {
 	}
 
 	for i := 0; i < nrTrials; i++ {
-		value := w.sample()
+		value := w.Sample()
 		hist[value]++
 	}
 
@@ -78,5 +78,3 @@ func TestWeightedDist(t *testing.T) {
 		}
 	}
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/common/replayfilter/replay_filter.go b/common/replayfilter/replay_filter.go
new file mode 100644
index 0000000..95cc5d6
--- /dev/null
+++ b/common/replayfilter/replay_filter.go
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package replayfilter implements a generic replay detection filter with a
+// caller specifiable time-to-live.  It only detects if a given byte sequence
+// has been seen before based on the SipHash-2-4 digest of the sequence.
+// Collisions are treated as positive matches, though the probability of this
+// happening is negligible.
+package replayfilter
+
+import (
+	"container/list"
+	"encoding/binary"
+	"sync"
+	"time"
+
+	"github.com/dchest/siphash"
+
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+)
+
+// maxFilterSize is the maximum capacity of a replay filter.  This value is
+// more as a safeguard to prevent runaway filter growth, and is sized to be
+// serveral orders of magnitude greater than the number of connections a busy
+// bridge sees in one day, so in practice should never be reached.
+const maxFilterSize = 100 * 1024
+
+type entry struct {
+	digest    uint64
+	firstSeen time.Time
+	element   *list.Element
+}
+
+// ReplayFilter is a simple filter designed only to detect if a given byte
+// sequence has been seen before.
+type ReplayFilter struct {
+	sync.Mutex
+
+	filter map[uint64]*entry
+	fifo   *list.List
+
+	key [2]uint64
+	ttl time.Duration
+}
+
+// New creates a new ReplayFilter instance.
+func New(ttl time.Duration) (filter *ReplayFilter, err error) {
+	// Initialize the SipHash-2-4 instance with a random key.
+	var key [16]byte
+	if err = csrand.Bytes(key[:]); err != nil {
+		return
+	}
+
+	filter = new(ReplayFilter)
+	filter.filter = make(map[uint64]*entry)
+	filter.fifo = list.New()
+	filter.key[0] = binary.BigEndian.Uint64(key[0:8])
+	filter.key[1] = binary.BigEndian.Uint64(key[8:16])
+	filter.ttl = ttl
+
+	return
+}
+
+// TestAndSet queries the filter for a given byte sequence, inserts the
+// sequence, and returns if it was present before the insertion operation.
+func (f *ReplayFilter) TestAndSet(now time.Time, buf []byte) bool {
+	digest := siphash.Hash(f.key[0], f.key[1], buf)
+
+	f.Lock()
+	defer f.Unlock()
+
+	f.compactFilter(now)
+
+	if e := f.filter[digest]; e != nil {
+		// Hit.  Just return.
+		return true
+	}
+
+	// Miss.  Add a new entry.
+	e := new(entry)
+	e.digest = digest
+	e.firstSeen = now
+	e.element = f.fifo.PushBack(e)
+	f.filter[digest] = e
+
+	return false
+}
+
+func (f *ReplayFilter) compactFilter(now time.Time) {
+	e := f.fifo.Front()
+	for e != nil {
+		ent, _ := e.Value.(*entry)
+
+		// If the filter is not full, only purge entries that exceed the TTL,
+		// otherwise purge at least one entry, then revert to TTL based
+		// compaction.
+		if f.fifo.Len() < maxFilterSize && f.ttl > 0 {
+			deltaT := now.Sub(ent.firstSeen)
+			if deltaT < 0 {
+				// Aeeeeeee, the system time jumped backwards, potentially by
+				// a lot.  This will eventually self-correct, but "eventually"
+				// could be a long time.  As much as this sucks, jettison the
+				// entire filter.
+				f.reset()
+				return
+			} else if deltaT < f.ttl {
+				return
+			}
+		}
+
+		// Remove the eldest entry.
+		eNext := e.Next()
+		delete(f.filter, ent.digest)
+		f.fifo.Remove(ent.element)
+		ent.element = nil
+		e = eNext
+	}
+}
+
+func (f *ReplayFilter) reset() {
+	f.filter = make(map[uint64]*entry)
+	f.fifo = list.New()
+}
diff --git a/replay_filter_test.go b/common/replayfilter/replay_filter_test.go
similarity index 87%
rename from replay_filter_test.go
rename to common/replayfilter/replay_filter_test.go
index 09337c0..884e4fb 100644
--- a/replay_filter_test.go
+++ b/common/replayfilter/replay_filter_test.go
@@ -25,55 +25,58 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-package obfs4
+package replayfilter
 
 import (
 	"testing"
+	"time"
 )
 
 func TestReplayFilter(t *testing.T) {
-	f, err := newReplayFilter()
+	ttl := 10 * time.Second
+
+	f, err := New(ttl)
 	if err != nil {
 		t.Fatal("newReplayFilter failed:", err)
 	}
 
 	buf := []byte("This is a test of the Emergency Broadcast System.")
-	var now int64 = 3600
+	now := time.Now()
 
 	// testAndSet into empty filter, returns false (not present).
-	set := f.testAndSet(now, buf)
+	set := f.TestAndSet(now, buf)
 	if set {
-		t.Fatal("testAndSet empty filter returned true")
+		t.Fatal("TestAndSet empty filter returned true")
 	}
 
 	// testAndSet into filter containing entry, should return true(present).
-	set = f.testAndSet(now, buf)
+	set = f.TestAndSet(now, buf)
 	if !set {
 		t.Fatal("testAndSet populated filter (replayed) returned false")
 	}
 
 	buf2 := []byte("This concludes this test of the Emergency Broadcast System.")
-	now += 3600 * 2
+	now = now.Add(ttl)
 
 	// testAndSet with time advanced.
-	set = f.testAndSet(now, buf2)
+	set = f.TestAndSet(now, buf2)
 	if set {
 		t.Fatal("testAndSet populated filter, 2nd entry returned true")
 	}
-	set = f.testAndSet(now, buf2)
+	set = f.TestAndSet(now, buf2)
 	if !set {
 		t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false")
 	}
 
 	// Ensure that the first entry has been removed by compact.
-	set = f.testAndSet(now, buf)
+	set = f.TestAndSet(now, buf)
 	if set {
 		t.Fatal("testAndSet populated filter, compact check returned true")
 	}
 
 	// Ensure that the filter gets reaped if the clock jumps backwards.
-	now = 0
-	set = f.testAndSet(now, buf)
+	now = time.Time{}
+	set = f.TestAndSet(now, buf)
 	if set {
 		t.Fatal("testAndSet populated filter, backward time jump returned true")
 	}
@@ -85,7 +88,7 @@ func TestReplayFilter(t *testing.T) {
 	}
 
 	// Ensure that the entry is properly added after reaping.
-	set = f.testAndSet(now, buf)
+	set = f.TestAndSet(now, buf)
 	if !set {
 		t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false")
 	}
diff --git a/common/uniformdh/uniformdh.go b/common/uniformdh/uniformdh.go
new file mode 100644
index 0000000..ab94a2e
--- /dev/null
+++ b/common/uniformdh/uniformdh.go
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package uniformdh implements the Tor Project's UniformDH key exchange
+// mechanism as defined in the obfs3 protocol specification.  This
+// implementation is suitable for obfuscation but MUST NOT BE USED when strong
+// security is required as it is not constant time.
+package uniformdh
+
+import (
+	"fmt"
+	"io"
+	"math/big"
+)
+
+const (
+	// Size is the size of a UniformDH key or shared secret in bytes.
+	Size = 1536 / 8
+
+	// modpStr is the RFC3526 1536-bit MODP Group (Group 5).
+	modpStr = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
+		"29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
+		"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
+		"E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
+		"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" +
+		"C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" +
+		"83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
+		"670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF"
+
+	g = 2
+)
+
+var modpGroup *big.Int
+var gen *big.Int
+
+// A PrivateKey represents a UniformDH private key.
+type PrivateKey struct {
+	PublicKey
+	privateKey *big.Int
+}
+
+// A PublicKey represents a UniformDH public key.
+type PublicKey struct {
+	bytes     []byte
+	publicKey *big.Int
+}
+
+// Bytes returns the byte representation of a PublicKey.
+func (pub *PublicKey) Bytes() (pubBytes []byte, err error) {
+	if len(pub.bytes) != Size || pub.bytes == nil {
+		return nil, fmt.Errorf("public key is not initialized")
+	}
+	pubBytes = make([]byte, Size)
+	copy(pubBytes, pub.bytes)
+
+	return
+}
+
+// SetBytes sets the PublicKey from a byte slice.
+func (pub *PublicKey) SetBytes(pubBytes []byte) error {
+	if len(pubBytes) != Size {
+		return fmt.Errorf("public key length %d is not %d", len(pubBytes), Size)
+	}
+	pub.bytes = make([]byte, Size)
+	copy(pub.bytes, pubBytes)
+	pub.publicKey = new(big.Int).SetBytes(pub.bytes)
+
+	return nil
+}
+
+// GenerateKey generates a UniformDH keypair using the random source random.
+func GenerateKey(random io.Reader) (priv *PrivateKey, err error) {
+	privBytes := make([]byte, Size)
+	if _, err = io.ReadFull(random, privBytes); err != nil {
+		return
+	}
+	priv, err = generateKey(privBytes)
+
+	return
+}
+
+func generateKey(privBytes []byte) (priv *PrivateKey, err error) {
+	// This function does all of the actual heavy lifting of creating a public
+	// key from a raw 192 byte private key.  It is split so that the KAT tests
+	// can be written easily, and not exposed since non-ephemeral keys are a
+	// terrible idea.
+
+	if len(privBytes) != Size {
+		return nil, fmt.Errorf("invalid private key size %d", len(privBytes))
+	}
+
+	// To pick a private UniformDH key, we pick a random 1536-bit number,
+	// and make it even by setting its low bit to 0
+	privBn := new(big.Int).SetBytes(privBytes)
+	wasEven := privBn.Bit(0) == 0
+	privBn = privBn.SetBit(privBn, 0, 0)
+
+	// Let x be that private key, and X = g^x (mod p).
+	pubBn := new(big.Int).Exp(gen, privBn, modpGroup)
+	pubAlt := new(big.Int).Sub(modpGroup, pubBn)
+
+	// When someone sends her public key to the other party, she randomly
+	// decides whether to send X or p-X.  Use the lowest most bit of the
+	// private key here as the random coin flip since it is masked out and not
+	// used.
+	//
+	// Note: The spec doesn't explicitly specify it, but here we prepend zeros
+	// to the key so that it is always exactly Size bytes.
+	pubBytes := make([]byte, Size)
+	if wasEven {
+		err = prependZeroBytes(pubBytes, pubBn.Bytes())
+	} else {
+		err = prependZeroBytes(pubBytes, pubAlt.Bytes())
+	}
+	if err != nil {
+		return
+	}
+
+	priv = new(PrivateKey)
+	priv.PublicKey.bytes = pubBytes
+	priv.PublicKey.publicKey = pubBn
+	priv.privateKey = privBn
+
+	return
+}
+
+// Handshake generates a shared secret given a PrivateKey and PublicKey.
+func Handshake(privateKey *PrivateKey, publicKey *PublicKey) (sharedSecret []byte, err error) {
+	// When a party wants to calculate the shared secret, she raises the
+	// foreign public key to her private key.
+	secretBn := new(big.Int).Exp(publicKey.publicKey, privateKey.privateKey, modpGroup)
+	sharedSecret = make([]byte, Size)
+	err = prependZeroBytes(sharedSecret, secretBn.Bytes())
+
+	return
+}
+
+func prependZeroBytes(dst, src []byte) error {
+	zeros := len(dst) - len(src)
+	if zeros < 0 {
+		return fmt.Errorf("src length is greater than destination: %d", zeros)
+	}
+	for i := 0; i < zeros; i++ {
+		dst[i] = 0
+	}
+	copy(dst[zeros:], src)
+
+	return nil
+}
+
+func init() {
+	// Load the MODP group and the generator.
+	var ok bool
+	modpGroup, ok = new(big.Int).SetString(modpStr, 16)
+	if !ok {
+		panic("Failed to load the RFC3526 MODP Group")
+	}
+	gen = big.NewInt(g)
+}
diff --git a/common/uniformdh/uniformdh_test.go b/common/uniformdh/uniformdh_test.go
new file mode 100644
index 0000000..d705d66
--- /dev/null
+++ b/common/uniformdh/uniformdh_test.go
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package uniformdh
+
+import (
+	"bytes"
+	"crypto/rand"
+	"encoding/hex"
+	"testing"
+)
+
+const (
+	xPrivStr = "6f592d676f536874746f20686e6b776f" +
+		"20736874206561676574202e6f592d67" +
+		"6f536874746f20687369742065686720" +
+		"74612e655920676f532d746f6f686874" +
+		"6920207368742065656b20796e612064" +
+		"7567726169646e616f20206668742065" +
+		"61676574202e61507473202c72707365" +
+		"6e652c746620747572752c6561206c6c" +
+		"612065726f20656e6920206e6f592d67" +
+		"6f536874746f2e68482020656e6b776f" +
+		"2073687772652065687420656c4f2064" +
+		"6e4f736562206f72656b74207268756f"
+
+	xPubStr = "76a3d17d5c55b03e865fa3e8267990a7" +
+		"24baa24b0bdd0cc4af93be8de30be120" +
+		"d5533c91bf63ef923b02edcb84b74438" +
+		"3f7de232cca6eb46d07cad83dcaa317f" +
+		"becbc68ca13e2c4019e6a36531067450" +
+		"04aecc0be1dff0a78733fb0e7d5cb7c4" +
+		"97cab77b1331bf347e5f3a7847aa0bc0" +
+		"f4bc64146b48407fed7b931d16972d25" +
+		"fb4da5e6dc074ce2a58daa8de7624247" +
+		"cdf2ebe4e4dfec6d5989aac778c87559" +
+		"d3213d6040d4111ce3a2acae19f9ee15" +
+		"32509e037f69b252fdc30243cbbce9d0"
+
+	yPrivStr = "736562206f72656b74207268756f6867" +
+		"6f2020666c6f2c646120646e77206568" +
+		"657254206568207968736c61206c7262" +
+		"6165206b68746f726775206867616961" +
+		"2e6e482020656e6b776f207368777265" +
+		"2065685479656820766120657274646f" +
+		"652072616874732766206569646c2c73" +
+		"6120646e772065686572542065682079" +
+		"74736c69206c72746165206468746d65" +
+		"202c6e612064687720796f6e6f20656e" +
+		"63206e61622068656c6f206468546d65" +
+		"61202073685479657420657264610a2e"
+
+	yPubStr = "d04e156e554c37ffd7aba749df662350" +
+		"1e4ff4466cb12be055617c1a36872237" +
+		"36d2c3fdce9ee0f9b27774350849112a" +
+		"a5aeb1f126811c9c2f3a9cb13d2f0c3a" +
+		"7e6fa2d3bf71baf50d839171534f227e" +
+		"fbb2ce4227a38c25abdc5ba7fc430111" +
+		"3a2cb2069c9b305faac4b72bf21fec71" +
+		"578a9c369bcac84e1a7dcf0754e342f5" +
+		"bc8fe4917441b88254435e2abaf297e9" +
+		"3e1e57968672d45bd7d4c8ba1bc3d314" +
+		"889b5bc3d3e4ea33d4f2dfdd34e5e5a7" +
+		"2ff24ee46316d4757dad09366a0b66b3"
+
+	ssStr = "78afaf5f457f1fdb832bebc397644a33" +
+		"038be9dba10ca2ce4a076f327f3a0ce3" +
+		"151d477b869ee7ac467755292ad8a77d" +
+		"b9bd87ffbbc39955bcfb03b1583888c8" +
+		"fd037834ff3f401d463c10f899aa6378" +
+		"445140b7f8386a7d509e7b9db19b677f" +
+		"062a7a1a4e1509604d7a0839ccd5da61" +
+		"73e10afd9eab6dda74539d60493ca37f" +
+		"a5c98cd9640b409cd8bb3be2bc5136fd" +
+		"42e764fc3f3c0ddb8db3d87abcf2e659" +
+		"8d2b101bef7a56f50ebc658f9df1287d" +
+		"a81359543e77e4a4cfa7598a4152e4c0"
+)
+
+var xPriv, xPub, yPriv, yPub, ss []byte
+
+// TestGenerateKeyOdd tests creating a UniformDH keypair with a odd private
+// key.
+func TestGenerateKeyOdd(t *testing.T) {
+	xX, err := generateKey(xPriv)
+	if err != nil {
+		t.Fatal("generateKey(xPriv) failed:", err)
+	}
+
+	xPubGen, err := xX.PublicKey.Bytes()
+	if err != nil {
+		t.Fatal("xX.PublicKey.Bytes() failed:", err)
+	}
+	if 0 != bytes.Compare(xPubGen, xPub) {
+		t.Fatal("Generated public key does not match known answer")
+	}
+}
+
+// TestGenerateKeyEven tests creating a UniformDH keypair with a even private
+// key.
+func TestGenerateKeyEven(t *testing.T) {
+	yY, err := generateKey(yPriv)
+	if err != nil {
+		t.Fatal("generateKey(yPriv) failed:", err)
+	}
+
+	yPubGen, err := yY.PublicKey.Bytes()
+	if err != nil {
+		t.Fatal("yY.PublicKey.Bytes() failed:", err)
+	}
+	if 0 != bytes.Compare(yPubGen, yPub) {
+		t.Fatal("Generated public key does not match known answer")
+	}
+}
+
+// TestHandshake tests conductiong a UniformDH handshake with know values.
+func TestHandshake(t *testing.T) {
+	xX, err := generateKey(xPriv)
+	if err != nil {
+		t.Fatal("generateKey(xPriv) failed:", err)
+	}
+	yY, err := generateKey(yPriv)
+	if err != nil {
+		t.Fatal("generateKey(yPriv) failed:", err)
+	}
+
+	xY, err := Handshake(xX, &yY.PublicKey)
+	if err != nil {
+		t.Fatal("Handshake(xX, yY.PublicKey) failed:", err)
+	}
+	yX, err := Handshake(yY, &xX.PublicKey)
+	if err != nil {
+		t.Fatal("Handshake(yY, xX.PublicKey) failed:", err)
+	}
+
+	if 0 != bytes.Compare(xY, yX) {
+		t.Fatal("Generated shared secrets do not match between peers")
+	}
+	if 0 != bytes.Compare(xY, ss) {
+		t.Fatal("Generated shared secret does not match known value")
+	}
+}
+
+// Benchmark UniformDH key generation + exchange.  THe actual time taken per
+// peer is half of the reported time as this does 2 key generation an
+// handshake operations.
+func BenchmarkHandshake(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		xX, err := GenerateKey(rand.Reader)
+		if err != nil {
+			b.Fatal("Failed to generate xX keypair", err)
+		}
+
+		yY, err := GenerateKey(rand.Reader)
+		if err != nil {
+			b.Fatal("Failed to generate yY keypair", err)
+		}
+
+		xY, err := Handshake(xX, &yY.PublicKey)
+		if err != nil {
+			b.Fatal("Handshake(xX, yY.PublicKey) failed:", err)
+		}
+		yX, err := Handshake(yY, &xX.PublicKey)
+		if err != nil {
+			b.Fatal("Handshake(yY, xX.PublicKey) failed:", err)
+		}
+
+		_ = xY
+		_ = yX
+	}
+}
+
+func init() {
+	// Load the test vectors into byte slices.
+	var err error
+	xPriv, err = hex.DecodeString(xPrivStr)
+	if err != nil {
+		panic("hex.DecodeString(xPrivStr) failed")
+	}
+	xPub, err = hex.DecodeString(xPubStr)
+	if err != nil {
+		panic("hex.DecodeString(xPubStr) failed")
+	}
+	yPriv, err = hex.DecodeString(yPrivStr)
+	if err != nil {
+		panic("hex.DecodeString(yPrivStr) failed")
+	}
+	yPub, err = hex.DecodeString(yPubStr)
+	if err != nil {
+		panic("hex.DecodeString(yPubStr) failed")
+	}
+	ss, err = hex.DecodeString(ssStr)
+	if err != nil {
+		panic("hex.DecodeString(ssStr) failed")
+	}
+}
diff --git a/obfs4.go b/obfs4.go
deleted file mode 100644
index 4cc159c..0000000
--- a/obfs4.go
+++ /dev/null
@@ -1,758 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  * Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- *
- *  * Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-// Package obfs4 implements the obfs4 protocol.  For the most part, obfs4
-// connections are exposed via the net.Conn and net.Listener interface, though
-// accepting connections as a server requires calling ServerHandshake on the
-// conn to finish connection establishment.
-package obfs4
-
-import (
-	"bytes"
-	"crypto/sha256"
-	"encoding/base64"
-	"fmt"
-	"io"
-	"math/rand"
-	"net"
-	"syscall"
-	"time"
-
-	"git.torproject.org/pluggable-transports/obfs4.git/drbg"
-	"git.torproject.org/pluggable-transports/obfs4.git/framing"
-	"git.torproject.org/pluggable-transports/obfs4.git/ntor"
-)
-
-const (
-	// SeedLength is the length of the obfs4 polymorphism seed.
-	SeedLength        = 32
-	headerLength      = framing.FrameOverhead + packetOverhead
-	connectionTimeout = time.Duration(30) * time.Second
-
-	maxCloseDelayBytes = maxHandshakeLength
-	maxCloseDelay      = 60
-
-	maxIatDelay = 100
-)
-
-type connState int
-
-const (
-	stateInit connState = iota
-	stateEstablished
-	stateBroken
-	stateClosed
-)
-
-// Obfs4Conn is the implementation of the net.Conn interface for obfs4
-// connections.
-type Obfs4Conn struct {
-	conn net.Conn
-
-	sessionKey *ntor.Keypair
-
-	lenProbDist *wDist
-	iatProbDist *wDist
-
-	encoder *framing.Encoder
-	decoder *framing.Decoder
-
-	receiveBuffer        bytes.Buffer
-	receiveDecodedBuffer bytes.Buffer
-
-	state    connState
-	isServer bool
-
-	// Server side state.
-	listener  *Obfs4Listener
-	startTime time.Time
-}
-
-func (c *Obfs4Conn) padBurst(burst *bytes.Buffer) (err error) {
-	tailLen := burst.Len() % framing.MaximumSegmentLength
-	toPadTo := c.lenProbDist.sample()
-
-	padLen := 0
-	if toPadTo >= tailLen {
-		padLen = toPadTo - tailLen
-	} else {
-		padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo
-	}
-
-	if padLen > headerLength {
-		err = c.producePacket(burst, packetTypePayload, []byte{},
-			uint16(padLen-headerLength))
-		if err != nil {
-			return
-		}
-	} else if padLen > 0 {
-		err = c.producePacket(burst, packetTypePayload, []byte{},
-			maxPacketPayloadLength)
-		if err != nil {
-			return
-		}
-		err = c.producePacket(burst, packetTypePayload, []byte{},
-			uint16(padLen))
-		if err != nil {
-			return
-		}
-	}
-
-	return
-}
-
-func (c *Obfs4Conn) closeAfterDelay() {
-	// I-it's not like I w-wanna handshake with you or anything.  B-b-baka!
-	defer c.conn.Close()
-
-	delay := time.Duration(c.listener.closeDelay)*time.Second + connectionTimeout
-	deadline := c.startTime.Add(delay)
-	if time.Now().After(deadline) {
-		return
-	}
-
-	err := c.conn.SetReadDeadline(deadline)
-	if err != nil {
-		return
-	}
-
-	// Consume and discard data on this connection until either the specified
-	// interval passes or a certain size has been reached.
-	discarded := 0
-	var buf [framing.MaximumSegmentLength]byte
-	for discarded < int(c.listener.closeDelayBytes) {
-		n, err := c.conn.Read(buf[:])
-		if err != nil {
-			return
-		}
-		discarded += n
-	}
-}
-
-func (c *Obfs4Conn) setBroken() {
-	c.state = stateBroken
-}
-
-func (c *Obfs4Conn) clientHandshake(nodeID *ntor.NodeID, publicKey *ntor.PublicKey) (err error) {
-	if c.isServer {
-		panic(fmt.Sprintf("BUG: clientHandshake() called for server connection"))
-	}
-
-	defer func() {
-		c.sessionKey = nil
-		if err != nil {
-			c.setBroken()
-		}
-	}()
-
-	// Generate/send the client handshake.
-	var hs *clientHandshake
-	var blob []byte
-	hs = newClientHandshake(nodeID, publicKey, c.sessionKey)
-	blob, err = hs.generateHandshake()
-	if err != nil {
-		return
-	}
-
-	err = c.conn.SetDeadline(time.Now().Add(connectionTimeout * 2))
-	if err != nil {
-		return
-	}
-
-	_, err = c.conn.Write(blob)
-	if err != nil {
-		return
-	}
-
-	// Consume the server handshake.
-	var hsBuf [maxHandshakeLength]byte
-	for {
-		var n int
-		n, err = c.conn.Read(hsBuf[:])
-		if err != nil {
-			// Yes, just bail out of handshaking even if the Read could have
-			// returned data, no point in continuing on EOF/etc.
-			return
-		}
-		c.receiveBuffer.Write(hsBuf[:n])
-
-		var seed []byte
-		n, seed, err = hs.parseServerHandshake(c.receiveBuffer.Bytes())
-		if err == ErrMarkNotFoundYet {
-			continue
-		} else if err != nil {
-			return
-		}
-		_ = c.receiveBuffer.Next(n)
-
-		err = c.conn.SetDeadline(time.Time{})
-		if err != nil {
-			return
-		}
-
-		// Use the derived key material to intialize the link crypto.
-		okm := ntor.Kdf(seed, framing.KeyLength*2)
-		c.encoder = framing.NewEncoder(okm[:framing.KeyLength])
-		c.decoder = framing.NewDecoder(okm[framing.KeyLength:])
-
-		c.state = stateEstablished
-
-		return nil
-	}
-}
-
-func (c *Obfs4Conn) serverHandshake(nodeID *ntor.NodeID, keypair *ntor.Keypair) (err error) {
-	if !c.isServer {
-		panic(fmt.Sprintf("BUG: serverHandshake() called for client connection"))
-	}
-
-	defer func() {
-		c.sessionKey = nil
-		if err != nil {
-			c.setBroken()
-		}
-	}()
-
-	hs := newServerHandshake(nodeID, keypair, c.sessionKey)
-	err = c.conn.SetDeadline(time.Now().Add(connectionTimeout))
-	if err != nil {
-		return
-	}
-
-	// Consume the client handshake.
-	var hsBuf [maxHandshakeLength]byte
-	for {
-		var n int
-		n, err = c.conn.Read(hsBuf[:])
-		if err != nil {
-			// Yes, just bail out of handshaking even if the Read could have
-			// returned data, no point in continuing on EOF/etc.
-			return
-		}
-		c.receiveBuffer.Write(hsBuf[:n])
-
-		var seed []byte
-		seed, err = hs.parseClientHandshake(c.listener.filter, c.receiveBuffer.Bytes())
-		if err == ErrMarkNotFoundYet {
-			continue
-		} else if err != nil {
-			return
-		}
-		c.receiveBuffer.Reset()
-
-		err = c.conn.SetDeadline(time.Time{})
-		if err != nil {
-			return
-		}
-
-		// Use the derived key material to intialize the link crypto.
-		okm := ntor.Kdf(seed, framing.KeyLength*2)
-		c.encoder = framing.NewEncoder(okm[framing.KeyLength:])
-		c.decoder = framing.NewDecoder(okm[:framing.KeyLength])
-
-		break
-	}
-
-	//
-	// Since the current and only implementation always sends a PRNG seed for
-	// the length obfuscation, this makes the amount of data received from the
-	// server inconsistent with the length sent from the client.
-	//
-	// Rebalance this by tweaking the client mimimum padding/server maximum
-	// padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed
-	// as part of the server response).  See inlineSeedFrameLength in
-	// handshake_ntor.go.
-	//
-
-	// Generate/send the response.
-	var blob []byte
-	blob, err = hs.generateHandshake()
-	if err != nil {
-		return
-	}
-	var frameBuf bytes.Buffer
-	_, err = frameBuf.Write(blob)
-	if err != nil {
-		return
-	}
-	c.state = stateEstablished
-
-	// Send the PRNG seed as the first packet.
-	err = c.producePacket(&frameBuf, packetTypePrngSeed, c.listener.rawSeed, 0)
-	if err != nil {
-		return
-	}
-	_, err = c.conn.Write(frameBuf.Bytes())
-	if err != nil {
-		return
-	}
-
-	return
-}
-
-// CanHandshake queries the connection state to see if it is appropriate to
-// call ServerHandshake to complete connection establishment.
-func (c *Obfs4Conn) CanHandshake() bool {
-	return c.state == stateInit
-}
-
-// CanReadWrite queries the connection state to see if it is possible to read
-// and write data.
-func (c *Obfs4Conn) CanReadWrite() bool {
-	return c.state == stateEstablished
-}
-
-// ServerHandshake completes the server side of the obfs4 handshake.  Servers
-// are required to call this after accepting a connection.  ServerHandshake
-// will treat errors encountered during the handshake as fatal and drop the
-// connection before returning.
-func (c *Obfs4Conn) ServerHandshake() error {
-	// Handshakes when already established are a no-op.
-	if c.CanReadWrite() {
-		return nil
-	} else if !c.CanHandshake() {
-		return syscall.EINVAL
-	}
-
-	if !c.isServer {
-		panic(fmt.Sprintf("BUG: ServerHandshake() called for client connection"))
-	}
-
-	// Complete the handshake.
-	err := c.serverHandshake(c.listener.nodeID, c.listener.keyPair)
-	if err != nil {
-		c.closeAfterDelay()
-	}
-	c.listener = nil
-
-	return err
-}
-
-// Read implements the net.Conn Read method.
-func (c *Obfs4Conn) Read(b []byte) (n int, err error) {
-	if !c.CanReadWrite() {
-		return 0, syscall.EINVAL
-	}
-
-	for c.receiveDecodedBuffer.Len() == 0 {
-		_, err = c.consumeFramedPackets(nil)
-		if err == framing.ErrAgain {
-			continue
-		} else if err != nil {
-			return
-		}
-	}
-
-	n, err = c.receiveDecodedBuffer.Read(b)
-	return
-}
-
-// WriteTo implements the io.WriterTo WriteTo method.
-func (c *Obfs4Conn) WriteTo(w io.Writer) (n int64, err error) {
-	if !c.CanReadWrite() {
-		return 0, syscall.EINVAL
-	}
-
-	// If there is buffered payload from earlier Read() calls, write.
-	wrLen := 0
-	if c.receiveDecodedBuffer.Len() > 0 {
-		wrLen, err = w.Write(c.receiveDecodedBuffer.Bytes())
-		if err != nil {
-			c.setBroken()
-			return int64(wrLen), err
-		} else if wrLen < int(c.receiveDecodedBuffer.Len()) {
-			c.setBroken()
-			return int64(wrLen), io.ErrShortWrite
-		}
-		c.receiveDecodedBuffer.Reset()
-	}
-
-	for {
-		wrLen, err = c.consumeFramedPackets(w)
-		n += int64(wrLen)
-		if err == framing.ErrAgain {
-			continue
-		} else if err != nil {
-			// io.EOF is treated as not an error.
-			if err == io.EOF {
-				err = nil
-			}
-			break
-		}
-	}
-
-	return
-}
-
-// Write implements the net.Conn Write method.  The obfs4 lengt obfuscation is
-// done based on the amount of data passed to Write (each call to Write results
-// in up to 2 frames of padding).  Passing excessively short buffers to Write
-// will result in significant overhead.
-func (c *Obfs4Conn) Write(b []byte) (n int, err error) {
-	if !c.CanReadWrite() {
-		return 0, syscall.EINVAL
-	}
-
-	defer func() {
-		if err != nil {
-			c.setBroken()
-		}
-	}()
-
-	// TODO: Change this to write directly to c.conn skipping frameBuf.
-	chopBuf := bytes.NewBuffer(b)
-	var payload [maxPacketPayloadLength]byte
-	var frameBuf bytes.Buffer
-
-	for chopBuf.Len() > 0 {
-		// Send maximum sized frames.
-		rdLen := 0
-		rdLen, err = chopBuf.Read(payload[:])
-		if err != nil {
-			return 0, err
-		} else if rdLen == 0 {
-			panic(fmt.Sprintf("BUG: Write(), chopping length was 0"))
-		}
-		n += rdLen
-
-		err = c.producePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0)
-		if err != nil {
-			return 0, err
-		}
-	}
-
-	// Insert random padding.  In theory for some padding lengths, this can be
-	// inlined with the payload, but doing it this way simplifies the code
-	// significantly.
-	err = c.padBurst(&frameBuf)
-	if err != nil {
-		return 0, err
-	}
-
-	// Spit frame(s) onto the network.
-	//
-	// Partial writes are fatal because the frame encoder state is advanced
-	// at this point.  It's possible to keep frameBuf around, but fuck it.
-	// Someone that wants write timeouts can change this.
-	if c.iatProbDist != nil {
-		var iatFrame [framing.MaximumSegmentLength]byte
-		for frameBuf.Len() > 0 {
-			iatWrLen := 0
-			iatWrLen, err = frameBuf.Read(iatFrame[:])
-			if err != nil {
-				return 0, err
-			} else if iatWrLen == 0 {
-				panic(fmt.Sprintf("BUG: Write(), iat length was 0"))
-			}
-
-			// Calculate the delay.  The delay resolution is 100 usec, leading
-			// to a maximum delay of 10 msec.
-			iatDelta := time.Duration(c.iatProbDist.sample() * 100)
-
-			// Write then sleep.
-			_, err = c.conn.Write(iatFrame[:iatWrLen])
-			if err != nil {
-				return 0, err
-			}
-			time.Sleep(iatDelta * time.Microsecond)
-		}
-	} else {
-		_, err = c.conn.Write(frameBuf.Bytes())
-		if err != nil {
-			return 0, err
-		}
-	}
-
-	return
-}
-
-// Close closes the connection.
-func (c *Obfs4Conn) Close() error {
-	if c.conn == nil {
-		return syscall.EINVAL
-	}
-
-	c.state = stateClosed
-
-	return c.conn.Close()
-}
-
-// LocalAddr returns the local network address.
-func (c *Obfs4Conn) LocalAddr() net.Addr {
-	if c.state == stateClosed {
-		return nil
-	}
-
-	return c.conn.LocalAddr()
-}
-
-// RemoteAddr returns the remote network address.
-func (c *Obfs4Conn) RemoteAddr() net.Addr {
-	if c.state == stateClosed {
-		return nil
-	}
-
-	return c.conn.RemoteAddr()
-}
-
-// SetDeadline is a convoluted way to get syscall.ENOTSUP.
-func (c *Obfs4Conn) SetDeadline(t time.Time) error {
-	return syscall.ENOTSUP
-}
-
-// SetReadDeadline implements the net.Conn SetReadDeadline method.  Connections
-// must be in the established state (CanReadWrite).
-func (c *Obfs4Conn) SetReadDeadline(t time.Time) error {
-	if !c.CanReadWrite() {
-		return syscall.EINVAL
-	}
-
-	return c.conn.SetReadDeadline(t)
-}
-
-// SetWriteDeadline is a convoluted way to get syscall.ENOTSUP.
-func (c *Obfs4Conn) SetWriteDeadline(t time.Time) error {
-	return syscall.ENOTSUP
-}
-
-// DialFn is a function pointer to a dial routine that matches the
-// net.Dialer.Dial routine.
-type DialFn func(string, string) (net.Conn, error)
-
-// DialObfs4 connects to the remote address on the network, and handshakes with
-// the peer's obfs4 Node ID and Identity Public Key.  nodeID and publicKey are
-// expected as strings containing the Base64 encoded values.
-func DialObfs4(network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) {
-
-	return DialObfs4DialFn(net.Dial, network, address, nodeID, publicKey, iatObfuscation)
-}
-
-// DialObfs4DialFn connects to the remote address on the network via DialFn,
-// and handshakes with the peers' obfs4 Node ID and Identity Public Key.
-func DialObfs4DialFn(dialFn DialFn, network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) {
-	// Decode the node_id/public_key.
-	pub, err := ntor.PublicKeyFromBase64(publicKey)
-	if err != nil {
-		return nil, err
-	}
-	id, err := ntor.NodeIDFromBase64(nodeID)
-	if err != nil {
-		return nil, err
-	}
-
-	// Generate the initial length obfuscation distribution.
-	seed, err := drbg.NewSeed()
-	if err != nil {
-		return nil, err
-	}
-
-	// Generate the Obfs4Conn.
-	c := new(Obfs4Conn)
-	c.lenProbDist = newWDist(seed, 0, framing.MaximumSegmentLength)
-	if iatObfuscation {
-		iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
-		iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
-		if err != nil {
-			return nil, err
-		}
-		c.iatProbDist = newWDist(iatSeed, 0, maxIatDelay)
-	}
-
-	// Generate the session keypair *before* connecting to the remote peer.
-	c.sessionKey, err = ntor.NewKeypair(true)
-	if err != nil {
-		return nil, err
-	}
-
-	// Connect to the remote peer.
-	c.conn, err = dialFn(network, address)
-	if err != nil {
-		return nil, err
-	}
-
-	// Handshake.
-	err = c.clientHandshake(id, pub)
-	if err != nil {
-		c.conn.Close()
-		return nil, err
-	}
-
-	return c, nil
-}
-
-// Obfs4Listener is the implementation of the net.Listener interface for obfs4
-// connections.
-type Obfs4Listener struct {
-	listener net.Listener
-
-	filter *replayFilter
-
-	keyPair *ntor.Keypair
-	nodeID  *ntor.NodeID
-
-	rawSeed        []byte
-	seed           *drbg.Seed
-	iatSeed        *drbg.Seed
-	iatObfuscation bool
-
-	closeDelayBytes int
-	closeDelay      int
-}
-
-// Accept implements the Accept method of the net.Listener interface; it waits
-// for the next call and returns a generic net.Conn.  Callers are responsible
-// for completing the handshake by calling Obfs4Conn.ServerHandshake().
-func (l *Obfs4Listener) Accept() (net.Conn, error) {
-	conn, err := l.AcceptObfs4()
-	if err != nil {
-		return nil, err
-	}
-
-	return conn, nil
-}
-
-// AcceptObfs4 accepts the next incoming call and returns a new connection.
-// Callers are responsible for completing the handshake by calling
-// Obfs4Conn.ServerHandshake().
-func (l *Obfs4Listener) AcceptObfs4() (*Obfs4Conn, error) {
-	// Accept a connection.
-	c, err := l.listener.Accept()
-	if err != nil {
-		return nil, err
-	}
-
-	// Allocate the obfs4 connection state.
-	cObfs := new(Obfs4Conn)
-
-	// Generate the session keypair *before* consuming data from the peer, to
-	// add more noise to the keypair generation time.  The idea is that jitter
-	// here is masked by network latency (the time it takes for a server to
-	// accept a socket out of the backlog should not be fixed, and the client
-	// needs to send the public key).
-	cObfs.sessionKey, err = ntor.NewKeypair(true)
-	if err != nil {
-		return nil, err
-	}
-
-	cObfs.conn = c
-	cObfs.isServer = true
-	cObfs.listener = l
-	cObfs.lenProbDist = newWDist(l.seed, 0, framing.MaximumSegmentLength)
-	if l.iatObfuscation {
-		cObfs.iatProbDist = newWDist(l.iatSeed, 0, maxIatDelay)
-	}
-	if err != nil {
-		c.Close()
-		return nil, err
-	}
-	cObfs.startTime = time.Now()
-
-	return cObfs, nil
-}
-
-// Close stops listening on the Obfs4 endpoint.  Already Accepted connections
-// are not closed.
-func (l *Obfs4Listener) Close() error {
-	return l.listener.Close()
-}
-
-// Addr returns the listener's network address.
-func (l *Obfs4Listener) Addr() net.Addr {
-	return l.listener.Addr()
-}
-
-// PublicKey returns the listener's Identity Public Key, a Base64 encoded
-// obfs4.ntor.PublicKey.
-func (l *Obfs4Listener) PublicKey() string {
-	if l.keyPair == nil {
-		return ""
-	}
-
-	return l.keyPair.Public().Base64()
-}
-
-// NodeID returns the listener's NodeID, a Base64 encoded obfs4.ntor.NodeID.
-func (l *Obfs4Listener) NodeID() string {
-	if l.nodeID == nil {
-		return ""
-	}
-
-	return l.nodeID.Base64()
-}
-
-// ListenObfs4 annnounces on the network and address, and returns and
-// Obfs4Listener. nodeId, privateKey and seed are expected as strings
-// containing the Base64 encoded values.
-func ListenObfs4(network, laddr, nodeID, privateKey, seed string, iatObfuscation bool) (*Obfs4Listener, error) {
-	var err error
-
-	// Decode node_id/private_key.
-	l := new(Obfs4Listener)
-	l.keyPair, err = ntor.KeypairFromBase64(privateKey)
-	if err != nil {
-		return nil, err
-	}
-	l.nodeID, err = ntor.NodeIDFromBase64(nodeID)
-	if err != nil {
-		return nil, err
-	}
-	l.rawSeed, err = base64.StdEncoding.DecodeString(seed)
-	if err != nil {
-		return nil, err
-	}
-	l.seed, err = drbg.SeedFromBytes(l.rawSeed)
-	if err != nil {
-		return nil, err
-	}
-	l.iatObfuscation = iatObfuscation
-	if l.iatObfuscation {
-		iatSeedSrc := sha256.Sum256(l.seed.Bytes()[:])
-		l.iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:])
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	l.filter, err = newReplayFilter()
-	if err != nil {
-		return nil, err
-	}
-
-	rng := rand.New(drbg.NewHashDrbg(l.seed))
-	l.closeDelayBytes = rng.Intn(maxCloseDelayBytes)
-	l.closeDelay = rng.Intn(maxCloseDelay)
-
-	// Start up the listener.
-	l.listener, err = net.Listen(network, laddr)
-	if err != nil {
-		return nil, err
-	}
-
-	return l, nil
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/obfs4proxy.go b/obfs4proxy/obfs4proxy.go
index c49f3d0..d3490bf 100644
--- a/obfs4proxy/obfs4proxy.go
+++ b/obfs4proxy/obfs4proxy.go
@@ -23,31 +23,13 @@
  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
- *
- * This file is based off goptlib's dummy-[client,server].go files.
  */
 
-// obfs4 pluggable transport.  Works only as a managed proxy.
-//
-// Client usage (in torrc):
-//   UseBridges 1
-//   Bridge obfs4 X.X.X.X:YYYY <Fingerprint> public-key=<Base64 Bridge Public Key> node-id=<Base64 Bridge Node ID>
-//   ClientTransportPlugin obfs4 exec obfs4proxy
-//
-// Server usage (in torrc):
-//   BridgeRelay 1
-//   ORPort 9001
-//   ExtORPort 6669
-//   ServerTransportPlugin obfs4 exec obfs4proxy
-//   ServerTransportOptions obfs4 private-key=<Base64 Bridge Private Key> node-id=<Base64 Node ID> drbg-seed=<Base64 DRBG Seed>
-//
-// Because the pluggable transport requires arguments, obfs4proxy requires
-// tor-0.2.5.x to be useful.
+// Go language Tor Pluggable Transport suite.  Works only as a managed
+// client/server.
 package main
 
 import (
-	"encoding/base64"
-	"encoding/hex"
 	"flag"
 	"fmt"
 	"io"
@@ -61,292 +43,307 @@ import (
 	"sync"
 	"syscall"
 
+	"code.google.com/p/go.net/proxy"
+
 	"git.torproject.org/pluggable-transports/goptlib.git"
-	"git.torproject.org/pluggable-transports/obfs4.git"
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
-	"git.torproject.org/pluggable-transports/obfs4.git/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
 )
 
 const (
-	obfs4Method  = "obfs4"
-	obfs4LogFile = "obfs4proxy.log"
+	obfs4proxyLogFile = "obfs4proxy.log"
+	socksAddr         = "127.0.0.1:0"
+	elidedAddr        = "[scrubbed]"
 )
 
 var enableLogging bool
 var unsafeLogging bool
-var iatObfuscation bool
-var ptListeners []net.Listener
+var stateDir string
+var handlerChan chan int
 
-// When a connection handler starts, +1 is written to this channel; when it
-// ends, -1 is written.
-var handlerChan = make(chan int)
+// DialFn is a function pointer to a function that matches the net.Dialer.Dial
+// interface.
+type DialFn func(string, string) (net.Conn, error)
 
-func logAndRecover(conn *obfs4.Obfs4Conn) {
-	if err := recover(); err != nil {
-		log.Printf("[ERROR] %p: Panic: %s", conn, err)
+func elideAddr(addrStr string) string {
+	if unsafeLogging {
+		return addrStr
 	}
+
+	if addr, err := resolveAddrStr(addrStr); err == nil {
+		// Only scrub off the address so that it's slightly easier to follow
+		// the logs by looking at the port.
+		return fmt.Sprintf("%s:%d", elidedAddr, addr.Port)
+	}
+
+	return elidedAddr
 }
 
-func copyLoop(a net.Conn, b *obfs4.Obfs4Conn) {
-	var wg sync.WaitGroup
-	wg.Add(2)
+func clientSetup() (launched bool, listeners []net.Listener) {
+	ptClientInfo, err := pt.ClientSetup(transports.Transports())
+	if err != nil {
+		log.Fatal(err)
+	}
 
-	go func() {
-		defer logAndRecover(b)
-		defer wg.Done()
-		defer b.Close()
-		defer a.Close()
+	ptClientProxy, err := ptGetProxy()
+	if err != nil {
+		log.Fatal(err)
+	} else if ptClientProxy != nil {
+		ptProxyDone()
+	}
 
-		_, err := io.Copy(b, a)
-		if err != nil {
-			log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err)
+	// Launch each of the client listeners.
+	for _, name := range ptClientInfo.MethodNames {
+		t := transports.Get(name)
+		if t == nil {
+			pt.CmethodError(name, "no such transport is supported")
+			continue
 		}
-	}()
-	go func() {
-		defer logAndRecover(b)
-		defer wg.Done()
-		defer a.Close()
-		defer b.Close()
 
-		_, err := io.Copy(a, b)
+		f, err := t.ClientFactory(stateDir)
 		if err != nil {
-			log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err)
+			pt.CmethodError(name, "failed to get ClientFactory")
+			continue
 		}
-	}()
-
-	wg.Wait()
-}
-
-func serverHandler(conn *obfs4.Obfs4Conn, info *pt.ServerInfo) error {
-	defer conn.Close()
-	defer logAndRecover(conn)
-
-	handlerChan <- 1
-	defer func() {
-		handlerChan <- -1
-	}()
 
-	var addr string
-	if unsafeLogging {
-		addr = conn.RemoteAddr().String()
-	} else {
-		addr = "[scrubbed]"
-	}
+		ln, err := pt.ListenSocks("tcp", socksAddr)
+		if err != nil {
+			pt.CmethodError(name, err.Error())
+			continue
+		}
 
-	log.Printf("[INFO] server: %p: New connection from %s", conn, addr)
+		go clientAcceptLoop(f, ln, ptClientProxy)
+		pt.Cmethod(name, ln.Version(), ln.Addr())
 
-	// Handshake with the client.
-	err := conn.ServerHandshake()
-	if err != nil {
-		log.Printf("[WARN] server: %p: Handshake failed: %s", conn, err)
-		return err
-	}
+		log.Printf("[INFO]: %s - registered listener: %s", name, ln.Addr())
 
-	or, err := pt.DialOr(info, conn.RemoteAddr().String(), obfs4Method)
-	if err != nil {
-		log.Printf("[ERROR] server: %p: DialOr failed: %s", conn, err)
-		return err
+		listeners = append(listeners, ln)
+		launched = true
 	}
-	defer or.Close()
-
-	copyLoop(or, conn)
+	pt.CmethodsDone()
 
-	return nil
+	return
 }
 
-func serverAcceptLoop(ln *obfs4.Obfs4Listener, info *pt.ServerInfo) error {
+func clientAcceptLoop(f base.ClientFactory, ln *pt.SocksListener, proxyURI *url.URL) error {
 	defer ln.Close()
 	for {
-		conn, err := ln.AcceptObfs4()
+		conn, err := ln.AcceptSocks()
 		if err != nil {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
 				return err
 			}
 			continue
 		}
-		go serverHandler(conn, info)
+		go clientHandler(f, conn, proxyURI)
 	}
 }
 
-func serverSetup() (launched bool) {
-	// Initialize pt logging.
-	err := ptInitializeLogging(enableLogging)
+func clientHandler(f base.ClientFactory, conn *pt.SocksConn, proxyURI *url.URL) {
+	defer conn.Close()
+	handlerChan <- 1
+	defer func() {
+		handlerChan <- -1
+	}()
+
+	name := f.Transport().Name()
+	addrStr := elideAddr(conn.Req.Target)
+	log.Printf("[INFO]: %s(%s) - new connection", name, addrStr)
+
+	// Deal with arguments.
+	args, err := f.ParseArgs(&conn.Req.Args)
 	if err != nil {
+		log.Printf("[ERROR]: %s(%s) - invalid arguments: %s", name, addrStr, err)
+		conn.Reject()
 		return
 	}
 
-	ptServerInfo, err := pt.ServerSetup([]string{obfs4Method})
+	// Obtain the proxy dialer if any, and create the outgoing TCP connection.
+	var dialFn DialFn
+	if proxyURI == nil {
+		dialFn = proxy.Direct.Dial
+	} else {
+		// This is unlikely to happen as the proxy protocol is verified during
+		// the configuration phase.
+		dialer, err := proxy.FromURL(proxyURI, proxy.Direct)
+		if err != nil {
+			log.Printf("[ERROR]: %s(%s) - failed to obtain proxy dialer: %s", name, addrStr, err)
+			conn.Reject()
+			return
+		}
+		dialFn = dialer.Dial
+	}
+	remoteConn, err := dialFn("tcp", conn.Req.Target) // XXX: Allow UDP?
 	if err != nil {
+		// Note: The error message returned from the dialer can include the IP
+		// address/port of the remote peer.
+		if unsafeLogging {
+			log.Printf("[ERROR]: %s(%s) - outgoing connection failed: %s", name, addrStr, err)
+		} else {
+			log.Printf("[ERROR]: %s(%s) - outgoing connection failed", name, addrStr)
+		}
+		conn.Reject()
 		return
 	}
+	defer remoteConn.Close()
 
-	for _, bindaddr := range ptServerInfo.Bindaddrs {
-		switch bindaddr.MethodName {
-		case obfs4Method:
-			// Handle the mandetory arguments.
-			privateKey, ok := bindaddr.Options.Get("private-key")
-			if !ok {
-				pt.SmethodError(bindaddr.MethodName, "needs a private-key option")
-				break
-			}
-			nodeID, ok := bindaddr.Options.Get("node-id")
-			if !ok {
-				pt.SmethodError(bindaddr.MethodName, "needs a node-id option")
-				break
-			}
-			seed, ok := bindaddr.Options.Get("drbg-seed")
-			if !ok {
-				pt.SmethodError(bindaddr.MethodName, "needs a drbg-seed option")
-				break
-			}
-
-			// Initialize the listener.
-			ln, err := obfs4.ListenObfs4("tcp", bindaddr.Addr.String(), nodeID,
-				privateKey, seed, iatObfuscation)
-			if err != nil {
-				pt.SmethodError(bindaddr.MethodName, err.Error())
-				break
-			}
+	// Instantiate the client transport method, handshake, and start pushing
+	// bytes back and forth.
+	remote, err := f.WrapConn(remoteConn, args)
+	if err != nil {
+		log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err)
+		conn.Reject()
+		return
+	}
+	err = conn.Grant(remoteConn.RemoteAddr().(*net.TCPAddr))
+	if err != nil {
+		log.Printf("[ERROR]: %s(%s) - SOCKS grant failed: %s", name, addrStr, err)
+		return
+	}
 
-			// Report the SMETHOD including the parameters.
-			args := pt.Args{}
-			args.Add("node-id", nodeID)
-			args.Add("public-key", ln.PublicKey())
-			go serverAcceptLoop(ln, &ptServerInfo)
-			pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args)
-			ptListeners = append(ptListeners, ln)
-			launched = true
-		default:
-			pt.SmethodError(bindaddr.MethodName, "no such method")
-		}
+	err = copyLoop(conn, remote)
+	if err != nil {
+		log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err)
+	} else {
+		log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr)
 	}
-	pt.SmethodsDone()
 
 	return
 }
 
-func clientHandler(conn *pt.SocksConn, proxyURI *url.URL) error {
-	defer conn.Close()
-
-	var addr string
-	if unsafeLogging {
-		addr = conn.Req.Target
-	} else {
-		addr = "[scrubbed]"
+func serverSetup() (launched bool, listeners []net.Listener) {
+	ptServerInfo, err := pt.ServerSetup(transports.Transports())
+	if err != nil {
+		log.Fatal(err)
 	}
 
-	log.Printf("[INFO] client: New connection to %s", addr)
+	for _, bindaddr := range ptServerInfo.Bindaddrs {
+		name := bindaddr.MethodName
+		t := transports.Get(name)
+		if t == nil {
+			pt.SmethodError(name, "no such transport is supported")
+			continue
+		}
 
-	// Extract the peer's node ID and public key.
-	nodeID, ok := conn.Req.Args.Get("node-id")
-	if !ok {
-		log.Printf("[ERROR] client: missing node-id argument")
-		conn.Reject()
-		return nil
-	}
-	publicKey, ok := conn.Req.Args.Get("public-key")
-	if !ok {
-		log.Printf("[ERROR] client: missing public-key argument")
-		conn.Reject()
-		return nil
-	}
+		f, err := t.ServerFactory(stateDir, &bindaddr.Options)
+		if err != nil {
+			pt.SmethodError(name, err.Error())
+			continue
+		}
 
-	handlerChan <- 1
-	defer func() {
-		handlerChan <- -1
-	}()
+		ln, err := net.ListenTCP("tcp", bindaddr.Addr)
+		if err != nil {
+			pt.SmethodError(name, err.Error())
+		}
 
-	defer logAndRecover(nil)
-	dialFn, err := getProxyDialer(proxyURI)
-	if err != nil {
-		log.Printf("[ERROR] client: failed to get proxy dialer: %s", err)
-		conn.Reject()
-		return err
-	}
-	remote, err := obfs4.DialObfs4DialFn(dialFn, "tcp", conn.Req.Target, nodeID, publicKey, iatObfuscation)
-	if err != nil {
-		log.Printf("[ERROR] client: %p: Handshake failed: %s", remote, err)
-		conn.Reject()
-		return err
-	}
-	defer remote.Close()
-	err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr))
-	if err != nil {
-		return err
-	}
+		go serverAcceptLoop(f, ln, &ptServerInfo)
+		if args := f.Args(); args != nil {
+			pt.SmethodArgs(name, ln.Addr(), *args)
+		} else {
+			pt.SmethodArgs(name, ln.Addr(), nil)
+		}
 
-	copyLoop(conn, remote)
+		log.Printf("[INFO]: %s - registered listener: %s", name, elideAddr(ln.Addr().String()))
 
-	return nil
+		listeners = append(listeners, ln)
+		launched = true
+	}
+	pt.SmethodsDone()
+
+	return
 }
 
-func clientAcceptLoop(ln *pt.SocksListener, proxyURI *url.URL) error {
+func serverAcceptLoop(f base.ServerFactory, ln net.Listener, info *pt.ServerInfo) error {
 	defer ln.Close()
 	for {
-		conn, err := ln.AcceptSocks()
+		conn, err := ln.Accept()
 		if err != nil {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
 				return err
 			}
 			continue
 		}
-		go clientHandler(conn, proxyURI)
+		go serverHandler(f, conn, info)
 	}
 }
 
-func clientSetup() (launched bool) {
-	// Initialize pt logging.
-	err := ptInitializeLogging(enableLogging)
+func serverHandler(f base.ServerFactory, conn net.Conn, info *pt.ServerInfo) {
+	defer conn.Close()
+	handlerChan <- 1
+	defer func() {
+		handlerChan <- -1
+	}()
+
+	name := f.Transport().Name()
+	addrStr := elideAddr(conn.RemoteAddr().String())
+	log.Printf("[INFO]: %s(%s) - new connection", name, addrStr)
+
+	// Instantiate the server transport method and handshake.
+	remote, err := f.WrapConn(conn)
 	if err != nil {
+		log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err)
 		return
 	}
 
-	ptClientInfo, err := pt.ClientSetup([]string{obfs4Method})
+	// Connect to the orport.
+	orConn, err := pt.DialOr(info, conn.RemoteAddr().String(), name)
 	if err != nil {
-		log.Fatal(err)
+		log.Printf("[ERROR]: %s(%s) - failed to connect to ORPort: %s", name, addrStr, err)
+		return
 	}
+	defer orConn.Close()
 
-	ptClientProxy, err := ptGetProxy()
+	err = copyLoop(orConn, remote)
 	if err != nil {
-		log.Fatal(err)
-	} else if ptClientProxy != nil {
-		ptProxyDone()
+		log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err)
+	} else {
+		log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr)
 	}
 
-	for _, methodName := range ptClientInfo.MethodNames {
-		switch methodName {
-		case obfs4Method:
-			ln, err := pt.ListenSocks("tcp", "127.0.0.1:0")
-			if err != nil {
-				pt.CmethodError(methodName, err.Error())
-				break
-			}
-			go clientAcceptLoop(ln, ptClientProxy)
-			pt.Cmethod(methodName, ln.Version(), ln.Addr())
-			ptListeners = append(ptListeners, ln)
-			launched = true
-		default:
-			pt.CmethodError(methodName, "no such method")
-		}
+	return
+}
+
+func copyLoop(a net.Conn, b net.Conn) error {
+	// Note: b is always the pt connection.  a is the SOCKS/ORPort connection.
+	errChan := make(chan error, 2)
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		defer wg.Done()
+		defer b.Close()
+		defer a.Close()
+		_, err := io.Copy(b, a)
+		errChan <- err
+	}()
+	go func() {
+		defer wg.Done()
+		defer a.Close()
+		defer b.Close()
+		_, err := io.Copy(a, b)
+		errChan <- err
+	}()
+
+	// Wait for both upstream and downstream to close.  Since one side
+	// terminating closes the other, the second error in the channel will be
+	// something like EINVAL (though io.Copy() will swallow EOF), so only the
+	// first error is returned.
+	wg.Wait()
+	if len(errChan) > 0 {
+		return <-errChan
 	}
-	pt.CmethodsDone()
 
-	return
+	return nil
 }
 
 func ptInitializeLogging(enable bool) error {
 	if enable {
-		// pt.MakeStateDir will ENV-ERROR for us.
-		dir, err := pt.MakeStateDir()
-		if err != nil {
-			return err
-		}
-
 		// While we could just exit, log an ENV-ERROR so it will propagate to
 		// the tor log.
-		f, err := os.OpenFile(path.Join(dir, obfs4LogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+		f, err := os.OpenFile(path.Join(stateDir, obfs4proxyLogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
 		if err != nil {
-			return ptEnvError(fmt.Sprintf("Failed to open log file: %s\n", err))
+			return ptEnvError(fmt.Sprintf("failed to open log file: %s\n", err))
 		}
 		log.SetOutput(f)
 	} else {
@@ -356,122 +353,74 @@ func ptInitializeLogging(enable bool) error {
 	return nil
 }
 
-func generateServerParams(id string) {
-	idIsFP := id != ""
-	var rawID []byte
-
-	if idIsFP {
-		var err error
-		rawID, err = hex.DecodeString(id)
-		if err != nil {
-			fmt.Println("Failed to hex decode id:", err)
-			return
-		}
-	} else {
-		rawID = make([]byte, ntor.NodeIDLength)
-		err := csrand.Bytes(rawID)
-		if err != nil {
-			fmt.Println("Failed to generate random node-id:", err)
-			return
-		}
-	}
-	parsedID, err := ntor.NewNodeID(rawID)
-	if err != nil {
-		fmt.Println("Failed to parse id:", err)
-		return
-	}
-
-	fmt.Println("Generated node-id:", parsedID.Base64())
-
-	keypair, err := ntor.NewKeypair(false)
-	if err != nil {
-		fmt.Println("Failed to generate keypair:", err)
-		return
-	}
-
-	seed := make([]byte, obfs4.SeedLength)
-	err = csrand.Bytes(seed)
-	if err != nil {
-		fmt.Println("Failed to generate DRBG seed:", err)
-		return
-	}
-	seedBase64 := base64.StdEncoding.EncodeToString(seed)
-
-	fmt.Println("Generated private-key:", keypair.Private().Base64())
-	fmt.Println("Generated public-key:", keypair.Public().Base64())
-	fmt.Println("Generated drbg-seed:", seedBase64)
-	fmt.Println()
-	fmt.Println("Client config: ")
-	if idIsFP {
-		fmt.Printf("  Bridge obfs4 <IP Address:Port> %s node-id=%s public-key=%s\n",
-			id, parsedID.Base64(), keypair.Public().Base64())
-	} else {
-		fmt.Printf("  Bridge obfs4 <IP Address:Port> <Fingerprint> node-id=%s public-key=%s\n",
-			parsedID.Base64(), keypair.Public().Base64())
-	}
-	fmt.Println()
-	fmt.Println("Server config:")
-	fmt.Printf("  ServerTransportOptions obfs4 node-id=%s private-key=%s drbg-seed=%s\n",
-		parsedID.Base64(), keypair.Private().Base64(), seedBase64)
-}
-
 func main() {
-	// Some command line args.
-	genParams := flag.Bool("genServerParams", false, "Generate Bridge operator torrc parameters")
-	genParamsFP := flag.String("genServerParamsFP", "", "Optional bridge fingerprint for genServerParams")
-	flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/obfs4proxy.log")
-	flag.BoolVar(&iatObfuscation, "iatObfuscation", false, "Enable IAT obufscation (EXPENSIVE)")
+	// Handle the command line arguments.
+	_, execName := path.Split(os.Args[0])
+	flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/"+obfs4proxyLogFile)
 	flag.BoolVar(&unsafeLogging, "unsafeLogging", false, "Disable the address scrubber")
 	flag.Parse()
-	if *genParams {
-		generateServerParams(*genParamsFP)
-		return
-	}
 
-	// Go through the pt protocol and initialize client or server mode.
+	// Determine if this is a client or server, initialize logging, and finish
+	// the pt configuration.
+	var ptListeners []net.Listener
+	handlerChan = make(chan int)
 	launched := false
 	isClient, err := ptIsClient()
 	if err != nil {
-		log.Fatal("[ERROR] obfs4proxy must be run as a managed transport or server")
-	} else if isClient {
-		launched = clientSetup()
+		log.Fatalf("[ERROR]: %s - must be run as a managed transport", execName)
+	}
+	if stateDir, err = pt.MakeStateDir(); err != nil {
+		log.Fatalf("[ERROR]: %s - No state directory: %s", execName, err)
+	}
+	if err = ptInitializeLogging(enableLogging); err != nil {
+		log.Fatalf("[ERROR]: %s - failed to initialize logging", execName)
+	}
+	if isClient {
+		log.Printf("[INFO]: %s - initializing client transport listeners", execName)
+		launched, ptListeners = clientSetup()
 	} else {
-		launched = serverSetup()
+		log.Printf("[INFO]: %s - initializing server transport listeners", execName)
+		launched, ptListeners = serverSetup()
 	}
 	if !launched {
-		// Something must have failed in client/server setup, just bail.
+		// Initialization failed, the client or server setup routines should
+		// have logged, so just exit here.
 		os.Exit(-1)
 	}
 
-	log.Println("[INFO] obfs4proxy - Launched and listening")
+	log.Printf("[INFO]: %s - launched and accepting connections", execName)
 	defer func() {
-		log.Println("[INFO] obfs4proxy - Terminated")
+		log.Printf("[INFO]: %s - terminated", execName)
 	}()
 
-	// Handle termination notification.
-	numHandlers := 0
-	var sig os.Signal
+	// At this point, the pt config protocol is finished, and incoming
+	// connections will be processed.  Per the pt spec, on sane platforms
+	// termination is signaled via SIGINT (or SIGTERM), so wait on tor to
+	// request a shutdown of some sort.
+
 	sigChan := make(chan os.Signal, 1)
 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
 
-	// wait for first signal
-	sig = nil
+	// Wait for the first SIGINT (close listeners).
+	var sig os.Signal
+	numHandlers := 0
 	for sig == nil {
 		select {
 		case n := <-handlerChan:
 			numHandlers += n
 		case sig = <-sigChan:
+			if sig == syscall.SIGTERM {
+				// SIGTERM causes immediate termination.
+				return
+			}
 		}
 	}
 	for _, ln := range ptListeners {
 		ln.Close()
 	}
 
-	if sig == syscall.SIGTERM {
-		return
-	}
-
-	// wait for second signal or no more handlers
+	// Wait for the 2nd SIGINT (or a SIGTERM), or for all current sessions to
+	// finish.
 	sig = nil
 	for sig == nil && numHandlers != 0 {
 		select {
@@ -481,5 +430,3 @@ func main() {
 		}
 	}
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/proxy_extras.go b/obfs4proxy/proxy_extras.go
deleted file mode 100644
index 5ead3b8..0000000
--- a/obfs4proxy/proxy_extras.go
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  * Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- *
- *  * Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package main
-
-import (
-	"net/url"
-
-	"code.google.com/p/go.net/proxy"
-
-	"git.torproject.org/pluggable-transports/obfs4.git"
-)
-
-// getProxyDialer is a trival wrapper around the go.net/proxy package to avoid
-// having it as a dependency for anything else.
-func getProxyDialer(uri *url.URL) (obfs4.DialFn, error) {
-	if uri == nil {
-		return proxy.Direct.Dial, nil
-	}
-
-	dialer, err := proxy.FromURL(uri, proxy.Direct)
-	if err != nil {
-		return nil, err
-	}
-
-	return dialer.Dial, nil
-}
diff --git a/obfs4proxy/proxy_http.go b/obfs4proxy/proxy_http.go
index c7b926a..2db6ca0 100644
--- a/obfs4proxy/proxy_http.go
+++ b/obfs4proxy/proxy_http.go
@@ -108,7 +108,6 @@ func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
 	return conn, nil
 }
 
-// httpConn is the mountain of bullshit we need to do just for staleReader.
 type httpConn struct {
 	remoteAddr   *net.TCPAddr
 	httpConn     *httputil.ClientConn
@@ -157,5 +156,3 @@ func (c *httpConn) SetWriteDeadline(t time.Time) error {
 func init() {
 	proxy.RegisterDialerType("http", newHTTP)
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/proxy_socks4.go b/obfs4proxy/proxy_socks4.go
index 95cc7b6..9d6bd4d 100644
--- a/obfs4proxy/proxy_socks4.go
+++ b/obfs4proxy/proxy_socks4.go
@@ -162,5 +162,3 @@ func init() {
 	// Despite the scheme name, this really is SOCKS4.
 	proxy.RegisterDialerType("socks4a", newSOCKS4)
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/obfs4proxy/pt_extras.go b/obfs4proxy/pt_extras.go
index 2d09cc3..9eddd26 100644
--- a/obfs4proxy/pt_extras.go
+++ b/obfs4proxy/pt_extras.go
@@ -124,7 +124,7 @@ func ptGetProxy() (*url.URL, error) {
 		return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid scheme: %s", spec.Scheme))
 	}
 
-	err = validateAddrStr(spec.Host)
+	_, err = resolveAddrStr(spec.Host)
 	if err != nil {
 		return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid host: %s", err))
 	}
@@ -135,27 +135,26 @@ func ptGetProxy() (*url.URL, error) {
 // Sigh, pt.resolveAddr() isn't exported.  Include our own getto version that
 // doesn't work around #7011, because we don't work with pre-0.2.5.x tor, and
 // all we care about is validation anyway.
-func validateAddrStr(addrStr string) error {
+func resolveAddrStr(addrStr string) (*net.TCPAddr, error) {
 	ipStr, portStr, err := net.SplitHostPort(addrStr)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	if ipStr == "" {
-		return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr))
+		return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr))
 	}
 	if portStr == "" {
-		return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr))
+		return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr))
 	}
-	if net.ParseIP(ipStr) == nil {
-		return net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr))
+	ip := net.ParseIP(ipStr)
+	if ip == nil {
+		return nil, net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr))
 	}
-	_, err = strconv.ParseUint(portStr, 10, 16)
+	port, err := strconv.ParseUint(portStr, 10, 16)
 	if err != nil {
-		return net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr))
+		return nil, net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr))
 	}
 
-	return nil
+	return &net.TCPAddr{IP: ip, Port: int(port), Zone: ""}, nil
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/replay_filter.go b/replay_filter.go
deleted file mode 100644
index b8f284a..0000000
--- a/replay_filter.go
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  * Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- *
- *  * Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package obfs4
-
-import (
-	"container/list"
-	"encoding/binary"
-	"sync"
-
-	"github.com/dchest/siphash"
-
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
-)
-
-// maxFilterSize is the maximum capacity of the replay filter.  The busiest
-// bridge I know about processes something along the order of 3000 connections
-// per day.  The maximum timespan any entry can live in the filter is 2 hours,
-// so this value should be sufficient.
-const maxFilterSize = 100 * 1024
-
-// replayFilter is a simple filter designed only to answer if it has seen a
-// given byte sequence before.  It is based around comparing the SipHash-2-4
-// digest of data to match against.  Collisions are treated as positive matches
-// however, the probability of such occurences is negligible.
-type replayFilter struct {
-	lock   sync.Mutex
-	key    [2]uint64
-	filter map[uint64]*filterEntry
-	fifo   *list.List
-}
-
-type filterEntry struct {
-	firstSeen int64
-	hash      uint64
-	element   *list.Element
-}
-
-// newReplayFilter creates a new replayFilter instance.
-func newReplayFilter() (filter *replayFilter, err error) {
-	// Initialize the SipHash-2-4 instance with a random key.
-	var key [16]byte
-	err = csrand.Bytes(key[:])
-	if err != nil {
-		return
-	}
-
-	filter = new(replayFilter)
-	filter.key[0] = binary.BigEndian.Uint64(key[0:8])
-	filter.key[1] = binary.BigEndian.Uint64(key[8:16])
-	filter.filter = make(map[uint64]*filterEntry)
-	filter.fifo = list.New()
-
-	return
-}
-
-// testAndSet queries the filter for buf, adds it if it was not present and
-// returns if it has added the entry or not.  This method is threadsafe.
-func (f *replayFilter) testAndSet(now int64, buf []byte) bool {
-	hash := siphash.Hash(f.key[0], f.key[1], buf)
-
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	f.compactFilter(now)
-
-	entry := f.filter[hash]
-	if entry != nil {
-		return true
-	}
-
-	entry = new(filterEntry)
-	entry.hash = hash
-	entry.firstSeen = now
-	entry.element = f.fifo.PushBack(entry)
-	f.filter[hash] = entry
-
-	return false
-}
-
-// compactFilter purges entries that are too old to be relevant.  If the filter
-// is filled to maxFilterCapacity, it will force purge a single entry.  This
-// method is NOT threadsafe.
-func (f *replayFilter) compactFilter(now int64) {
-	e := f.fifo.Front()
-	for e != nil {
-		entry, _ := e.Value.(*filterEntry)
-
-		// If the filter is at max capacity, force purge at least one entry.
-		if f.fifo.Len() < maxFilterSize {
-			deltaT := now - entry.firstSeen
-			if deltaT < 0 {
-				// Aeeeeeee, the system time jumped backwards, potentially by
-				// a lot.  This will eventually self-correct, but "eventually"
-				// could be a long time.  As much as this sucks, jettison the
-				// entire filter.
-				f.reset()
-				return
-			}
-			if deltaT < 3600*2 {
-				// Why yes, this is 2 hours.  The MAC code includes a hour
-				// resolution timestamp, but to deal with clock skew, it
-				// accepts time +- 1 hour.
-				break
-			}
-		}
-		eNext := e.Next()
-		delete(f.filter, entry.hash)
-		f.fifo.Remove(entry.element)
-		entry.element = nil
-		e = eNext
-	}
-}
-
-// reset purges the entire filter.  This methoid is NOT threadsafe.
-func (f *replayFilter) reset() {
-	f.filter = make(map[uint64]*filterEntry)
-	f.fifo = list.New()
-}
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/transports/base/base.go b/transports/base/base.go
new file mode 100644
index 0000000..e81ea03
--- /dev/null
+++ b/transports/base/base.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package base provides the common interface that each supported transport
+// protocol must implement.
+package base
+
+import (
+	"net"
+
+	"git.torproject.org/pluggable-transports/goptlib.git"
+)
+
+// ClientFactory is the interface that defines the factory for creating
+// pluggable transport protocol client instances.
+type ClientFactory interface {
+	// Transport returns the Transport instance that this ClientFactory belongs
+	// to.
+	Transport() Transport
+
+	// ParseArgs parses the supplied arguments into an internal representation
+	// for use with WrapConn.  This routine is called before the outgoing
+	// TCP/IP connection is created to allow doing things (like keypair
+	// generation) to be hidden from third parties.
+	ParseArgs(args *pt.Args) (interface{}, error)
+
+	// WrapConn wraps the provided net.Conn with a transport protocol
+	// implementation, and does whatever is required (eg: handshaking) to get
+	// the connection to a point where it is ready to relay data.
+	WrapConn(conn net.Conn, args interface{}) (net.Conn, error)
+}
+
+// ServerFactory is the interface that defines the factory for creating
+// plugable transport protocol server instances.  As the arguments are the
+// property of the factory, validation is done at factory creation time.
+type ServerFactory interface {
+	// Transport returns the Transport instance that this ServerFactory belongs
+	// to.
+	Transport() Transport
+
+	// Args returns the Args required on the client side to handshake with
+	// server connections created by this factory.
+	Args() *pt.Args
+
+	// WrapConn wraps the provided net.Conn with a transport protocol
+	// implementation, and does whatever is required (eg: handshaking) to get
+	// the connection to a point where it is ready to relay data.
+	WrapConn(conn net.Conn) (net.Conn, error)
+}
+
+// Transport is an interface that defines a pluggable transport protocol.
+type Transport interface {
+	// Name returns the name of the transport protocol.  It MUST be a valid C
+	// identifier.
+	Name() string
+
+	// ClientFactory returns a ClientFactory instance for this transport
+	// protocol.
+	ClientFactory(stateDir string) (ClientFactory, error)
+
+	// ServerFactory returns a ServerFactory instance for this transport
+	// protocol.  This can fail if the provided arguments are invalid.
+	ServerFactory(stateDir string, args *pt.Args) (ServerFactory, error)
+}
diff --git a/transports/obfs2/obfs2.go b/transports/obfs2/obfs2.go
new file mode 100644
index 0000000..3490646
--- /dev/null
+++ b/transports/obfs2/obfs2.go
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs2 provides an implementation of the Tor Project's obfs2
+// obfuscation protocol.  This protocol is considered trivially broken by most
+// sophisticated adversaries.
+package obfs2
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha256"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"net"
+	"time"
+
+	"git.torproject.org/pluggable-transports/goptlib.git"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+)
+
+const (
+	transportName = "obfs2"
+	sharedSecretArg = "shared-secret"
+
+	clientHandshakeTimeout = time.Duration(30) * time.Second
+	serverHandshakeTimeout = time.Duration(30) * time.Second
+
+	magicValue         = 0x2bf5ca7e
+	initiatorPadString = "Initiator obfuscation padding"
+	responderPadString = "Responder obfuscation padding"
+	initiatorKdfString = "Initiator obfuscated data"
+	responderKdfString = "Responder obfuscated data"
+	maxPadding         = 8192
+	keyLen             = 16
+	seedLen            = 16
+	hsLen              = 4 + 4
+)
+
+func validateArgs(args *pt.Args) error {
+	if _, ok := args.Get(sharedSecretArg); ok {
+		// "shared-secret" is something no bridges use in practice and is thus
+		// unimplemented.
+		return fmt.Errorf("unsupported argument '%s'", sharedSecretArg)
+	}
+	return nil
+}
+
+// Transport is the obfs2 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs2 transport protocol.
+func (t *Transport) Name() string {
+	return transportName
+}
+
+// ClientFactory returns a new obfs2ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+	cf := &obfs2ClientFactory{transport: t}
+	return cf, nil
+}
+
+// ServerFactory returns a new obfs2ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+	if err := validateArgs(args); err != nil {
+		return nil, err
+	}
+
+	sf := &obfs2ServerFactory{t}
+	return sf, nil
+}
+
+type obfs2ClientFactory struct {
+	transport base.Transport
+}
+
+func (cf *obfs2ClientFactory) Transport() base.Transport {
+	return cf.transport
+}
+
+func (cf *obfs2ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+	return nil, validateArgs(args)
+}
+
+func (cf *obfs2ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+	return newObfs2ClientConn(conn)
+}
+
+type obfs2ServerFactory struct {
+	transport base.Transport
+}
+
+func (sf *obfs2ServerFactory) Transport() base.Transport {
+	return sf.transport
+}
+
+func (sf *obfs2ServerFactory) Args() *pt.Args {
+	return nil
+}
+
+func (sf *obfs2ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+	return newObfs2ServerConn(conn)
+}
+
+type obfs2Conn struct {
+	net.Conn
+
+	isInitiator bool
+
+	rx *cipher.StreamReader
+	tx *cipher.StreamWriter
+}
+
+func (conn *obfs2Conn) Read(b []byte) (int, error) {
+	return conn.rx.Read(b)
+}
+
+func (conn *obfs2Conn) Write(b []byte) (int, error) {
+	return conn.tx.Write(b)
+}
+
+func newObfs2ClientConn(conn net.Conn) (c *obfs2Conn, err error) {
+	// Initialize a client connection, and start the handshake timeout.
+	c = &obfs2Conn{conn, true, nil, nil}
+	deadline := time.Now().Add(clientHandshakeTimeout)
+	if err = c.SetDeadline(deadline); err != nil {
+		return nil, err
+	}
+
+	// Handshake.
+	if err = c.handshake(); err != nil {
+		return nil, err
+	}
+
+	// Disarm the handshake timer.
+	if err = c.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func newObfs2ServerConn(conn net.Conn) (c *obfs2Conn, err error) {
+	// Initialize a server connection, and start the handshake timeout.
+	c = &obfs2Conn{conn, false, nil, nil}
+	deadline := time.Now().Add(serverHandshakeTimeout)
+	if err = c.SetDeadline(deadline); err != nil {
+		return nil, err
+	}
+
+	// Handshake.
+	if err = c.handshake(); err != nil {
+		return nil, err
+	}
+
+	// Disarm the handshake timer.
+	if err = c.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func (conn *obfs2Conn) handshake() (err error) {
+	// Each begins by generating a seed and a padding key as follows.
+	// The initiator generates:
+	//
+	//  INIT_SEED = SR(SEED_LENGTH)
+	//  INIT_PAD_KEY = MAC("Initiator obfuscation padding", INIT_SEED)[:KEYLEN]
+	//
+	// And the responder generates:
+	//
+	//  RESP_SEED = SR(SEED_LENGTH)
+	//  RESP_PAD_KEY = MAC("Responder obfuscation padding", INIT_SEED)[:KEYLEN]
+	//
+	// Each then generates a random number PADLEN in range from 0 through
+	// MAX_PADDING (inclusive).
+	var seed [seedLen]byte
+	if err = csrand.Bytes(seed[:]); err != nil {
+		return
+	}
+	var padMagic []byte
+	if conn.isInitiator {
+		padMagic = []byte(initiatorPadString)
+	} else {
+		padMagic = []byte(responderPadString)
+	}
+	padKey, padIV := hsKdf(padMagic, seed[:], conn.isInitiator)
+	padLen := uint32(csrand.IntRange(0, maxPadding))
+
+	hsBlob := make([]byte, hsLen+padLen)
+	binary.BigEndian.PutUint32(hsBlob[0:4], magicValue)
+	binary.BigEndian.PutUint32(hsBlob[4:8], padLen)
+	if padLen > 0 {
+		if err = csrand.Bytes(hsBlob[8:]); err != nil {
+			return
+		}
+	}
+
+	// The initiator then sends:
+	//
+	//  INIT_SEED | E(INIT_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+	//
+	// and the responder sends:
+	//
+	//  RESP_SEED | E(RESP_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN))
+	var txBlock cipher.Block
+	if txBlock, err = aes.NewCipher(padKey); err != nil {
+		return
+	}
+	txStream := cipher.NewCTR(txBlock, padIV)
+	conn.tx = &cipher.StreamWriter{txStream, conn.Conn, nil}
+	if _, err = conn.Conn.Write(seed[:]); err != nil {
+		return
+	}
+	if _, err = conn.Write(hsBlob); err != nil {
+		return
+	}
+
+	// Upon receiving the SEED from the other party, each party derives
+	// the other party's padding key value as above, and decrypts the next
+	// 8 bytes of the key establishment message.
+	var peerSeed [seedLen]byte
+	if _, err = io.ReadFull(conn.Conn, peerSeed[:]); err != nil {
+		return
+	}
+	var peerPadMagic []byte
+	if conn.isInitiator {
+		peerPadMagic = []byte(responderPadString)
+	} else {
+		peerPadMagic = []byte(initiatorPadString)
+	}
+	peerKey, peerIV := hsKdf(peerPadMagic, peerSeed[:], !conn.isInitiator)
+	var rxBlock cipher.Block
+	if rxBlock, err = aes.NewCipher(peerKey); err != nil {
+		return
+	}
+	rxStream := cipher.NewCTR(rxBlock, peerIV)
+	conn.rx = &cipher.StreamReader{rxStream, conn.Conn}
+	hsHdr := make([]byte, hsLen)
+	if _, err = io.ReadFull(conn, hsHdr[:]); err != nil {
+		return
+	}
+
+	// If the MAGIC_VALUE does not match, or the PADLEN value is greater than
+	// MAX_PADDING, the party receiving it should close the connection
+	// immediately.
+	if peerMagic := binary.BigEndian.Uint32(hsHdr[0:4]); peerMagic != magicValue {
+		err = fmt.Errorf("invalid magic value: %x", peerMagic)
+		return
+	}
+	padLen = binary.BigEndian.Uint32(hsHdr[4:8])
+	if padLen > maxPadding {
+		err = fmt.Errorf("padlen too long: %d", padLen)
+		return
+	}
+
+	// Otherwise, it should read the remaining PADLEN bytes of padding data
+	// and discard them.
+	tmp := make([]byte, padLen)
+	if _, err = io.ReadFull(conn.Conn, tmp); err != nil { // Note: Skips AES.
+		return
+	}
+
+	// Derive the actual keys.
+	if err = conn.kdf(seed[:], peerSeed[:]); err != nil {
+		return
+	}
+
+	return
+}
+
+func (conn *obfs2Conn) kdf(seed, peerSeed []byte) (err error) {
+	// Additional keys are then derived as:
+	//
+	//  INIT_SECRET = MAC("Initiator obfuscated data", INIT_SEED|RESP_SEED)
+	//  RESP_SECRET = MAC("Responder obfuscated data", INIT_SEED|RESP_SEED)
+	//  INIT_KEY = INIT_SECRET[:KEYLEN]
+	//  INIT_IV = INIT_SECRET[KEYLEN:]
+	//  RESP_KEY = RESP_SECRET[:KEYLEN]
+	//  RESP_IV = RESP_SECRET[KEYLEN:]
+	combSeed := make([]byte, 0, seedLen*2)
+	if conn.isInitiator {
+		combSeed = append(combSeed, seed...)
+		combSeed = append(combSeed, peerSeed...)
+	} else {
+		combSeed = append(combSeed, peerSeed...)
+		combSeed = append(combSeed, seed...)
+	}
+
+	initKey, initIV := hsKdf([]byte(initiatorKdfString), combSeed, true)
+	var initBlock cipher.Block
+	if initBlock, err = aes.NewCipher(initKey); err != nil {
+		return
+	}
+	initStream := cipher.NewCTR(initBlock, initIV)
+
+	respKey, respIV := hsKdf([]byte(responderKdfString), combSeed, false)
+	var respBlock cipher.Block
+	if respBlock, err = aes.NewCipher(respKey); err != nil {
+		return
+	}
+	respStream := cipher.NewCTR(respBlock, respIV)
+
+	if conn.isInitiator {
+		conn.tx.S = initStream
+		conn.rx.S = respStream
+	} else {
+		conn.tx.S = respStream
+		conn.rx.S = initStream
+	}
+
+	return
+}
+
+func hsKdf(magic, seed []byte, isInitiator bool) (padKey, padIV []byte) {
+	// The actual key/IV is derived in the form of:
+	// m = MAC(magic, seed)
+	// KEY = m[:KEYLEN]
+	// IV = m[KEYLEN:]
+	m := mac(magic, seed)
+	padKey = m[:keyLen]
+	padIV = m[keyLen:]
+
+	return
+}
+
+func mac(s, x []byte) []byte {
+	// H(x) is SHA256 of x.
+	// MAC(s, x) = H(s | x | s)
+	h := sha256.New()
+	h.Write(s)
+	h.Write(x)
+	h.Write(s)
+	return h.Sum(nil)
+}
+
+var _ base.ClientFactory = (*obfs2ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs2ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs2Conn)(nil)
diff --git a/transports/obfs3/obfs3.go b/transports/obfs3/obfs3.go
new file mode 100644
index 0000000..7844443
--- /dev/null
+++ b/transports/obfs3/obfs3.go
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs3 provides an implementation of the Tor Project's obfs3
+// obfuscation protocol.
+package obfs3
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/hmac"
+	"crypto/sha256"
+	"errors"
+	"io"
+	"net"
+	"time"
+
+	"git.torproject.org/pluggable-transports/goptlib.git"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+)
+
+const (
+	transportName = "obfs3"
+
+	clientHandshakeTimeout = time.Duration(30) * time.Second
+	serverHandshakeTimeout = time.Duration(30) * time.Second
+
+	initiatorKdfString   = "Initiator obfuscated data"
+	responderKdfString   = "Responder obfuscated data"
+	initiatorMagicString = "Initiator magic"
+	responderMagicString = "Responder magic"
+	maxPadding           = 8194
+	keyLen               = 16
+)
+
+// Transport is the obfs3 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs3 transport protocol.
+func (t *Transport) Name() string {
+	return transportName
+}
+
+// ClientFactory returns a new obfs3ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+	cf := &obfs3ClientFactory{transport: t}
+	return cf, nil
+}
+
+// ServerFactory returns a new obfs3ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+	sf := &obfs3ServerFactory{transport: t}
+	return sf, nil
+}
+
+type obfs3ClientFactory struct {
+	transport base.Transport
+}
+
+func (cf *obfs3ClientFactory) Transport() base.Transport {
+	return cf.transport
+}
+
+func (cf *obfs3ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+	return nil, nil
+}
+
+func (cf *obfs3ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+	return newObfs3ClientConn(conn)
+}
+
+type obfs3ServerFactory struct {
+	transport base.Transport
+}
+
+func (sf *obfs3ServerFactory) Transport() base.Transport {
+	return sf.transport
+}
+
+func (sf *obfs3ServerFactory) Args() *pt.Args {
+	return nil
+}
+
+func (sf *obfs3ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+	return newObfs3ServerConn(conn)
+}
+
+type obfs3Conn struct {
+	net.Conn
+
+	isInitiator bool
+	rxMagic     []byte
+	txMagic     []byte
+	rxBuf       *bytes.Buffer
+
+	rx *cipher.StreamReader
+	tx *cipher.StreamWriter
+}
+
+func newObfs3ClientConn(conn net.Conn) (c *obfs3Conn, err error) {
+	// Initialize a client connection, and start the handshake timeout.
+	c = &obfs3Conn{conn, true, nil, nil, new(bytes.Buffer), nil, nil}
+	deadline := time.Now().Add(clientHandshakeTimeout)
+	if err = c.SetDeadline(deadline); err != nil {
+		return nil, err
+	}
+
+	// Handshake.
+	if err = c.handshake(); err != nil {
+		return nil, err
+	}
+
+	// Disarm the handshake timer.
+	if err = c.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func newObfs3ServerConn(conn net.Conn) (c *obfs3Conn, err error) {
+	// Initialize a server connection, and start the handshake timeout.
+	c = &obfs3Conn{conn, false, nil, nil, new(bytes.Buffer), nil, nil}
+	deadline := time.Now().Add(serverHandshakeTimeout)
+	if err = c.SetDeadline(deadline); err != nil {
+		return nil, err
+	}
+
+	// Handshake.
+	if err = c.handshake(); err != nil {
+		return nil, err
+	}
+
+	// Disarm the handshake timer.
+	if err = c.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func (conn *obfs3Conn) handshake() (err error) {
+	// The party who opens the connection is the 'initiator'; the one who
+	// accepts it is the 'responder'.  Each begins by generating a
+	// UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2].
+	// Both parties then send:
+	//
+	//  PUB_KEY | WR(PADLEN)
+	var privateKey *uniformdh.PrivateKey
+	if privateKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil {
+		return
+	}
+	padLen := csrand.IntRange(0, maxPadding/2)
+	blob := make([]byte, uniformdh.Size+padLen)
+	var publicKey []byte
+	if publicKey, err = privateKey.PublicKey.Bytes(); err != nil {
+		return
+	}
+	copy(blob[0:], publicKey)
+	if err = csrand.Bytes(blob[uniformdh.Size:]); err != nil {
+		return
+	}
+	if _, err = conn.Conn.Write(blob); err != nil {
+		return
+	}
+
+	// Read the public key from the peer.
+	rawPeerPublicKey := make([]byte, uniformdh.Size)
+	if _, err = io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil {
+		return
+	}
+	var peerPublicKey uniformdh.PublicKey
+	if err = peerPublicKey.SetBytes(rawPeerPublicKey); err != nil {
+		return
+	}
+
+	// After retrieving the public key of the other end, each party
+	// completes the DH key exchange and generates a shared-secret for the
+	// session (named SHARED_SECRET).
+	var sharedSecret []byte
+	if sharedSecret, err = uniformdh.Handshake(privateKey, &peerPublicKey); err != nil {
+		return
+	}
+	if err = conn.kdf(sharedSecret); err != nil {
+		return
+	}
+
+	return
+}
+
+func (conn *obfs3Conn) kdf(sharedSecret []byte) (err error) {
+	// Using that shared-secret each party derives its encryption keys as
+	// follows:
+	//
+	//   INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data")
+	//   RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data")
+	//   INIT_KEY = INIT_SECRET[:KEYLEN]
+	//   INIT_COUNTER = INIT_SECRET[KEYLEN:]
+	//   RESP_KEY = RESP_SECRET[:KEYLEN]
+	//   RESP_COUNTER = RESP_SECRET[KEYLEN:]
+	initHmac := hmac.New(sha256.New, sharedSecret)
+	initHmac.Write([]byte(initiatorKdfString))
+	initSecret := initHmac.Sum(nil)
+	initHmac.Reset()
+	initHmac.Write([]byte(initiatorMagicString))
+	initMagic := initHmac.Sum(nil)
+
+	respHmac := hmac.New(sha256.New, sharedSecret)
+	respHmac.Write([]byte(responderKdfString))
+	respSecret := respHmac.Sum(nil)
+	respHmac.Reset()
+	respHmac.Write([]byte(responderMagicString))
+	respMagic := respHmac.Sum(nil)
+
+	// The INIT_KEY value keys a block cipher (in CTR mode) used to
+	// encrypt values from initiator to responder thereafter.  The counter
+	// mode's initial counter value is INIT_COUNTER.  The RESP_KEY value
+	// keys a block cipher (in CTR mode) used to encrypt values from
+	// responder to initiator thereafter.  That counter mode's initial
+	// counter value is RESP_COUNTER.
+	//
+	// Note: To have this be the last place where the shared secret is used,
+	// also generate the magic value to send/scan for here.
+	var initBlock cipher.Block
+	if initBlock, err = aes.NewCipher(initSecret[:keyLen]); err != nil {
+		return err
+	}
+	initStream := cipher.NewCTR(initBlock, initSecret[keyLen:])
+
+	var respBlock cipher.Block
+	if respBlock, err = aes.NewCipher(respSecret[:keyLen]); err != nil {
+		return err
+	}
+	respStream := cipher.NewCTR(respBlock, respSecret[keyLen:])
+
+	if conn.isInitiator {
+		conn.tx = &cipher.StreamWriter{initStream, conn.Conn, nil}
+		conn.rx = &cipher.StreamReader{respStream, conn.rxBuf}
+		conn.txMagic = initMagic
+		conn.rxMagic = respMagic
+	} else {
+		conn.tx = &cipher.StreamWriter{respStream, conn.Conn, nil}
+		conn.rx = &cipher.StreamReader{initStream, conn.rxBuf}
+		conn.txMagic = respMagic
+		conn.rxMagic = initMagic
+	}
+
+	return
+}
+
+func (conn *obfs3Conn) findPeerMagic() error {
+	var hsBuf [maxPadding + sha256.Size]byte
+	for {
+		n, err := conn.Conn.Read(hsBuf[:])
+		if err != nil {
+			// Yes, Read can return partial data and an error, but continuing
+			// past that is nonsensical.
+			return err
+		}
+		conn.rxBuf.Write(hsBuf[:n])
+
+		pos := bytes.Index(conn.rxBuf.Bytes(), conn.rxMagic)
+		if pos == -1 {
+			if conn.rxBuf.Len() >= maxPadding+sha256.Size {
+				return errors.New("failed to find peer magic value")
+			}
+			continue
+		} else if pos > maxPadding {
+			return errors.New("peer sent too much pre-magic-padding")
+		}
+
+		// Discard the padding/MAC.
+		pos += len(conn.rxMagic)
+		_ = conn.rxBuf.Next(pos)
+
+		return nil
+	}
+}
+
+func (conn *obfs3Conn) Read(b []byte) (n int, err error) {
+	// If this is the first time we read data post handshake, scan for the
+	// magic value.
+	if conn.rxMagic != nil {
+		if err = conn.findPeerMagic(); err != nil {
+			conn.Close()
+			return
+		}
+		conn.rxMagic = nil
+	}
+
+	// If the handshake receive buffer is still present...
+	if conn.rxBuf != nil {
+		// And it is empty...
+		if conn.rxBuf.Len() == 0 {
+			// There is no more trailing data left from the handshake process,
+			// so rewire the cipher.StreamReader to pull data from the network
+			// instead of the temporary receive buffer.
+			conn.rx.R = conn.Conn
+			conn.rxBuf = nil
+		}
+	}
+
+	return conn.rx.Read(b)
+}
+
+func (conn *obfs3Conn) Write(b []byte) (n int, err error) {
+	// If this is the first time we write data post handshake, send the
+	// padding/magic value.
+	if conn.txMagic != nil {
+		padLen := csrand.IntRange(0, maxPadding/2)
+		blob := make([]byte, padLen+len(conn.txMagic))
+		if err = csrand.Bytes(blob[:padLen]); err != nil {
+			conn.Close()
+			return
+		}
+		copy(blob[padLen:], conn.txMagic)
+		if _, err = conn.Conn.Write(blob); err != nil {
+			conn.Close()
+			return
+		}
+		conn.txMagic = nil
+	}
+
+	return conn.tx.Write(b)
+}
+
+
+var _ base.ClientFactory = (*obfs3ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs3ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs3Conn)(nil)
diff --git a/framing/framing.go b/transports/obfs4/framing/framing.go
similarity index 97%
rename from framing/framing.go
rename to transports/obfs4/framing/framing.go
index 725a762..04e788f 100644
--- a/framing/framing.go
+++ b/transports/obfs4/framing/framing.go
@@ -69,8 +69,8 @@ import (
 
 	"code.google.com/p/go.crypto/nacl/secretbox"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
-	"git.torproject.org/pluggable-transports/obfs4.git/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
 )
 
 const (
@@ -168,7 +168,7 @@ func NewEncoder(key []byte) *Encoder {
 	if err != nil {
 		panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
 	}
-	encoder.drbg = drbg.NewHashDrbg(seed)
+	encoder.drbg, _ = drbg.NewHashDrbg(seed)
 
 	return encoder
 }
@@ -231,7 +231,7 @@ func NewDecoder(key []byte) *Decoder {
 	if err != nil {
 		panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
 	}
-	decoder.drbg = drbg.NewHashDrbg(seed)
+	decoder.drbg, _ = drbg.NewHashDrbg(seed)
 
 	return decoder
 }
@@ -306,5 +306,3 @@ func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) {
 
 	return len(out), nil
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/framing/framing_test.go b/transports/obfs4/framing/framing_test.go
similarity index 99%
rename from framing/framing_test.go
rename to transports/obfs4/framing/framing_test.go
index 7df0e28..03e0d3b 100644
--- a/framing/framing_test.go
+++ b/transports/obfs4/framing/framing_test.go
@@ -167,5 +167,3 @@ func BenchmarkEncoder_Encode(b *testing.B) {
 		}
 	}
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/handshake_ntor.go b/transports/obfs4/handshake_ntor.go
similarity index 96%
rename from handshake_ntor.go
rename to transports/obfs4/handshake_ntor.go
index 8192494..8dcf0c8 100644
--- a/handshake_ntor.go
+++ b/transports/obfs4/handshake_ntor.go
@@ -38,9 +38,10 @@ import (
 	"strconv"
 	"time"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/csrand"
-	"git.torproject.org/pluggable-transports/obfs4.git/framing"
-	"git.torproject.org/pluggable-transports/obfs4.git/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
 )
 
 const (
@@ -247,7 +248,7 @@ func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair, sessi
 	return hs
 }
 
-func (hs *serverHandshake) parseClientHandshake(filter *replayFilter, resp []byte) ([]byte, error) {
+func (hs *serverHandshake) parseClientHandshake(filter *replayfilter.ReplayFilter, resp []byte) ([]byte, error) {
 	// No point in examining the data unless the miminum plausible response has
 	// been received.
 	if clientMinHandshakeLength > len(resp) {
@@ -287,7 +288,7 @@ func (hs *serverHandshake) parseClientHandshake(filter *replayFilter, resp []byt
 		macRx := resp[pos+markLength : pos+markLength+macLength]
 		if hmac.Equal(macCmp, macRx) {
 			// Ensure that this handshake has not been seen previously.
-			if filter.testAndSet(time.Now().Unix(), macRx) {
+			if filter.TestAndSet(time.Now(), macRx) {
 				// The client either happened to generate exactly the same
 				// session key and padding, or someone is replaying a previous
 				// handshake.  In either case, fuck them.
@@ -423,5 +424,3 @@ func makePad(padLen int) ([]byte, error) {
 
 	return pad, err
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go
similarity index 97%
rename from handshake_ntor_test.go
rename to transports/obfs4/handshake_ntor_test.go
index 94d2a7f..fa03420 100644
--- a/handshake_ntor_test.go
+++ b/transports/obfs4/handshake_ntor_test.go
@@ -31,14 +31,15 @@ import (
 	"bytes"
 	"testing"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
 )
 
 func TestHandshakeNtor(t *testing.T) {
 	// Generate the server node id and id keypair.
 	nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"))
 	idKeypair, _ := ntor.NewKeypair(false)
-	serverFilter, _ := newReplayFilter()
+	serverFilter, _ := replayfilter.New(replayTTL)
 
 	// Test client handshake padding.
 	for l := clientMinPadLength; l <= clientMaxPadLength; l++ {
diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go
new file mode 100644
index 0000000..7af7224
--- /dev/null
+++ b/transports/obfs4/obfs4.go
@@ -0,0 +1,579 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package obfs4 provides an implementation of the Tor Project's obfs4
+// obfuscation protocol.
+package obfs4
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"fmt"
+	"math/rand"
+	"net"
+	"syscall"
+	"time"
+
+	"git.torproject.org/pluggable-transports/goptlib.git"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
+)
+
+const (
+	transportName = "obfs4"
+
+	nodeIDArg     = "node-id"
+	publicKeyArg  = "public-key"
+	privateKeyArg = "private-key"
+	seedArg       = "drbg-seed"
+
+	seedLength             = 32
+	headerLength           = framing.FrameOverhead + packetOverhead
+	clientHandshakeTimeout = time.Duration(60) * time.Second
+	serverHandshakeTimeout = time.Duration(30) * time.Second
+	replayTTL              = time.Duration(3) * time.Hour
+
+	// Use a ScrambleSuit style biased probability table.
+	biasedDist = false
+
+	// Use IAT obfuscation.
+	iatObfuscation = false
+
+	// Maximum IAT delay (100 usec increments).
+	maxIATDelay = 100
+
+	maxCloseDelayBytes = maxHandshakeLength
+	maxCloseDelay      = 60
+)
+
+type obfs4ClientArgs struct {
+	nodeID     *ntor.NodeID
+	publicKey  *ntor.PublicKey
+	sessionKey *ntor.Keypair
+}
+
+// Transport is the obfs4 implementation of the base.Transport interface.
+type Transport struct{}
+
+// Name returns the name of the obfs4 transport protocol.
+func (t *Transport) Name() string {
+	return transportName
+}
+
+// ClientFactory returns a new obfs4ClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
+	cf := &obfs4ClientFactory{transport: t}
+	return cf, nil
+}
+
+// ServerFactory returns a new obfs4ServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
+	var err error
+
+	var st *obfs4ServerState
+	if st, err = serverStateFromArgs(stateDir, args); err != nil {
+		return nil, err
+	}
+
+	var iatSeed *drbg.Seed
+	if iatObfuscation {
+		iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:])
+		iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:])
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Store the arguments that should appear in our descriptor for the clients.
+	ptArgs := pt.Args{}
+	ptArgs.Add(nodeIDArg, st.nodeID.Base64())
+	ptArgs.Add(publicKeyArg, st.identityKey.Public().Base64())
+
+	// Initialize the replay filter.
+	filter, err := replayfilter.New(replayTTL)
+	if err != nil {
+		return nil, err
+	}
+
+	// Initialize the close thresholds for failed connections.
+	drbg, err := drbg.NewHashDrbg(st.drbgSeed)
+	if err != nil {
+		return nil, err
+	}
+	rng := rand.New(drbg)
+
+	sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)}
+	return sf, nil
+}
+
+type obfs4ClientFactory struct {
+	transport base.Transport
+}
+
+func (cf *obfs4ClientFactory) Transport() base.Transport {
+	return cf.transport
+}
+
+func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+	var err error
+
+	// Handle the arguments.
+	nodeIDStr, ok := args.Get(nodeIDArg)
+	if !ok {
+		return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+	}
+	var nodeID *ntor.NodeID
+	if nodeID, err = ntor.NodeIDFromBase64(nodeIDStr); err != nil {
+		return nil, err
+	}
+
+	publicKeyStr, ok := args.Get(publicKeyArg)
+	if !ok {
+		return nil, fmt.Errorf("missing argument '%s'", publicKeyArg)
+	}
+	var publicKey *ntor.PublicKey
+	if publicKey, err = ntor.PublicKeyFromBase64(publicKeyStr); err != nil {
+		return nil, err
+	}
+
+	// Generate the session key pair before connectiong to hide the Elligator2
+	// rejection sampling from network observers.
+	sessionKey, err := ntor.NewKeypair(true)
+	if err != nil {
+		return nil, err
+	}
+
+	return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil
+}
+
+func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
+	ca, ok := args.(*obfs4ClientArgs)
+	if !ok {
+		return nil, fmt.Errorf("invalid argument type for args")
+	}
+
+	return newObfs4ClientConn(conn, ca)
+}
+
+type obfs4ServerFactory struct {
+	transport base.Transport
+	args      *pt.Args
+
+	nodeID       *ntor.NodeID
+	identityKey  *ntor.Keypair
+	lenSeed      *drbg.Seed
+	iatSeed      *drbg.Seed
+	replayFilter *replayfilter.ReplayFilter
+
+	closeDelayBytes int
+	closeDelay      int
+}
+
+func (sf *obfs4ServerFactory) Transport() base.Transport {
+	return sf.transport
+}
+
+func (sf *obfs4ServerFactory) Args() *pt.Args {
+	return sf.args
+}
+
+func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) {
+	// Not much point in having a separate newObfs4ServerConn routine when
+	// wrapping requires using values from the factory instance.
+
+	// Generate the session keypair *before* consuming data from the peer, to
+	// attempt to mask the rejection sampling due to use of Elligator2.  This
+	// might be futile, but the timing differential isn't very large on modern
+	// hardware, and there are far easier statistical attacks that can be
+	// mounted as a distinguisher.
+	sessionKey, err := ntor.NewKeypair(true)
+	if err != nil {
+		return nil, err
+	}
+
+	lenDist := probdist.New(sf.lenSeed, 0, framing.MaximumSegmentLength, biasedDist)
+	var iatDist *probdist.WeightedDist
+	if sf.iatSeed != nil {
+		iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist)
+	}
+
+	c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+	startTime := time.Now()
+
+	if err = c.serverHandshake(sf, sessionKey); err != nil {
+		c.closeAfterDelay(sf, startTime)
+		return nil, err
+	}
+
+	return c, nil
+}
+
+type obfs4Conn struct {
+	net.Conn
+
+	isServer bool
+
+	lenDist *probdist.WeightedDist
+	iatDist *probdist.WeightedDist
+
+	receiveBuffer        *bytes.Buffer
+	receiveDecodedBuffer *bytes.Buffer
+
+	encoder *framing.Encoder
+	decoder *framing.Decoder
+}
+
+func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err error) {
+	// Generate the initial protocol polymorphism distribution(s).
+	var seed *drbg.Seed
+	if seed, err = drbg.NewSeed(); err != nil {
+		return
+	}
+	lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist)
+	var iatDist *probdist.WeightedDist
+	if iatObfuscation {
+		var iatSeed *drbg.Seed
+		iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
+		if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil {
+			return
+		}
+		iatDist = probdist.New(iatSeed, 0, maxIATDelay, biasedDist)
+	}
+
+	// Allocate the client structure.
+	c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil}
+
+	// Start the handshake timeout.
+	deadline := time.Now().Add(clientHandshakeTimeout)
+	if err = conn.SetDeadline(deadline); err != nil {
+		return nil, err
+	}
+
+	if err = c.clientHandshake(args.nodeID, args.publicKey, args.sessionKey); err != nil {
+		return nil, err
+	}
+
+	// Stop the handshake timeout.
+	if err = conn.SetDeadline(time.Time{}); err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func (conn *obfs4Conn) clientHandshake(nodeID *ntor.NodeID, peerIdentityKey *ntor.PublicKey, sessionKey *ntor.Keypair) error {
+	if conn.isServer {
+		return fmt.Errorf("clientHandshake called on server connection")
+	}
+
+	// Generate and send the client handshake.
+	hs := newClientHandshake(nodeID, peerIdentityKey, sessionKey)
+	blob, err := hs.generateHandshake()
+	if err != nil {
+		return err
+	}
+	if _, err = conn.Conn.Write(blob); err != nil {
+		return err
+	}
+
+	// Consume the server handshake.
+	var hsBuf [maxHandshakeLength]byte
+	for {
+		var n int
+		if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+			// The Read() could have returned data and an error, but there is
+			// no point in continuing on an EOF or whatever.
+			return err
+		}
+		conn.receiveBuffer.Write(hsBuf[:n])
+
+		var seed []byte
+		n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes())
+		if err == ErrMarkNotFoundYet {
+			continue
+		} else if err != nil {
+			return err
+		}
+		_ = conn.receiveBuffer.Next(n)
+
+		// Use the derived key material to intialize the link crypto.
+		okm := ntor.Kdf(seed, framing.KeyLength*2)
+		conn.encoder = framing.NewEncoder(okm[:framing.KeyLength])
+		conn.decoder = framing.NewDecoder(okm[framing.KeyLength:])
+
+		return nil
+	}
+}
+
+func (conn *obfs4Conn) serverHandshake(sf *obfs4ServerFactory, sessionKey *ntor.Keypair) (err error) {
+	if !conn.isServer {
+		return fmt.Errorf("serverHandshake called on client connection")
+	}
+
+	// Generate the server handshake, and arm the base timeout.
+	hs := newServerHandshake(sf.nodeID, sf.identityKey, sessionKey)
+	if err = conn.Conn.SetDeadline(time.Now().Add(serverHandshakeTimeout)); err != nil {
+		return
+	}
+
+	// Consume the client handshake.
+	var hsBuf [maxHandshakeLength]byte
+	for {
+		var n int
+		if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+			// The Read() could have returned data and an error, but there is
+			// no point in continuing on an EOF or whatever.
+			return
+		}
+		conn.receiveBuffer.Write(hsBuf[:n])
+
+		var seed []byte
+		seed, err = hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes())
+		if err == ErrMarkNotFoundYet {
+			continue
+		} else if err != nil {
+			return
+		}
+		conn.receiveBuffer.Reset()
+
+		if err = conn.Conn.SetDeadline(time.Time{}); err != nil {
+			return
+		}
+
+		// Use the derived key material to intialize the link crypto.
+		okm := ntor.Kdf(seed, framing.KeyLength*2)
+		conn.encoder = framing.NewEncoder(okm[framing.KeyLength:])
+		conn.decoder = framing.NewDecoder(okm[:framing.KeyLength])
+
+		break
+	}
+
+	// Since the current and only implementation always sends a PRNG seed for
+	// the length obfuscation, this makes the amount of data received from the
+	// server inconsistent with the length sent from the client.
+	//
+	// Rebalance this by tweaking the client mimimum padding/server maximum
+	// padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed
+	// as part of the server response).  See inlineSeedFrameLength in
+	// handshake_ntor.go.
+
+	// Generate/send the response.
+	var blob []byte
+	blob, err = hs.generateHandshake()
+	if err != nil {
+		return
+	}
+	var frameBuf bytes.Buffer
+	_, err = frameBuf.Write(blob)
+	if err != nil {
+		return
+	}
+
+	// Send the PRNG seed as the first packet.
+	if err = conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil {
+		return
+	}
+	if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil {
+		return
+	}
+
+	return
+}
+
+func (conn *obfs4Conn) Read(b []byte) (n int, err error) {
+	// If there is no payload from the previous Read() calls, consume data off
+	// the network.  Not all data received is guaranteed to be usable payload,
+	// so do this in a loop till data is present or an error occurs.
+	for conn.receiveDecodedBuffer.Len() == 0 {
+		err = conn.readPackets()
+		if err == framing.ErrAgain {
+			// Don't proagate this back up the call stack if we happen to break
+			// out of the loop.
+			err = nil
+			continue
+		} else if err != nil {
+			break
+		}
+	}
+
+	// Even if err is set, attempt to do the read anyway so that all decoded
+	// data gets relayed before the connection is torn down.
+	if conn.receiveDecodedBuffer.Len() > 0 {
+		var berr error
+		n, berr = conn.receiveDecodedBuffer.Read(b)
+		if err == nil {
+			// Only propagate berr if there are not more important (fatal)
+			// errors from the network/crypto/packet processing.
+			err = berr
+		}
+	}
+
+	return
+}
+
+func (conn *obfs4Conn) Write(b []byte) (n int, err error) {
+	chopBuf := bytes.NewBuffer(b)
+	var payload [maxPacketPayloadLength]byte
+	var frameBuf bytes.Buffer
+
+	// Chop the pending data into payload frames.
+	for chopBuf.Len() > 0 {
+		// Send maximum sized frames.
+		rdLen := 0
+		rdLen, err = chopBuf.Read(payload[:])
+		if err != nil {
+			return 0, err
+		} else if rdLen == 0 {
+			panic(fmt.Sprintf("BUG: Write(), chopping length was 0"))
+		}
+		n += rdLen
+
+		err = conn.makePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0)
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	// Add the length obfuscation padding.  In theory, this could be inlined
+	// with the last chopped packet for certain (most?) payload lenghts, but
+	// this is simpler.
+
+	if err = conn.padBurst(&frameBuf); err != nil {
+		return 0, err
+	}
+
+	// Write the pending data onto the network.  Partial writes are fatal,
+	// because the frame encoder state is advanced, and the code doesn't keep
+	// frameBuf around.  In theory, write timeouts and whatnot could be
+	// supported if this wasn't the case, but that complicates the code.
+
+	if conn.iatDist != nil {
+		var iatFrame [framing.MaximumSegmentLength]byte
+		for frameBuf.Len() > 0 {
+			iatWrLen := 0
+			iatWrLen, err = frameBuf.Read(iatFrame[:])
+			if err != nil {
+				return 0, err
+			} else if iatWrLen == 0 {
+				panic(fmt.Sprintf("BUG: Write(), iat length was 0"))
+			}
+
+			// Calculate the delay.  The delay resolution is 100 usec, leading
+			// to a maximum delay of 10 msec.
+			iatDelta := time.Duration(conn.iatDist.Sample() * 100)
+
+			// Write then sleep.
+			_, err = conn.Conn.Write(iatFrame[:iatWrLen])
+			if err != nil {
+				return 0, err
+			}
+			time.Sleep(iatDelta * time.Microsecond)
+		}
+	} else {
+		_, err = conn.Conn.Write(frameBuf.Bytes())
+	}
+
+	return
+}
+
+func (conn *obfs4Conn) SetDeadline(t time.Time) error {
+	return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) SetWriteDeadline(t time.Time) error {
+	return syscall.ENOTSUP
+}
+
+func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Time) {
+	// I-it's not like I w-wanna handshake with you or anything.  B-b-baka!
+	defer conn.Conn.Close()
+
+	delay := time.Duration(sf.closeDelay)*time.Second + serverHandshakeTimeout
+	deadline := startTime.Add(delay)
+	if time.Now().After(deadline) {
+		return
+	}
+
+	if err := conn.Conn.SetReadDeadline(deadline); err != nil {
+		return
+	}
+
+	// Consume and discard data on this connection until either the specified
+	// interval passes or a certain size has been reached.
+	discarded := 0
+	var buf [framing.MaximumSegmentLength]byte
+	for discarded < int(sf.closeDelayBytes) {
+		n, err := conn.Conn.Read(buf[:])
+		if err != nil {
+			return
+		}
+		discarded += n
+	}
+}
+
+func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) {
+	tailLen := burst.Len() % framing.MaximumSegmentLength
+	toPadTo := conn.lenDist.Sample()
+
+	padLen := 0
+	if toPadTo >= tailLen {
+		padLen = toPadTo - tailLen
+	} else {
+		padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo
+	}
+
+	if padLen > headerLength {
+		err = conn.makePacket(burst, packetTypePayload, []byte{},
+			uint16(padLen-headerLength))
+		if err != nil {
+			return
+		}
+	} else if padLen > 0 {
+		err = conn.makePacket(burst, packetTypePayload, []byte{},
+			maxPacketPayloadLength)
+		if err != nil {
+			return
+		}
+		err = conn.makePacket(burst, packetTypePayload, []byte{},
+			uint16(padLen))
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+var _ base.ClientFactory = (*obfs4ClientFactory)(nil)
+var _ base.ServerFactory = (*obfs4ServerFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
+var _ net.Conn = (*obfs4Conn)(nil)
diff --git a/packet.go b/transports/obfs4/packet.go
similarity index 73%
rename from packet.go
rename to transports/obfs4/packet.go
index 58a5877..9865c82 100644
--- a/packet.go
+++ b/transports/obfs4/packet.go
@@ -32,17 +32,16 @@ import (
 	"encoding/binary"
 	"fmt"
 	"io"
-	"syscall"
 
-	"git.torproject.org/pluggable-transports/obfs4.git/drbg"
-	"git.torproject.org/pluggable-transports/obfs4.git/framing"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing"
 )
 
 const (
 	packetOverhead          = 2 + 1
 	maxPacketPayloadLength  = framing.MaximumFramePayloadLength - packetOverhead
 	maxPacketPaddingLength  = maxPacketPayloadLength
-	seedPacketPayloadLength = SeedLength
+	seedPacketPayloadLength = seedLength
 
 	consumeReadSize = framing.MaximumSegmentLength * 16
 )
@@ -70,24 +69,14 @@ func (e InvalidPayloadLengthError) Error() string {
 
 var zeroPadBytes [maxPacketPaddingLength]byte
 
-func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) {
+func (conn *obfs4Conn) makePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) {
 	var pkt [framing.MaximumFramePayloadLength]byte
 
-	if !c.CanReadWrite() {
-		return syscall.EINVAL
-	}
-
 	if len(data)+int(padLen) > maxPacketPayloadLength {
 		panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d",
 			len(data), padLen, maxPacketPayloadLength))
 	}
 
-	defer func() {
-		if err != nil {
-			c.setBroken()
-		}
-	}()
-
 	// Packets are:
 	//   uint8_t type      packetTypePayload (0x00)
 	//   uint16_t length   Length of the payload (Big Endian).
@@ -105,7 +94,7 @@ func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLe
 	// Encode the packet in an AEAD frame.
 	var frame [framing.MaximumSegmentLength]byte
 	frameLen := 0
-	frameLen, err = c.encoder.Encode(frame[:], pkt[:pktLen])
+	frameLen, err = conn.encoder.Encode(frame[:], pkt[:pktLen])
 	if err != nil {
 		// All encoder errors are fatal.
 		return
@@ -122,19 +111,17 @@ func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLe
 	return
 }
 
-func (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) {
-	if !c.CanReadWrite() {
-		return n, syscall.EINVAL
-	}
-
+func (conn *obfs4Conn) readPackets() (err error) {
+	// Attempt to read off the network.
 	var buf [consumeReadSize]byte
-	rdLen, rdErr := c.conn.Read(buf[:])
-	c.receiveBuffer.Write(buf[:rdLen])
+	rdLen, rdErr := conn.Conn.Read(buf[:])
+	conn.receiveBuffer.Write(buf[:rdLen])
+
 	var decoded [framing.MaximumFramePayloadLength]byte
-	for c.receiveBuffer.Len() > 0 {
+	for conn.receiveBuffer.Len() > 0 {
 		// Decrypt an AEAD frame.
 		decLen := 0
-		decLen, err = c.decoder.Decode(decoded[:], &c.receiveBuffer)
+		decLen, err = conn.decoder.Decode(decoded[:], conn.receiveBuffer)
 		if err == framing.ErrAgain {
 			break
 		} else if err != nil {
@@ -157,56 +144,36 @@ func (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) {
 		switch pktType {
 		case packetTypePayload:
 			if payloadLen > 0 {
-				if w != nil {
-					// c.WriteTo() skips buffering in c.receiveDecodedBuffer
-					var wrLen int
-					wrLen, err = w.Write(payload)
-					n += wrLen
-					if err != nil {
-						break
-					} else if wrLen < int(payloadLen) {
-						err = io.ErrShortWrite
-						break
-					}
-				} else {
-					// c.Read() stashes decoded payload in receiveDecodedBuffer
-					c.receiveDecodedBuffer.Write(payload)
-					n += int(payloadLen)
-				}
+				conn.receiveDecodedBuffer.Write(payload)
 			}
 		case packetTypePrngSeed:
 			// Only regenerate the distribution if we are the client.
-			if len(payload) == seedPacketPayloadLength && !c.isServer {
+			if len(payload) == seedPacketPayloadLength && !conn.isServer {
 				var seed *drbg.Seed
 				seed, err = drbg.SeedFromBytes(payload)
 				if err != nil {
 					break
 				}
-				c.lenProbDist.reset(seed)
-				if c.iatProbDist != nil {
+				conn.lenDist.Reset(seed)
+				if conn.iatDist != nil {
 					iatSeedSrc := sha256.Sum256(seed.Bytes()[:])
 					iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:])
 					if err != nil {
 						break
 					}
-					c.iatProbDist.reset(iatSeed)
+					conn.iatDist.Reset(iatSeed)
 				}
 			}
 		default:
-			// Ignore unrecognised packet types.
+			// Ignore unknown packet types.
 		}
 	}
 
-	// Read errors and non-framing.ErrAgain errors are all fatal.
-	if (err != nil && err != framing.ErrAgain) || rdErr != nil {
-		// Propagate read errors correctly.
-		if err == nil && rdErr != nil {
-			err = rdErr
-		}
-		c.setBroken()
+	// Read errors (all fatal) take priority over various frame processing
+	// errors.
+	if rdErr != nil {
+		return rdErr
 	}
 
 	return
 }
-
-/* vim :set ts=4 sw=4 sts=4 noet : */
diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go
new file mode 100644
index 0000000..814a545
--- /dev/null
+++ b/transports/obfs4/statefile.go
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package obfs4
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+
+	"git.torproject.org/pluggable-transports/goptlib.git"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
+	"git.torproject.org/pluggable-transports/obfs4.git/common/ntor"
+)
+
+const (
+	stateFile = "obfs4_state.json"
+)
+
+type jsonServerState struct {
+	NodeID     string `json:"node-id"`
+	PrivateKey string `json:"private-key"`
+	PublicKey  string `json:"public-key"`
+	DrbgSeed   string `json:"drbgSeed"`
+}
+
+type obfs4ServerState struct {
+	nodeID      *ntor.NodeID
+	identityKey *ntor.Keypair
+	drbgSeed    *drbg.Seed
+}
+
+func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, error) {
+	var js jsonServerState
+	var nodeIDOk, privKeyOk, seedOk bool
+
+	js.NodeID, nodeIDOk = args.Get(nodeIDArg)
+	js.PrivateKey, privKeyOk = args.Get(privateKeyArg)
+	js.DrbgSeed, seedOk = args.Get(seedArg)
+
+	if !privKeyOk && !nodeIDOk && !seedOk {
+		if err := jsonServerStateFromFile(stateDir, &js); err != nil {
+			return nil, err
+		}
+	} else if !privKeyOk {
+		return nil, fmt.Errorf("missing argument '%s'", privateKeyArg)
+	} else if !nodeIDOk {
+		return nil, fmt.Errorf("missing argument '%s'", nodeIDArg)
+	} else if !seedOk {
+		return nil, fmt.Errorf("missing argument '%s'", seedArg)
+	}
+
+	return serverStateFromJSONServerState(&js)
+}
+
+func serverStateFromJSONServerState(js *jsonServerState) (*obfs4ServerState, error) {
+	var err error
+
+	st := new(obfs4ServerState)
+	if st.nodeID, err = ntor.NodeIDFromBase64(js.NodeID); err != nil {
+		return nil, err
+	}
+	if st.identityKey, err = ntor.KeypairFromBase64(js.PrivateKey); err != nil {
+		return nil, err
+	}
+	var rawSeed []byte
+	if rawSeed, err = base64.StdEncoding.DecodeString(js.DrbgSeed); err != nil {
+		return nil, err
+	}
+	if st.drbgSeed, err = drbg.SeedFromBytes(rawSeed); err != nil {
+		return nil, err
+	}
+
+	return st, nil
+}
+
+func jsonServerStateFromFile(stateDir string, js *jsonServerState) error {
+	f, err := ioutil.ReadFile(path.Join(stateDir, stateFile))
+	if err != nil {
+		if os.IsNotExist(err) {
+			if err = newJSONServerState(stateDir, js); err == nil {
+				return nil
+			}
+		}
+		return err
+	}
+
+	if err = json.Unmarshal(f, js); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func newJSONServerState(stateDir string, js *jsonServerState) (err error) {
+	// Generate everything a server needs, using the cryptographic PRNG.
+	var st obfs4ServerState
+	rawID := make([]byte, ntor.NodeIDLength)
+	if err = csrand.Bytes(rawID); err != nil {
+		return
+	}
+	if st.nodeID, err = ntor.NewNodeID(rawID); err != nil {
+		return
+	}
+	if st.identityKey, err = ntor.NewKeypair(false); err != nil {
+		return
+	}
+	if st.drbgSeed, err = drbg.NewSeed(); err != nil {
+		return
+	}
+
+	// Encode it into JSON format and write the state file.
+	js.NodeID = st.nodeID.Base64()
+	js.PrivateKey = st.identityKey.Private().Base64()
+	js.PublicKey = st.identityKey.Public().Base64()
+	js.DrbgSeed = st.drbgSeed.Base64()
+
+	var encoded []byte
+	if encoded, err = json.Marshal(js); err != nil {
+		return
+	}
+
+	if err = ioutil.WriteFile(path.Join(stateDir, stateFile), encoded, 0600); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/transports/transports.go b/transports/transports.go
new file mode 100644
index 0000000..6b80bdc
--- /dev/null
+++ b/transports/transports.go
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package transports provides a interface to query supported pluggable
+// transports.
+package transports
+
+import (
+	"fmt"
+	"sync"
+
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs2"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs3"
+	"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4"
+)
+
+var transportMapLock sync.Mutex
+var transportMap map[string]base.Transport
+
+// Register registers a transport protocol.
+func Register(transport base.Transport) error {
+	transportMapLock.Lock()
+	defer transportMapLock.Unlock()
+
+	name := transport.Name()
+	_, registered := transportMap[name]
+	if registered {
+		return fmt.Errorf("transport '%s' already registered", name)
+	}
+	transportMap[name] = transport
+
+	return nil
+}
+
+// Transports returns the list of registered transport protocols.
+func Transports() []string {
+	transportMapLock.Lock()
+	defer transportMapLock.Unlock()
+
+	var ret []string
+	for name := range transportMap {
+		ret = append(ret, name)
+	}
+
+	return ret
+}
+
+// Get returns a transport protocol implementation by name.
+func Get(name string) base.Transport {
+	transportMapLock.Lock()
+	defer transportMapLock.Unlock()
+
+	t := transportMap[name]
+
+	return t
+}
+
+func init() {
+	// Initialize the transport list.
+	transportMap = make(map[string]base.Transport)
+
+	// Register all the currently supported transports.
+	Register(new(obfs2.Transport))
+	Register(new(obfs3.Transport))
+	Register(new(obfs4.Transport))
+}

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



More information about the Pkg-privacy-commits mailing list