diff --git a/README.md b/README.md index c97588a..3ee9c0c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ ## obfs4 - The obfourscator #### Yawning Angel (yawning at torproject dot org) -### WARNING - -This is pre-alpha. Don't expect any security or wire protocol stability yet. -If you want to use something like this, you should currently probably be looking -at ScrambleSuit. - ### What? This is a look-like nothing obfuscation protocol that incorporates ideas and @@ -22,6 +16,9 @@ The notable differences between ScrambleSuit and obfs4: obfuscated via the Elligator 2 mapping. * The link layer encryption uses NaCl secret boxes (Poly1305/XSalsa20). +As an added bonus, obfs4proxy also supports acting as an obfs2/3 client and +bridge to ease the transition to the new protocol. + ### Why not extend ScrambleSuit? It's my protocol and I'll obfuscate if I want to. @@ -43,20 +40,6 @@ listed for clarity. * SipHash-2-4 (https://github.com/dchest/siphash) * goptlib (https://git.torproject.org/pluggable-transports/goptlib.git) -### TODO - - * Code cleanups. - * Write more unit tests. - * Optimize further. - -### WON'T DO - - * I do not care that much about standalone mode. Patches *MAY* be accepted, - especially if they are clean and are useful to Tor users. - * Yes, I use a bunch of code from the borg^w^wGoogle. If that bothers you - feel free to write your own implementation. - * I do not care about older versions of the go runtime. - ### Thanks * David Fifield for goptlib. diff --git a/csrand/csrand.go b/common/csrand/csrand.go similarity index 86% rename from csrand/csrand.go rename to common/csrand/csrand.go index b059ed0..45849d3 100644 --- a/csrand/csrand.go +++ b/common/csrand/csrand.go @@ -29,7 +29,7 @@ // with some utility functions for common random number/byte related tasks. // // Not all of the convinience routines are replicated, only those that are -// useful for obfs4. The CsRand variable provides access to the full math/rand +// immediately useful. The Rand variable provides access to the full math/rand // API. package csrand @@ -44,8 +44,8 @@ import ( var ( csRandSourceInstance csRandSource - // CsRand is a math/rand instance backed by crypto/rand CSPRNG. - CsRand = rand.New(csRandSourceInstance) + // Rand is a math/rand instance backed by crypto/rand CSPRNG. + Rand = rand.New(csRandSourceInstance) ) type csRandSource struct { @@ -54,8 +54,7 @@ type csRandSource struct { func (r csRandSource) Int63() int64 { var src [8]byte - err := Bytes(src[:]) - if err != nil { + if err := Bytes(src[:]); err != nil { panic(err) } val := binary.BigEndian.Uint64(src[:]) @@ -70,12 +69,12 @@ func (r csRandSource) Seed(seed int64) { // Intn returns, as a int, a pseudo random number in [0, n). func Intn(n int) int { - return CsRand.Intn(n) + return Rand.Intn(n) } // Float64 returns, as a float64, a pesudo random number in [0.0,1.0). func Float64() float64 { - return CsRand.Float64() + return Rand.Float64() } // IntRange returns a uniformly distributed int [min, max]. @@ -85,18 +84,18 @@ func IntRange(min, max int) int { } r := (max + 1) - min - ret := CsRand.Intn(r) + ret := Rand.Intn(r) return ret + min } // Bytes fills the slice with random data. func Bytes(buf []byte) error { - _, err := io.ReadFull(cryptRand.Reader, buf) - if err != nil { + if _, err := io.ReadFull(cryptRand.Reader, buf); err != nil { return err } return nil } -/* vim :set ts=4 sw=4 sts=4 noet : */ +// Reader is a alias of rand.Reader. +var Reader = cryptRand.Reader diff --git a/drbg/hash_drbg.go b/common/drbg/hash_drbg.go similarity index 91% rename from drbg/hash_drbg.go rename to common/drbg/hash_drbg.go index c94902a..5329828 100644 --- a/drbg/hash_drbg.go +++ b/common/drbg/hash_drbg.go @@ -37,7 +37,7 @@ import ( "github.com/dchest/siphash" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" ) // Size is the length of the HashDrbg output. @@ -63,8 +63,7 @@ func (seed *Seed) Base64() string { // NewSeed returns a Seed initialized with the runtime CSPRNG. func NewSeed() (seed *Seed, err error) { seed = new(Seed) - err = csrand.Bytes(seed.Bytes()[:]) - if err != nil { + if err = csrand.Bytes(seed.Bytes()[:]); err != nil { return nil, err } @@ -88,8 +87,7 @@ func SeedFromBytes(src []byte) (seed *Seed, err error) { // SeedLength as appropriate. func SeedFromBase64(encoded string) (seed *Seed, err error) { var raw []byte - raw, err = base64.StdEncoding.DecodeString(encoded) - if err != nil { + if raw, err = base64.StdEncoding.DecodeString(encoded); err != nil { return nil, err } @@ -112,12 +110,18 @@ type HashDrbg struct { // NewHashDrbg makes a HashDrbg instance based off an optional seed. The seed // is truncated to SeedLength. -func NewHashDrbg(seed *Seed) *HashDrbg { +func NewHashDrbg(seed *Seed) (*HashDrbg, error) { drbg := new(HashDrbg) + if seed == nil { + var err error + if seed, err = NewSeed(); err != nil { + return nil, err + } + } drbg.sip = siphash.New(seed.Bytes()[:16]) copy(drbg.ofb[:], seed.Bytes()[16:]) - return drbg + return drbg, nil } // Int63 returns a uniformly distributed random integer [0, 1 << 63). @@ -143,5 +147,3 @@ func (drbg *HashDrbg) NextBlock() []byte { copy(ret, drbg.ofb[:]) return ret } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/ntor/ntor.go b/common/ntor/ntor.go similarity index 98% rename from ntor/ntor.go rename to common/ntor/ntor.go index b178454..37cfe88 100644 --- a/ntor/ntor.go +++ b/common/ntor/ntor.go @@ -25,7 +25,6 @@ * POSSIBILITY OF SUCH DAMAGE. */ -// // Package ntor implements the Tor Project's ntor handshake as defined in // proposal 216 "Improved circuit-creation key exchange". It also supports // using Elligator to transform the Curve25519 public keys sent over the wire @@ -33,7 +32,6 @@ // // Before using this package, it is strongly recommended that the specification // is read and understood. -// package ntor import ( @@ -50,7 +48,7 @@ import ( "github.com/agl/ed25519/extra25519" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" ) const ( @@ -267,8 +265,7 @@ func NewKeypair(elligator bool) (*Keypair, error) { // Generate a Curve25519 private key. Like everyone who does this, // run the CSPRNG output through SHA256 for extra tinfoil hattery. priv := keypair.private.Bytes()[:] - err := csrand.Bytes(priv) - if err != nil { + if err := csrand.Bytes(priv); err != nil { return nil, err } digest := sha256.Sum256(priv) @@ -433,5 +430,3 @@ func Kdf(keySeed []byte, okmLen int) []byte { return okm } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/ntor/ntor_test.go b/common/ntor/ntor_test.go similarity index 99% rename from ntor/ntor_test.go rename to common/ntor/ntor_test.go index 9d7c687..c92c04e 100644 --- a/ntor/ntor_test.go +++ b/common/ntor/ntor_test.go @@ -178,5 +178,3 @@ func BenchmarkHandshake(b *testing.B) { } } } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/weighted_dist.go b/common/probdist/weighted_dist.go similarity index 77% rename from weighted_dist.go rename to common/probdist/weighted_dist.go index 4f1f2a5..2386bbe 100644 --- a/weighted_dist.go +++ b/common/probdist/weighted_dist.go @@ -25,15 +25,18 @@ * POSSIBILITY OF SUCH DAMAGE. */ -package obfs4 +// Package probdist implements a weighted probability distribution suitable for +// protocol parameterization. To allow for easy reproduction of a given +// distribution, the drbg package is used as the random number source. +package probdist import ( "container/list" "fmt" "math/rand" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" ) const ( @@ -41,10 +44,11 @@ const ( maxValues = 100 ) -// wDist is a weighted distribution. -type wDist struct { +// WeightedDist is a weighted distribution. +type WeightedDist struct { minValue int maxValue int + biased bool values []int weights []float64 @@ -52,23 +56,25 @@ type wDist struct { prob []float64 } -// newWDist creates a weighted distribution of values ranging from min to max -// based on a HashDrbg initialized with seed. -func newWDist(seed *drbg.Seed, min, max int) (w *wDist) { - w = &wDist{minValue: min, maxValue: max} +// New creates a weighted distribution of values ranging from min to max +// based on a HashDrbg initialized with seed. Optionally, bias the weight +// generation to match the ScrambleSuit non-uniform distribution from +// obfsproxy. +func New(seed *drbg.Seed, min, max int, biased bool) (w *WeightedDist) { + w = &WeightedDist{minValue: min, maxValue: max, biased: biased} if max <= min { panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max)) } - w.reset(seed) + w.Reset(seed) return } // genValues creates a slice containing a random number of random values // that when scaled by adding minValue will fall into [min, max]. -func (w *wDist) genValues(rng *rand.Rand) { +func (w *WeightedDist) genValues(rng *rand.Rand) { nValues := (w.maxValue + 1) - w.minValue values := rng.Perm(nValues) if nValues < minValues { @@ -83,11 +89,11 @@ func (w *wDist) genValues(rng *rand.Rand) { // genBiasedWeights generates a non-uniform weight list, similar to the // ScrambleSuit prob_dist module. -func (w *wDist) genBiasedWeights(rng *rand.Rand) { +func (w *WeightedDist) genBiasedWeights(rng *rand.Rand) { w.weights = make([]float64, len(w.values)) culmProb := 0.0 - for i := range w.values { + for i := range w.weights { p := (1.0 - culmProb) * rng.Float64() w.weights[i] = p culmProb += p @@ -95,7 +101,7 @@ func (w *wDist) genBiasedWeights(rng *rand.Rand) { } // genUniformWeights generates a uniform weight list. -func (w *wDist) genUniformWeights(rng *rand.Rand) { +func (w *WeightedDist) genUniformWeights(rng *rand.Rand) { w.weights = make([]float64, len(w.values)) for i := range w.weights { w.weights[i] = rng.Float64() @@ -104,7 +110,7 @@ func (w *wDist) genUniformWeights(rng *rand.Rand) { // genTables calculates the alias and prob tables used for Vose's Alias method. // Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/ -func (w *wDist) genTables() { +func (w *WeightedDist) genTables() { n := len(w.weights) var sum float64 for _, weight := range w.weights { @@ -179,20 +185,24 @@ func (w *wDist) genTables() { w.alias = alias } -// reset generates a new distribution with the same min/max based on a new seed. -func (w *wDist) reset(seed *drbg.Seed) { +// Reset generates a new distribution with the same min/max based on a new +// seed. +func (w *WeightedDist) Reset(seed *drbg.Seed) { // Initialize the deterministic random number generator. - drbg := drbg.NewHashDrbg(seed) + drbg, _ := drbg.NewHashDrbg(seed) rng := rand.New(drbg) w.genValues(rng) - //w.genBiasedWeights(rng) - w.genUniformWeights(rng) + if w.biased { + w.genBiasedWeights(rng) + } else { + w.genUniformWeights(rng) + } w.genTables() } -// sample generates a random value according to the distribution. -func (w *wDist) sample() int { +// Sample generates a random value according to the distribution. +func (w *WeightedDist) Sample() int { var idx int // Generate a fair die roll from an $n$-sided die; call the side $i$. @@ -208,5 +218,3 @@ func (w *wDist) sample() int { return w.minValue + w.values[idx] } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/weighted_dist_test.go b/common/probdist/weighted_dist_test.go similarity index 93% rename from weighted_dist_test.go rename to common/probdist/weighted_dist_test.go index 16b93c4..b705add 100644 --- a/weighted_dist_test.go +++ b/common/probdist/weighted_dist_test.go @@ -25,13 +25,13 @@ * POSSIBILITY OF SUCH DAMAGE. */ -package obfs4 +package probdist import ( "fmt" "testing" - "git.torproject.org/pluggable-transports/obfs4.git/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" ) const debug = false @@ -46,7 +46,7 @@ func TestWeightedDist(t *testing.T) { hist := make([]int, 1000) - w := newWDist(seed, 0, 999) + w := New(seed, 0, 999, true) if debug { // Dump a string representation of the probability table. fmt.Println("Table:") @@ -64,7 +64,7 @@ func TestWeightedDist(t *testing.T) { } for i := 0; i < nrTrials; i++ { - value := w.sample() + value := w.Sample() hist[value]++ } @@ -78,5 +78,3 @@ func TestWeightedDist(t *testing.T) { } } } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/common/replayfilter/replay_filter.go b/common/replayfilter/replay_filter.go new file mode 100644 index 0000000..95cc5d6 --- /dev/null +++ b/common/replayfilter/replay_filter.go @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2014, Yawning Angel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Package replayfilter implements a generic replay detection filter with a +// caller specifiable time-to-live. It only detects if a given byte sequence +// has been seen before based on the SipHash-2-4 digest of the sequence. +// Collisions are treated as positive matches, though the probability of this +// happening is negligible. +package replayfilter + +import ( + "container/list" + "encoding/binary" + "sync" + "time" + + "github.com/dchest/siphash" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" +) + +// maxFilterSize is the maximum capacity of a replay filter. This value is +// more as a safeguard to prevent runaway filter growth, and is sized to be +// serveral orders of magnitude greater than the number of connections a busy +// bridge sees in one day, so in practice should never be reached. +const maxFilterSize = 100 * 1024 + +type entry struct { + digest uint64 + firstSeen time.Time + element *list.Element +} + +// ReplayFilter is a simple filter designed only to detect if a given byte +// sequence has been seen before. +type ReplayFilter struct { + sync.Mutex + + filter map[uint64]*entry + fifo *list.List + + key [2]uint64 + ttl time.Duration +} + +// New creates a new ReplayFilter instance. +func New(ttl time.Duration) (filter *ReplayFilter, err error) { + // Initialize the SipHash-2-4 instance with a random key. + var key [16]byte + if err = csrand.Bytes(key[:]); err != nil { + return + } + + filter = new(ReplayFilter) + filter.filter = make(map[uint64]*entry) + filter.fifo = list.New() + filter.key[0] = binary.BigEndian.Uint64(key[0:8]) + filter.key[1] = binary.BigEndian.Uint64(key[8:16]) + filter.ttl = ttl + + return +} + +// TestAndSet queries the filter for a given byte sequence, inserts the +// sequence, and returns if it was present before the insertion operation. +func (f *ReplayFilter) TestAndSet(now time.Time, buf []byte) bool { + digest := siphash.Hash(f.key[0], f.key[1], buf) + + f.Lock() + defer f.Unlock() + + f.compactFilter(now) + + if e := f.filter[digest]; e != nil { + // Hit. Just return. + return true + } + + // Miss. Add a new entry. + e := new(entry) + e.digest = digest + e.firstSeen = now + e.element = f.fifo.PushBack(e) + f.filter[digest] = e + + return false +} + +func (f *ReplayFilter) compactFilter(now time.Time) { + e := f.fifo.Front() + for e != nil { + ent, _ := e.Value.(*entry) + + // If the filter is not full, only purge entries that exceed the TTL, + // otherwise purge at least one entry, then revert to TTL based + // compaction. + if f.fifo.Len() < maxFilterSize && f.ttl > 0 { + deltaT := now.Sub(ent.firstSeen) + if deltaT < 0 { + // Aeeeeeee, the system time jumped backwards, potentially by + // a lot. This will eventually self-correct, but "eventually" + // could be a long time. As much as this sucks, jettison the + // entire filter. + f.reset() + return + } else if deltaT < f.ttl { + return + } + } + + // Remove the eldest entry. + eNext := e.Next() + delete(f.filter, ent.digest) + f.fifo.Remove(ent.element) + ent.element = nil + e = eNext + } +} + +func (f *ReplayFilter) reset() { + f.filter = make(map[uint64]*entry) + f.fifo = list.New() +} diff --git a/replay_filter_test.go b/common/replayfilter/replay_filter_test.go similarity index 87% rename from replay_filter_test.go rename to common/replayfilter/replay_filter_test.go index 09337c0..884e4fb 100644 --- a/replay_filter_test.go +++ b/common/replayfilter/replay_filter_test.go @@ -25,55 +25,58 @@ * POSSIBILITY OF SUCH DAMAGE. */ -package obfs4 +package replayfilter import ( "testing" + "time" ) func TestReplayFilter(t *testing.T) { - f, err := newReplayFilter() + ttl := 10 * time.Second + + f, err := New(ttl) if err != nil { t.Fatal("newReplayFilter failed:", err) } buf := []byte("This is a test of the Emergency Broadcast System.") - var now int64 = 3600 + now := time.Now() // testAndSet into empty filter, returns false (not present). - set := f.testAndSet(now, buf) + set := f.TestAndSet(now, buf) if set { - t.Fatal("testAndSet empty filter returned true") + t.Fatal("TestAndSet empty filter returned true") } // testAndSet into filter containing entry, should return true(present). - set = f.testAndSet(now, buf) + set = f.TestAndSet(now, buf) if !set { t.Fatal("testAndSet populated filter (replayed) returned false") } buf2 := []byte("This concludes this test of the Emergency Broadcast System.") - now += 3600 * 2 + now = now.Add(ttl) // testAndSet with time advanced. - set = f.testAndSet(now, buf2) + set = f.TestAndSet(now, buf2) if set { t.Fatal("testAndSet populated filter, 2nd entry returned true") } - set = f.testAndSet(now, buf2) + set = f.TestAndSet(now, buf2) if !set { t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false") } // Ensure that the first entry has been removed by compact. - set = f.testAndSet(now, buf) + set = f.TestAndSet(now, buf) if set { t.Fatal("testAndSet populated filter, compact check returned true") } // Ensure that the filter gets reaped if the clock jumps backwards. - now = 0 - set = f.testAndSet(now, buf) + now = time.Time{} + set = f.TestAndSet(now, buf) if set { t.Fatal("testAndSet populated filter, backward time jump returned true") } @@ -85,7 +88,7 @@ func TestReplayFilter(t *testing.T) { } // Ensure that the entry is properly added after reaping. - set = f.testAndSet(now, buf) + set = f.TestAndSet(now, buf) if !set { t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false") } diff --git a/common/uniformdh/uniformdh.go b/common/uniformdh/uniformdh.go new file mode 100644 index 0000000..ab94a2e --- /dev/null +++ b/common/uniformdh/uniformdh.go @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 + * 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 - * 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 public-key= node-id= -// ClientTransportPlugin obfs4 exec obfs4proxy -// -// Server usage (in torrc): -// BridgeRelay 1 -// ORPort 9001 -// ExtORPort 6669 -// ServerTransportPlugin obfs4 exec obfs4proxy -// ServerTransportOptions obfs4 private-key= node-id= 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 %s node-id=%s public-key=%s\n", - id, parsedID.Base64(), keypair.Public().Base64()) - } else { - fmt.Printf(" Bridge obfs4 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 - * 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 - * 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 + * 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 + * 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 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Package obfs3 provides an implementation of the Tor Project's obfs3 +// obfuscation protocol. +package obfs3 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "errors" + "io" + "net" + "time" + + "git.torproject.org/pluggable-transports/goptlib.git" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" +) + +const ( + transportName = "obfs3" + + clientHandshakeTimeout = time.Duration(30) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + + initiatorKdfString = "Initiator obfuscated data" + responderKdfString = "Responder obfuscated data" + initiatorMagicString = "Initiator magic" + responderMagicString = "Responder magic" + maxPadding = 8194 + keyLen = 16 +) + +// Transport is the obfs3 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs3 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs3ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs3ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs3ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + sf := &obfs3ServerFactory{transport: t} + return sf, nil +} + +type obfs3ClientFactory struct { + transport base.Transport +} + +func (cf *obfs3ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs3ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + return nil, nil +} + +func (cf *obfs3ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + return newObfs3ClientConn(conn) +} + +type obfs3ServerFactory struct { + transport base.Transport +} + +func (sf *obfs3ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs3ServerFactory) Args() *pt.Args { + return nil +} + +func (sf *obfs3ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + return newObfs3ServerConn(conn) +} + +type obfs3Conn struct { + net.Conn + + isInitiator bool + rxMagic []byte + txMagic []byte + rxBuf *bytes.Buffer + + rx *cipher.StreamReader + tx *cipher.StreamWriter +} + +func newObfs3ClientConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a client connection, and start the handshake timeout. + c = &obfs3Conn{conn, true, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(clientHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func newObfs3ServerConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a server connection, and start the handshake timeout. + c = &obfs3Conn{conn, false, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(serverHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs3Conn) handshake() (err error) { + // The party who opens the connection is the 'initiator'; the one who + // accepts it is the 'responder'. Each begins by generating a + // UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2]. + // Both parties then send: + // + // PUB_KEY | WR(PADLEN) + var privateKey *uniformdh.PrivateKey + if privateKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil { + return + } + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, uniformdh.Size+padLen) + var publicKey []byte + if publicKey, err = privateKey.PublicKey.Bytes(); err != nil { + return + } + copy(blob[0:], publicKey) + if err = csrand.Bytes(blob[uniformdh.Size:]); err != nil { + return + } + if _, err = conn.Conn.Write(blob); err != nil { + return + } + + // Read the public key from the peer. + rawPeerPublicKey := make([]byte, uniformdh.Size) + if _, err = io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil { + return + } + var peerPublicKey uniformdh.PublicKey + if err = peerPublicKey.SetBytes(rawPeerPublicKey); err != nil { + return + } + + // After retrieving the public key of the other end, each party + // completes the DH key exchange and generates a shared-secret for the + // session (named SHARED_SECRET). + var sharedSecret []byte + if sharedSecret, err = uniformdh.Handshake(privateKey, &peerPublicKey); err != nil { + return + } + if err = conn.kdf(sharedSecret); err != nil { + return + } + + return +} + +func (conn *obfs3Conn) kdf(sharedSecret []byte) (err error) { + // Using that shared-secret each party derives its encryption keys as + // follows: + // + // INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data") + // RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data") + // INIT_KEY = INIT_SECRET[:KEYLEN] + // INIT_COUNTER = INIT_SECRET[KEYLEN:] + // RESP_KEY = RESP_SECRET[:KEYLEN] + // RESP_COUNTER = RESP_SECRET[KEYLEN:] + initHmac := hmac.New(sha256.New, sharedSecret) + initHmac.Write([]byte(initiatorKdfString)) + initSecret := initHmac.Sum(nil) + initHmac.Reset() + initHmac.Write([]byte(initiatorMagicString)) + initMagic := initHmac.Sum(nil) + + respHmac := hmac.New(sha256.New, sharedSecret) + respHmac.Write([]byte(responderKdfString)) + respSecret := respHmac.Sum(nil) + respHmac.Reset() + respHmac.Write([]byte(responderMagicString)) + respMagic := respHmac.Sum(nil) + + // The INIT_KEY value keys a block cipher (in CTR mode) used to + // encrypt values from initiator to responder thereafter. The counter + // mode's initial counter value is INIT_COUNTER. The RESP_KEY value + // keys a block cipher (in CTR mode) used to encrypt values from + // responder to initiator thereafter. That counter mode's initial + // counter value is RESP_COUNTER. + // + // Note: To have this be the last place where the shared secret is used, + // also generate the magic value to send/scan for here. + var initBlock cipher.Block + if initBlock, err = aes.NewCipher(initSecret[:keyLen]); err != nil { + return err + } + initStream := cipher.NewCTR(initBlock, initSecret[keyLen:]) + + var respBlock cipher.Block + if respBlock, err = aes.NewCipher(respSecret[:keyLen]); err != nil { + return err + } + respStream := cipher.NewCTR(respBlock, respSecret[keyLen:]) + + if conn.isInitiator { + conn.tx = &cipher.StreamWriter{initStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{respStream, conn.rxBuf} + conn.txMagic = initMagic + conn.rxMagic = respMagic + } else { + conn.tx = &cipher.StreamWriter{respStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{initStream, conn.rxBuf} + conn.txMagic = respMagic + conn.rxMagic = initMagic + } + + return +} + +func (conn *obfs3Conn) findPeerMagic() error { + var hsBuf [maxPadding + sha256.Size]byte + for { + n, err := conn.Conn.Read(hsBuf[:]) + if err != nil { + // Yes, Read can return partial data and an error, but continuing + // past that is nonsensical. + return err + } + conn.rxBuf.Write(hsBuf[:n]) + + pos := bytes.Index(conn.rxBuf.Bytes(), conn.rxMagic) + if pos == -1 { + if conn.rxBuf.Len() >= maxPadding+sha256.Size { + return errors.New("failed to find peer magic value") + } + continue + } else if pos > maxPadding { + return errors.New("peer sent too much pre-magic-padding") + } + + // Discard the padding/MAC. + pos += len(conn.rxMagic) + _ = conn.rxBuf.Next(pos) + + return nil + } +} + +func (conn *obfs3Conn) Read(b []byte) (n int, err error) { + // If this is the first time we read data post handshake, scan for the + // magic value. + if conn.rxMagic != nil { + if err = conn.findPeerMagic(); err != nil { + conn.Close() + return + } + conn.rxMagic = nil + } + + // If the handshake receive buffer is still present... + if conn.rxBuf != nil { + // And it is empty... + if conn.rxBuf.Len() == 0 { + // There is no more trailing data left from the handshake process, + // so rewire the cipher.StreamReader to pull data from the network + // instead of the temporary receive buffer. + conn.rx.R = conn.Conn + conn.rxBuf = nil + } + } + + return conn.rx.Read(b) +} + +func (conn *obfs3Conn) Write(b []byte) (n int, err error) { + // If this is the first time we write data post handshake, send the + // padding/magic value. + if conn.txMagic != nil { + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, padLen+len(conn.txMagic)) + if err = csrand.Bytes(blob[:padLen]); err != nil { + conn.Close() + return + } + copy(blob[padLen:], conn.txMagic) + if _, err = conn.Conn.Write(blob); err != nil { + conn.Close() + return + } + conn.txMagic = nil + } + + return conn.tx.Write(b) +} + + +var _ base.ClientFactory = (*obfs3ClientFactory)(nil) +var _ base.ServerFactory = (*obfs3ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs3Conn)(nil) diff --git a/framing/framing.go b/transports/obfs4/framing/framing.go similarity index 97% rename from framing/framing.go rename to transports/obfs4/framing/framing.go index 725a762..04e788f 100644 --- a/framing/framing.go +++ b/transports/obfs4/framing/framing.go @@ -69,8 +69,8 @@ import ( "code.google.com/p/go.crypto/nacl/secretbox" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" ) const ( @@ -168,7 +168,7 @@ func NewEncoder(key []byte) *Encoder { if err != nil { panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err)) } - encoder.drbg = drbg.NewHashDrbg(seed) + encoder.drbg, _ = drbg.NewHashDrbg(seed) return encoder } @@ -231,7 +231,7 @@ func NewDecoder(key []byte) *Decoder { if err != nil { panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err)) } - decoder.drbg = drbg.NewHashDrbg(seed) + decoder.drbg, _ = drbg.NewHashDrbg(seed) return decoder } @@ -306,5 +306,3 @@ func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) { return len(out), nil } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/framing/framing_test.go b/transports/obfs4/framing/framing_test.go similarity index 99% rename from framing/framing_test.go rename to transports/obfs4/framing/framing_test.go index 7df0e28..03e0d3b 100644 --- a/framing/framing_test.go +++ b/transports/obfs4/framing/framing_test.go @@ -167,5 +167,3 @@ func BenchmarkEncoder_Encode(b *testing.B) { } } } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/handshake_ntor.go b/transports/obfs4/handshake_ntor.go similarity index 96% rename from handshake_ntor.go rename to transports/obfs4/handshake_ntor.go index 8192494..8dcf0c8 100644 --- a/handshake_ntor.go +++ b/transports/obfs4/handshake_ntor.go @@ -38,9 +38,10 @@ import ( "strconv" "time" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/framing" - "git.torproject.org/pluggable-transports/obfs4.git/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" ) const ( @@ -247,7 +248,7 @@ func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair, sessi return hs } -func (hs *serverHandshake) parseClientHandshake(filter *replayFilter, resp []byte) ([]byte, error) { +func (hs *serverHandshake) parseClientHandshake(filter *replayfilter.ReplayFilter, resp []byte) ([]byte, error) { // No point in examining the data unless the miminum plausible response has // been received. if clientMinHandshakeLength > len(resp) { @@ -287,7 +288,7 @@ func (hs *serverHandshake) parseClientHandshake(filter *replayFilter, resp []byt macRx := resp[pos+markLength : pos+markLength+macLength] if hmac.Equal(macCmp, macRx) { // Ensure that this handshake has not been seen previously. - if filter.testAndSet(time.Now().Unix(), macRx) { + if filter.TestAndSet(time.Now(), macRx) { // The client either happened to generate exactly the same // session key and padding, or someone is replaying a previous // handshake. In either case, fuck them. @@ -423,5 +424,3 @@ func makePad(padLen int) ([]byte, error) { return pad, err } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go similarity index 97% rename from handshake_ntor_test.go rename to transports/obfs4/handshake_ntor_test.go index 94d2a7f..fa03420 100644 --- a/handshake_ntor_test.go +++ b/transports/obfs4/handshake_ntor_test.go @@ -31,14 +31,15 @@ import ( "bytes" "testing" - "git.torproject.org/pluggable-transports/obfs4.git/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" ) func TestHandshakeNtor(t *testing.T) { // Generate the server node id and id keypair. nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) idKeypair, _ := ntor.NewKeypair(false) - serverFilter, _ := newReplayFilter() + serverFilter, _ := replayfilter.New(replayTTL) // Test client handshake padding. for l := clientMinPadLength; l <= clientMaxPadLength; l++ { diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go new file mode 100644 index 0000000..7af7224 --- /dev/null +++ b/transports/obfs4/obfs4.go @@ -0,0 +1,579 @@ +/* + * Copyright (c) 2014, Yawning Angel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Package obfs4 provides an implementation of the Tor Project's obfs4 +// obfuscation protocol. +package obfs4 + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/rand" + "net" + "syscall" + "time" + + "git.torproject.org/pluggable-transports/goptlib.git" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/probdist" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" +) + +const ( + transportName = "obfs4" + + nodeIDArg = "node-id" + publicKeyArg = "public-key" + privateKeyArg = "private-key" + seedArg = "drbg-seed" + + seedLength = 32 + headerLength = framing.FrameOverhead + packetOverhead + clientHandshakeTimeout = time.Duration(60) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + replayTTL = time.Duration(3) * time.Hour + + // Use a ScrambleSuit style biased probability table. + biasedDist = false + + // Use IAT obfuscation. + iatObfuscation = false + + // Maximum IAT delay (100 usec increments). + maxIATDelay = 100 + + maxCloseDelayBytes = maxHandshakeLength + maxCloseDelay = 60 +) + +type obfs4ClientArgs struct { + nodeID *ntor.NodeID + publicKey *ntor.PublicKey + sessionKey *ntor.Keypair +} + +// Transport is the obfs4 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs4 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs4ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs4ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs4ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + var err error + + var st *obfs4ServerState + if st, err = serverStateFromArgs(stateDir, args); err != nil { + return nil, err + } + + var iatSeed *drbg.Seed + if iatObfuscation { + iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:]) + iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]) + if err != nil { + return nil, err + } + } + + // Store the arguments that should appear in our descriptor for the clients. + ptArgs := pt.Args{} + ptArgs.Add(nodeIDArg, st.nodeID.Base64()) + ptArgs.Add(publicKeyArg, st.identityKey.Public().Base64()) + + // Initialize the replay filter. + filter, err := replayfilter.New(replayTTL) + if err != nil { + return nil, err + } + + // Initialize the close thresholds for failed connections. + drbg, err := drbg.NewHashDrbg(st.drbgSeed) + if err != nil { + return nil, err + } + rng := rand.New(drbg) + + sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)} + return sf, nil +} + +type obfs4ClientFactory struct { + transport base.Transport +} + +func (cf *obfs4ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + var err error + + // Handle the arguments. + nodeIDStr, ok := args.Get(nodeIDArg) + if !ok { + return nil, fmt.Errorf("missing argument '%s'", nodeIDArg) + } + var nodeID *ntor.NodeID + if nodeID, err = ntor.NodeIDFromBase64(nodeIDStr); err != nil { + return nil, err + } + + publicKeyStr, ok := args.Get(publicKeyArg) + if !ok { + return nil, fmt.Errorf("missing argument '%s'", publicKeyArg) + } + var publicKey *ntor.PublicKey + if publicKey, err = ntor.PublicKeyFromBase64(publicKeyStr); err != nil { + return nil, err + } + + // Generate the session key pair before connectiong to hide the Elligator2 + // rejection sampling from network observers. + sessionKey, err := ntor.NewKeypair(true) + if err != nil { + return nil, err + } + + return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil +} + +func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + ca, ok := args.(*obfs4ClientArgs) + if !ok { + return nil, fmt.Errorf("invalid argument type for args") + } + + return newObfs4ClientConn(conn, ca) +} + +type obfs4ServerFactory struct { + transport base.Transport + args *pt.Args + + nodeID *ntor.NodeID + identityKey *ntor.Keypair + lenSeed *drbg.Seed + iatSeed *drbg.Seed + replayFilter *replayfilter.ReplayFilter + + closeDelayBytes int + closeDelay int +} + +func (sf *obfs4ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs4ServerFactory) Args() *pt.Args { + return sf.args +} + +func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + // Not much point in having a separate newObfs4ServerConn routine when + // wrapping requires using values from the factory instance. + + // Generate the session keypair *before* consuming data from the peer, to + // attempt to mask the rejection sampling due to use of Elligator2. This + // might be futile, but the timing differential isn't very large on modern + // hardware, and there are far easier statistical attacks that can be + // mounted as a distinguisher. + sessionKey, err := ntor.NewKeypair(true) + if err != nil { + return nil, err + } + + lenDist := probdist.New(sf.lenSeed, 0, framing.MaximumSegmentLength, biasedDist) + var iatDist *probdist.WeightedDist + if sf.iatSeed != nil { + iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist) + } + + c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + + startTime := time.Now() + + if err = c.serverHandshake(sf, sessionKey); err != nil { + c.closeAfterDelay(sf, startTime) + return nil, err + } + + return c, nil +} + +type obfs4Conn struct { + net.Conn + + isServer bool + + lenDist *probdist.WeightedDist + iatDist *probdist.WeightedDist + + receiveBuffer *bytes.Buffer + receiveDecodedBuffer *bytes.Buffer + + encoder *framing.Encoder + decoder *framing.Decoder +} + +func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err error) { + // Generate the initial protocol polymorphism distribution(s). + var seed *drbg.Seed + if seed, err = drbg.NewSeed(); err != nil { + return + } + lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist) + var iatDist *probdist.WeightedDist + if iatObfuscation { + var iatSeed *drbg.Seed + iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) + if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil { + return + } + iatDist = probdist.New(iatSeed, 0, maxIATDelay, biasedDist) + } + + // Allocate the client structure. + c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + + // Start the handshake timeout. + deadline := time.Now().Add(clientHandshakeTimeout) + if err = conn.SetDeadline(deadline); err != nil { + return nil, err + } + + if err = c.clientHandshake(args.nodeID, args.publicKey, args.sessionKey); err != nil { + return nil, err + } + + // Stop the handshake timeout. + if err = conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs4Conn) clientHandshake(nodeID *ntor.NodeID, peerIdentityKey *ntor.PublicKey, sessionKey *ntor.Keypair) error { + if conn.isServer { + return fmt.Errorf("clientHandshake called on server connection") + } + + // Generate and send the client handshake. + hs := newClientHandshake(nodeID, peerIdentityKey, sessionKey) + blob, err := hs.generateHandshake() + if err != nil { + return err + } + if _, err = conn.Conn.Write(blob); err != nil { + return err + } + + // Consume the server handshake. + var hsBuf [maxHandshakeLength]byte + for { + var n int + if n, err = conn.Conn.Read(hsBuf[:]); err != nil { + // The Read() could have returned data and an error, but there is + // no point in continuing on an EOF or whatever. + return err + } + conn.receiveBuffer.Write(hsBuf[:n]) + + var seed []byte + n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return err + } + _ = conn.receiveBuffer.Next(n) + + // Use the derived key material to intialize the link crypto. + okm := ntor.Kdf(seed, framing.KeyLength*2) + conn.encoder = framing.NewEncoder(okm[:framing.KeyLength]) + conn.decoder = framing.NewDecoder(okm[framing.KeyLength:]) + + return nil + } +} + +func (conn *obfs4Conn) serverHandshake(sf *obfs4ServerFactory, sessionKey *ntor.Keypair) (err error) { + if !conn.isServer { + return fmt.Errorf("serverHandshake called on client connection") + } + + // Generate the server handshake, and arm the base timeout. + hs := newServerHandshake(sf.nodeID, sf.identityKey, sessionKey) + if err = conn.Conn.SetDeadline(time.Now().Add(serverHandshakeTimeout)); err != nil { + return + } + + // Consume the client handshake. + var hsBuf [maxHandshakeLength]byte + for { + var n int + if n, err = conn.Conn.Read(hsBuf[:]); err != nil { + // The Read() could have returned data and an error, but there is + // no point in continuing on an EOF or whatever. + return + } + conn.receiveBuffer.Write(hsBuf[:n]) + + var seed []byte + seed, err = hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return + } + conn.receiveBuffer.Reset() + + if err = conn.Conn.SetDeadline(time.Time{}); err != nil { + return + } + + // Use the derived key material to intialize the link crypto. + okm := ntor.Kdf(seed, framing.KeyLength*2) + conn.encoder = framing.NewEncoder(okm[framing.KeyLength:]) + conn.decoder = framing.NewDecoder(okm[:framing.KeyLength]) + + break + } + + // Since the current and only implementation always sends a PRNG seed for + // the length obfuscation, this makes the amount of data received from the + // server inconsistent with the length sent from the client. + // + // Rebalance this by tweaking the client mimimum padding/server maximum + // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed + // as part of the server response). See inlineSeedFrameLength in + // handshake_ntor.go. + + // Generate/send the response. + var blob []byte + blob, err = hs.generateHandshake() + if err != nil { + return + } + var frameBuf bytes.Buffer + _, err = frameBuf.Write(blob) + if err != nil { + return + } + + // Send the PRNG seed as the first packet. + if err = conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil { + return + } + if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil { + return + } + + return +} + +func (conn *obfs4Conn) Read(b []byte) (n int, err error) { + // If there is no payload from the previous Read() calls, consume data off + // the network. Not all data received is guaranteed to be usable payload, + // so do this in a loop till data is present or an error occurs. + for conn.receiveDecodedBuffer.Len() == 0 { + err = conn.readPackets() + if err == framing.ErrAgain { + // Don't proagate this back up the call stack if we happen to break + // out of the loop. + err = nil + continue + } else if err != nil { + break + } + } + + // Even if err is set, attempt to do the read anyway so that all decoded + // data gets relayed before the connection is torn down. + if conn.receiveDecodedBuffer.Len() > 0 { + var berr error + n, berr = conn.receiveDecodedBuffer.Read(b) + if err == nil { + // Only propagate berr if there are not more important (fatal) + // errors from the network/crypto/packet processing. + err = berr + } + } + + return +} + +func (conn *obfs4Conn) Write(b []byte) (n int, err error) { + chopBuf := bytes.NewBuffer(b) + var payload [maxPacketPayloadLength]byte + var frameBuf bytes.Buffer + + // Chop the pending data into payload frames. + for chopBuf.Len() > 0 { + // Send maximum sized frames. + rdLen := 0 + rdLen, err = chopBuf.Read(payload[:]) + if err != nil { + return 0, err + } else if rdLen == 0 { + panic(fmt.Sprintf("BUG: Write(), chopping length was 0")) + } + n += rdLen + + err = conn.makePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0) + if err != nil { + return 0, err + } + } + + // Add the length obfuscation padding. In theory, this could be inlined + // with the last chopped packet for certain (most?) payload lenghts, but + // this is simpler. + + if err = conn.padBurst(&frameBuf); err != nil { + return 0, err + } + + // Write the pending data onto the network. Partial writes are fatal, + // because the frame encoder state is advanced, and the code doesn't keep + // frameBuf around. In theory, write timeouts and whatnot could be + // supported if this wasn't the case, but that complicates the code. + + if conn.iatDist != nil { + var iatFrame [framing.MaximumSegmentLength]byte + for frameBuf.Len() > 0 { + iatWrLen := 0 + iatWrLen, err = frameBuf.Read(iatFrame[:]) + if err != nil { + return 0, err + } else if iatWrLen == 0 { + panic(fmt.Sprintf("BUG: Write(), iat length was 0")) + } + + // Calculate the delay. The delay resolution is 100 usec, leading + // to a maximum delay of 10 msec. + iatDelta := time.Duration(conn.iatDist.Sample() * 100) + + // Write then sleep. + _, err = conn.Conn.Write(iatFrame[:iatWrLen]) + if err != nil { + return 0, err + } + time.Sleep(iatDelta * time.Microsecond) + } + } else { + _, err = conn.Conn.Write(frameBuf.Bytes()) + } + + return +} + +func (conn *obfs4Conn) SetDeadline(t time.Time) error { + return syscall.ENOTSUP +} + +func (conn *obfs4Conn) SetWriteDeadline(t time.Time) error { + return syscall.ENOTSUP +} + +func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Time) { + // I-it's not like I w-wanna handshake with you or anything. B-b-baka! + defer conn.Conn.Close() + + delay := time.Duration(sf.closeDelay)*time.Second + serverHandshakeTimeout + deadline := startTime.Add(delay) + if time.Now().After(deadline) { + return + } + + if err := conn.Conn.SetReadDeadline(deadline); err != nil { + return + } + + // Consume and discard data on this connection until either the specified + // interval passes or a certain size has been reached. + discarded := 0 + var buf [framing.MaximumSegmentLength]byte + for discarded < int(sf.closeDelayBytes) { + n, err := conn.Conn.Read(buf[:]) + if err != nil { + return + } + discarded += n + } +} + +func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) { + tailLen := burst.Len() % framing.MaximumSegmentLength + toPadTo := conn.lenDist.Sample() + + padLen := 0 + if toPadTo >= tailLen { + padLen = toPadTo - tailLen + } else { + padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo + } + + if padLen > headerLength { + err = conn.makePacket(burst, packetTypePayload, []byte{}, + uint16(padLen-headerLength)) + if err != nil { + return + } + } else if padLen > 0 { + err = conn.makePacket(burst, packetTypePayload, []byte{}, + maxPacketPayloadLength) + if err != nil { + return + } + err = conn.makePacket(burst, packetTypePayload, []byte{}, + uint16(padLen)) + if err != nil { + return + } + } + + return +} + +var _ base.ClientFactory = (*obfs4ClientFactory)(nil) +var _ base.ServerFactory = (*obfs4ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs4Conn)(nil) diff --git a/packet.go b/transports/obfs4/packet.go similarity index 73% rename from packet.go rename to transports/obfs4/packet.go index 58a5877..9865c82 100644 --- a/packet.go +++ b/transports/obfs4/packet.go @@ -32,17 +32,16 @@ import ( "encoding/binary" "fmt" "io" - "syscall" - "git.torproject.org/pluggable-transports/obfs4.git/drbg" - "git.torproject.org/pluggable-transports/obfs4.git/framing" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" ) const ( packetOverhead = 2 + 1 maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead maxPacketPaddingLength = maxPacketPayloadLength - seedPacketPayloadLength = SeedLength + seedPacketPayloadLength = seedLength consumeReadSize = framing.MaximumSegmentLength * 16 ) @@ -70,24 +69,14 @@ func (e InvalidPayloadLengthError) Error() string { var zeroPadBytes [maxPacketPaddingLength]byte -func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) { +func (conn *obfs4Conn) makePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) { var pkt [framing.MaximumFramePayloadLength]byte - if !c.CanReadWrite() { - return syscall.EINVAL - } - if len(data)+int(padLen) > maxPacketPayloadLength { panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d", len(data), padLen, maxPacketPayloadLength)) } - defer func() { - if err != nil { - c.setBroken() - } - }() - // Packets are: // uint8_t type packetTypePayload (0x00) // uint16_t length Length of the payload (Big Endian). @@ -105,7 +94,7 @@ func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLe // Encode the packet in an AEAD frame. var frame [framing.MaximumSegmentLength]byte frameLen := 0 - frameLen, err = c.encoder.Encode(frame[:], pkt[:pktLen]) + frameLen, err = conn.encoder.Encode(frame[:], pkt[:pktLen]) if err != nil { // All encoder errors are fatal. return @@ -122,19 +111,17 @@ func (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLe return } -func (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) { - if !c.CanReadWrite() { - return n, syscall.EINVAL - } - +func (conn *obfs4Conn) readPackets() (err error) { + // Attempt to read off the network. var buf [consumeReadSize]byte - rdLen, rdErr := c.conn.Read(buf[:]) - c.receiveBuffer.Write(buf[:rdLen]) + rdLen, rdErr := conn.Conn.Read(buf[:]) + conn.receiveBuffer.Write(buf[:rdLen]) + var decoded [framing.MaximumFramePayloadLength]byte - for c.receiveBuffer.Len() > 0 { + for conn.receiveBuffer.Len() > 0 { // Decrypt an AEAD frame. decLen := 0 - decLen, err = c.decoder.Decode(decoded[:], &c.receiveBuffer) + decLen, err = conn.decoder.Decode(decoded[:], conn.receiveBuffer) if err == framing.ErrAgain { break } else if err != nil { @@ -157,56 +144,36 @@ func (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) { switch pktType { case packetTypePayload: if payloadLen > 0 { - if w != nil { - // c.WriteTo() skips buffering in c.receiveDecodedBuffer - var wrLen int - wrLen, err = w.Write(payload) - n += wrLen - if err != nil { - break - } else if wrLen < int(payloadLen) { - err = io.ErrShortWrite - break - } - } else { - // c.Read() stashes decoded payload in receiveDecodedBuffer - c.receiveDecodedBuffer.Write(payload) - n += int(payloadLen) - } + conn.receiveDecodedBuffer.Write(payload) } case packetTypePrngSeed: // Only regenerate the distribution if we are the client. - if len(payload) == seedPacketPayloadLength && !c.isServer { + if len(payload) == seedPacketPayloadLength && !conn.isServer { var seed *drbg.Seed seed, err = drbg.SeedFromBytes(payload) if err != nil { break } - c.lenProbDist.reset(seed) - if c.iatProbDist != nil { + conn.lenDist.Reset(seed) + if conn.iatDist != nil { iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:]) if err != nil { break } - c.iatProbDist.reset(iatSeed) + conn.iatDist.Reset(iatSeed) } } default: - // Ignore unrecognised packet types. + // Ignore unknown packet types. } } - // Read errors and non-framing.ErrAgain errors are all fatal. - if (err != nil && err != framing.ErrAgain) || rdErr != nil { - // Propagate read errors correctly. - if err == nil && rdErr != nil { - err = rdErr - } - c.setBroken() + // Read errors (all fatal) take priority over various frame processing + // errors. + if rdErr != nil { + return rdErr } return } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go new file mode 100644 index 0000000..814a545 --- /dev/null +++ b/transports/obfs4/statefile.go @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 + * 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)) +}