summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md23
-rw-r--r--common/csrand/csrand.go (renamed from csrand/csrand.go)21
-rw-r--r--common/drbg/hash_drbg.go (renamed from drbg/hash_drbg.go)20
-rw-r--r--common/ntor/ntor.go (renamed from ntor/ntor.go)9
-rw-r--r--common/ntor/ntor_test.go (renamed from ntor/ntor_test.go)2
-rw-r--r--common/probdist/weighted_dist.go (renamed from weighted_dist.go)56
-rw-r--r--common/probdist/weighted_dist_test.go (renamed from weighted_dist_test.go)10
-rw-r--r--common/replayfilter/replay_filter.go147
-rw-r--r--common/replayfilter/replay_filter_test.go (renamed from replay_filter_test.go)29
-rw-r--r--common/uniformdh/uniformdh.go183
-rw-r--r--common/uniformdh/uniformdh_test.go220
-rw-r--r--obfs4.go758
-rw-r--r--obfs4proxy/obfs4proxy.go553
-rw-r--r--obfs4proxy/proxy_extras.go51
-rw-r--r--obfs4proxy/proxy_http.go3
-rw-r--r--obfs4proxy/proxy_socks4.go2
-rw-r--r--obfs4proxy/pt_extras.go23
-rw-r--r--replay_filter.go145
-rw-r--r--transports/base/base.go88
-rw-r--r--transports/obfs2/obfs2.go367
-rw-r--r--transports/obfs3/obfs3.go358
-rw-r--r--transports/obfs4/framing/framing.go (renamed from framing/framing.go)10
-rw-r--r--transports/obfs4/framing/framing_test.go (renamed from framing/framing_test.go)2
-rw-r--r--transports/obfs4/handshake_ntor.go (renamed from handshake_ntor.go)13
-rw-r--r--transports/obfs4/handshake_ntor_test.go (renamed from handshake_ntor_test.go)5
-rw-r--r--transports/obfs4/obfs4.go579
-rw-r--r--transports/obfs4/packet.go (renamed from packet.go)77
-rw-r--r--transports/obfs4/statefile.go156
-rw-r--r--transports/transports.go91
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
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
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
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
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
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
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
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
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
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
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
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
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))
+}