commit 0066cfc3932c50323fa596981f18ef5b0e862742
Author: Yawning Angel <[email protected]>
Date:   Tue Feb 17 11:33:29 2015 +0000

    Add support for acting as a ScrambleSuit client.
    
    This allows obfs4proxy to be used as a ScrambleSuit client that is wire
    compatible with the obfs4proxy implementation, including session ticket
    support, and length obfuscation.
    
    The current implementation has the following limitations:
     * IAT obfuscation is not supported (and is disabled in all other
       ScrambleSuit implementations by default).
     * The length distribution and probabilites are different from those
       generated by obfsproxy and obfsclient due to a different DRBG.
     * Server support is missing and is unlikely to be implemented.
---
 ChangeLog                                      |    1 +
 README.md                                      |    3 +
 common/probdist/weighted_dist.go               |   25 ++
 doc/obfs4proxy.1                               |    5 +-
 transports/scramblesuit/base.go                |   88 ++++
 transports/scramblesuit/conn.go                |  521 ++++++++++++++++++++++++
 transports/scramblesuit/handshake_ticket.go    |  228 +++++++++++
 transports/scramblesuit/handshake_uniformdh.go |  174 ++++++++
 transports/scramblesuit/hkdf_expand.go         |   67 +++
 transports/transports.go                       |    2 +
 10 files changed, 1112 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index b2387e1..85fc60f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,7 @@
 Changes in version 0.0.4 - UNRELEASED
  - Improve the runtime performance of the obfs4 handshake tests.
  - Changed the go.crypto import path to the new location (golang.org/x/crypto).
+ - Added client only support for ScrambleSuit.
 
 Changes in version 0.0.3 - 2014-10-01
  - Change the obfs4 bridge line format to use a "cert" argument instead of the
diff --git a/README.md b/README.md
index e4d4595..31c9e2b 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,9 @@ ServerTransportPlugin obfs4 exec /usr/local/bin/obfs4proxy
    `ClientTransportPlugin` and `ServerTransportPlugin` lines in the torrc as
    appropriate.
 
+ * obfs4proxy can also act as a ScrambleSuit client.  Adjust the
+   `ClientTransportPlugin` line in the torrc as appropriate.
+
  * The autogenerated obfs4 bridge parameters are placed in
    `DataDir/pt_state/obfs4_state.json`.  To ease deployment, the client side
    bridge line is written to `DataDir/pt_state/obfs4_bridgeline.txt`.
diff --git a/common/probdist/weighted_dist.go b/common/probdist/weighted_dist.go
index 2386bbe..811a8a0 100644
--- a/common/probdist/weighted_dist.go
+++ b/common/probdist/weighted_dist.go
@@ -31,9 +31,11 @@
 package probdist
 
 import (
+       "bytes"
        "container/list"
        "fmt"
        "math/rand"
+       "sync"
 
        "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
        "git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
@@ -46,6 +48,8 @@ const (
 
 // WeightedDist is a weighted distribution.
 type WeightedDist struct {
+       sync.Mutex
+
        minValue int
        maxValue int
        biased   bool
@@ -192,6 +196,9 @@ func (w *WeightedDist) Reset(seed *drbg.Seed) {
        drbg, _ := drbg.NewHashDrbg(seed)
        rng := rand.New(drbg)
 
+       w.Lock()
+       defer w.Unlock()
+
        w.genValues(rng)
        if w.biased {
                w.genBiasedWeights(rng)
@@ -205,6 +212,9 @@ func (w *WeightedDist) Reset(seed *drbg.Seed) {
 func (w *WeightedDist) Sample() int {
        var idx int
 
+       w.Lock()
+       defer w.Unlock()
+
        // Generate a fair die roll from an $n$-sided die; call the side $i$.
        i := csrand.Intn(len(w.values))
        // Flip a biased coin that comes up heads with probability $Prob[i]$.
@@ -218,3 +228,18 @@ func (w *WeightedDist) Sample() int {
 
        return w.minValue + w.values[idx]
 }
+
+// String returns a dump of the distribution table.
+func (w *WeightedDist) String() string {
+       var buf bytes.Buffer
+
+       buf.WriteString("[ ")
+       for i, v := range w.values {
+               p := w.weights[i]
+               if p > 0.01 { // Squelch tiny probabilities.
+                       buf.WriteString(fmt.Sprintf("%d: %f ", v, p)) 
+               }
+       }
+       buf.WriteString("]")
+       return buf.String()
+}
diff --git a/doc/obfs4proxy.1 b/doc/obfs4proxy.1
index e442946..a9c6257 100644
--- a/doc/obfs4proxy.1
+++ b/doc/obfs4proxy.1
@@ -1,4 +1,4 @@
-.TH OBFS4PROXY 1 "2014-09-24"
+.TH OBFS4PROXY 1 "2015-01-20"
 .SH NAME
 obfs4proxy \- pluggable transport proxy for Tor, implementing obfs4
 .SH SYNOPSIS
@@ -11,7 +11,8 @@ censors, who usually monitor traffic between the client and 
the bridge,
 will see innocent-looking transformed traffic instead of the actual Tor
 traffic.
 .PP
-obfs4proxy implements the obfuscation protocols obfs2, obfs3 and obfs4.
+obfs4proxy implements the obfuscation protocols obfs2, obfs3, 
+ScrambleSuit (client only) and obfs4.
 .PP
 obfs4proxy is currently only supported as a managed pluggable transport
 spawned as a helper process via the \fBtor\fR daemon.
diff --git a/transports/scramblesuit/base.go b/transports/scramblesuit/base.go
new file mode 100644
index 0000000..711c046
--- /dev/null
+++ b/transports/scramblesuit/base.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Package scramblesuit provides an implementation of the ScrambleSuit
+// obfuscation protocol.  The implementation is client only.
+package scramblesuit
+
+import (
+       "fmt"
+       "net"
+
+       "git.torproject.org/pluggable-transports/goptlib.git"
+       "git.torproject.org/pluggable-transports/obfs4.git/transports/base"
+)
+
+const transportName = "scramblesuit"
+
+// Transport is the ScrambleSuit implementation of the base.Transport 
interface.
+type Transport struct{}
+
+// Name returns the name of the ScrambleSuit transport protocol.
+func (t *Transport) Name() string {
+       return transportName
+}
+
+// ClientFactory returns a new ssClientFactory instance.
+func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) 
{
+       tStore, err := loadTicketStore(stateDir)
+       if err != nil {
+               return nil, err
+       }
+       cf := &ssClientFactory{transport: t, ticketStore: tStore}
+       return cf, nil
+}
+
+// ServerFactory will one day return a new ssServerFactory instance.
+func (t *Transport) ServerFactory(stateDir string, args *pt.Args) 
(base.ServerFactory, error) {
+       // TODO: Fill this in eventually, though obfs4 is better.
+       return nil, fmt.Errorf("server not supported")
+}
+
+type ssClientFactory struct {
+       transport   base.Transport
+       ticketStore *ssTicketStore
+}
+
+func (cf *ssClientFactory) Transport() base.Transport {
+       return cf.transport
+}
+
+func (cf *ssClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
+       return newClientArgs(args)
+}
+
+func (cf *ssClientFactory) WrapConn(conn net.Conn, args interface{}) 
(net.Conn, error) {
+       ca, ok := args.(*ssClientArgs)
+       if !ok {
+               return nil, fmt.Errorf("invalid argument type for args")
+       }
+       return newScrambleSuitClientConn(conn, cf.ticketStore, ca)
+}
+
+var _ base.ClientFactory = (*ssClientFactory)(nil)
+var _ base.Transport = (*Transport)(nil)
diff --git a/transports/scramblesuit/conn.go b/transports/scramblesuit/conn.go
new file mode 100644
index 0000000..b77bc5f
--- /dev/null
+++ b/transports/scramblesuit/conn.go
@@ -0,0 +1,521 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package scramblesuit
+
+import (
+       "bytes"
+       "crypto/aes"
+       "crypto/cipher"
+       "crypto/hmac"
+       "crypto/sha256"
+       "encoding/base32"
+       "encoding/binary"
+       "errors"
+       "fmt"
+       "hash"
+       "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/drbg"
+       "git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
+       "git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
+)
+
+const (
+       passwordArg = "password"
+
+       maxSegmentLength       = 1448
+       maxPayloadLength       = 1427
+       sharedSecretLength     = 160 / 8 // k_B
+       clientHandshakeTimeout = time.Duration(60) * time.Second
+
+       minLenDistLength = 21
+       maxLenDistLength = maxSegmentLength
+
+       keyLength = 32 + 8 + 32
+
+       pktPrngSeedLength = 32
+       pktOverhead       = macLength + pktHdrLength
+       pktHdrLength      = 2 + 2 + 1
+       pktPayload        = 1
+       pktNewTicket      = 1 << 1
+       pktPrngSeed       = 1 << 2
+)
+
+var (
+       // ErrNotSupported is the error returned for a unsupported operation.
+       ErrNotSupported = errors.New("scramblesuit: operation not supported")
+
+       // ErrInvalidPacket is the error returned when a invalid packet is 
received.
+       ErrInvalidPacket = errors.New("scramblesuit: invalid packet")
+
+       zeroPadBytes [maxPayloadLength]byte
+)
+
+type ssSharedSecret [sharedSecretLength]byte
+
+type ssClientArgs struct {
+       kB         *ssSharedSecret
+       sessionKey *uniformdh.PrivateKey
+}
+
+func newClientArgs(args *pt.Args) (ca *ssClientArgs, err error) {
+       ca = &ssClientArgs{}
+       if ca.kB, err = parsePasswordArg(args); err != nil {
+               return nil, err
+       }
+
+       // Generate the client keypair before opening a connection since the 
time
+       // taken is visible to an adversary.  This key might not end up being 
used
+       // if a session ticket is present, but this doesn't take that long.
+       if ca.sessionKey, err = uniformdh.GenerateKey(csrand.Reader); err != 
nil {
+               return nil, err
+       }
+       return
+}
+
+func parsePasswordArg(args *pt.Args) (*ssSharedSecret, error) {
+       str, ok := args.Get(passwordArg)
+       if !ok {
+               return nil, fmt.Errorf("missing argument '%s'", passwordArg)
+       }
+
+       // To match the obfsproxy behavior, 'str' should contain a Base32 
encoded
+       // shared secret (k_B) used for handshaking.
+       decoded, err := base32.StdEncoding.DecodeString(str)
+       if err != nil {
+               return nil, fmt.Errorf("failed to decode password: %s", err)
+       }
+       if len(decoded) != sharedSecretLength {
+               return nil, fmt.Errorf("password length %d is invalid", 
len(decoded))
+       }
+       ss := new(ssSharedSecret)
+       copy(ss[:], decoded)
+       return ss, nil
+}
+
+type ssCryptoState struct {
+       s   cipher.Stream
+       mac hash.Hash
+}
+
+func newCryptoState(aesKey []byte, ivPrefix []byte, macKey []byte) 
(*ssCryptoState, error) {
+       // The ScrambleSuit CTR-AES256 link crypto uses an 8 byte prefix from 
the
+       // KDF, and a 64 bit counter initialized to 1 as the IV.  The initial 
value
+       // of the counter isn't documented in the spec either.
+       var initialCtr = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
+       iv := make([]byte, 0, aes.BlockSize)
+       iv = append(iv, ivPrefix...)
+       iv = append(iv, initialCtr...)
+       b, err := aes.NewCipher(aesKey)
+       if err != nil {
+               return nil, err
+       }
+       s := cipher.NewCTR(b, iv)
+       mac := hmac.New(sha256.New, macKey)
+       return &ssCryptoState{s: s, mac: mac}, nil
+}
+
+type ssConn struct {
+       net.Conn
+
+       isServer bool
+
+       lenDist              *probdist.WeightedDist
+       receiveBuffer        *bytes.Buffer
+       receiveDecodedBuffer *bytes.Buffer
+       receiveState         ssRxState
+
+       txCrypto *ssCryptoState
+       rxCrypto *ssCryptoState
+
+       ticketStore *ssTicketStore
+}
+
+type ssRxState struct {
+       mac []byte
+       hdr []byte
+
+       totalLen   int
+       payloadLen int
+}
+
+func (conn *ssConn) Read(b []byte) (n int, err error) {
+       // If the receive payload buffer is empty, consume data off the network.
+       for conn.receiveDecodedBuffer.Len() == 0 {
+               if err = conn.readPackets(); err != nil {
+                       break
+               }
+       }
+
+       // Service the read request using buffered payload.
+       if conn.receiveDecodedBuffer.Len() > 0 {
+               n, _ = conn.receiveDecodedBuffer.Read(b)
+       }
+       return
+}
+
+func (conn *ssConn) Write(b []byte) (n int, err error) {
+       var frameBuf bytes.Buffer
+       p := b
+       toSend := len(p)
+
+       for toSend > 0 {
+               // Send as much payload as will fit into each frame as possible.
+               wrLen := len(p)
+               if wrLen > maxPayloadLength {
+                       wrLen = maxPayloadLength
+               }
+               payload := p[:wrLen]
+               if err = conn.makePacket(&frameBuf, pktPayload, payload, 0); 
err != nil {
+                       return 0, err
+               }
+
+               toSend -= wrLen
+               p = p[wrLen:]
+               n += wrLen
+       }
+
+       // Pad out the burst as appropriate.
+       if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); err != nil {
+               return 0, err
+       }
+
+       // Write and return.
+       _, err = conn.Conn.Write(frameBuf.Bytes())
+       return
+}
+
+func (conn *ssConn) SetDeadline(t time.Time) error {
+       return ErrNotSupported
+}
+
+func (conn *ssConn) SetReadDeadline(t time.Time) error {
+       return ErrNotSupported
+}
+
+func (conn *ssConn) SetWriteDeadline(t time.Time) error {
+       return ErrNotSupported
+}
+
+func (conn *ssConn) makePacket(w io.Writer, pktType byte, data []byte, padLen 
int) error {
+       payloadLen := len(data)
+       totalLen := payloadLen + padLen
+       if totalLen > maxPayloadLength {
+               panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > 
maxPayloadLength: %d + %d > %d", len(data), padLen, maxPayloadLength))
+       }
+
+       // Build the packet header (total length, payload length, flags),
+       // and append the payload and padding.
+       pkt := make([]byte, pktHdrLength, pktHdrLength+payloadLen+padLen)
+       binary.BigEndian.PutUint16(pkt[0:], uint16(totalLen))
+       binary.BigEndian.PutUint16(pkt[2:], uint16(payloadLen))
+       pkt[4] = pktType
+       pkt = append(pkt, data...)
+       pkt = append(pkt, zeroPadBytes[:padLen]...)
+
+       // Encrypt the packet, and calculate the MAC.
+       conn.txCrypto.s.XORKeyStream(pkt, pkt)
+       conn.txCrypto.mac.Reset()
+       conn.txCrypto.mac.Write(pkt)
+       mac := conn.txCrypto.mac.Sum(nil)[:macLength]
+
+       // Write out MAC | Packet.  Note that this does not go onto the network
+       // yet, as w is a byte.Buffer (This is done so each call to conn.Write()
+       // gets padding added).
+       if _, err := w.Write(mac); err != nil {
+               return err
+       }
+       _, err := w.Write(pkt)
+       return err
+}
+
+func (conn *ssConn) readPackets() error {
+       // Consume and buffer up to 1 MSS worth of data.
+       var buf [maxSegmentLength]byte
+       rdLen, rdErr := conn.Conn.Read(buf[:])
+       conn.receiveBuffer.Write(buf[:rdLen])
+
+       // Process incoming packets incrementally.  conn.receiveState stores
+       // the results of partial processing.
+       for conn.receiveBuffer.Len() > 0 {
+               if conn.receiveState.mac == nil {
+                       // Read and store the packet MAC.
+                       if conn.receiveBuffer.Len() < macLength {
+                               break
+                       }
+                       mac := make([]byte, macLength)
+                       conn.receiveBuffer.Read(mac)
+                       conn.receiveState.mac = mac
+               }
+
+               if conn.receiveState.hdr == nil {
+                       // Read and store the packet header.
+                       if conn.receiveBuffer.Len() < pktHdrLength {
+                               break
+                       }
+                       hdr := make([]byte, pktHdrLength)
+                       conn.receiveBuffer.Read(hdr)
+
+                       // Add the encrypted packet header to the HMAC 
instance, and then
+                       // decrypt it so that the length of the packet can be 
determined.
+                       conn.rxCrypto.mac.Reset()
+                       conn.rxCrypto.mac.Write(hdr)
+                       conn.rxCrypto.s.XORKeyStream(hdr, hdr)
+
+                       // Store the plaintext packet header, and host byte 
order length
+                       // values.
+                       totalLen := int(binary.BigEndian.Uint16(hdr[0:]))
+                       payloadLen := int(binary.BigEndian.Uint16(hdr[2:]))
+                       if payloadLen > totalLen || totalLen > maxPayloadLength 
{
+                               return ErrInvalidPacket
+                       }
+                       conn.receiveState.hdr = hdr
+                       conn.receiveState.totalLen = totalLen
+                       conn.receiveState.payloadLen = payloadLen
+               }
+
+               var data []byte
+               if conn.receiveState.totalLen > 0 {
+                       // If the packet actually has payload (including 
padding), read,
+                       // digest and decrypt it.
+                       if conn.receiveBuffer.Len() < 
conn.receiveState.totalLen {
+                               break
+                       }
+                       data = make([]byte, conn.receiveState.totalLen)
+                       conn.receiveBuffer.Read(data)
+                       conn.rxCrypto.mac.Write(data)
+                       conn.rxCrypto.s.XORKeyStream(data, data)
+               }
+
+               // Authenticate the packet, by comparing the received MAC with 
the one
+               // calculated over the ciphertext consumed off the network.
+               cmpMAC := conn.rxCrypto.mac.Sum(nil)[:macLength]
+               if !hmac.Equal(cmpMAC, conn.receiveState.mac[:]) {
+                       return ErrInvalidPacket
+               }
+
+               // Based on the packet flags, do something useful with the 
payload.
+               data = data[:conn.receiveState.payloadLen]
+               switch conn.receiveState.hdr[4] {
+               case pktPayload:
+                       // User data, write it into the decoded payload buffer 
so that Read
+                       // calls can be serviced.
+                       conn.receiveDecodedBuffer.Write(data)
+               case pktNewTicket:
+                       // New Session Ticket to be used for future handshakes, 
store it in
+                       // the Session Ticket store.
+                       if conn.isServer || len(data) != 
ticketKeyLength+ticketLength {
+                               return ErrInvalidPacket
+                       }
+                       conn.ticketStore.storeTicket(conn.RemoteAddr(), data)
+               case pktPrngSeed:
+                       // New PRNG_SEED for the protocol polymorphism.  
Regenerate the
+                       // length obfuscation probability distribution.
+                       if conn.isServer || len(data) != pktPrngSeedLength {
+                               return ErrInvalidPacket
+                       }
+                       seed, err := drbg.SeedFromBytes(data)
+                       if err != nil {
+                               return ErrInvalidPacket
+                       }
+                       conn.lenDist.Reset(seed)
+               default:
+                       return ErrInvalidPacket
+               }
+
+               // Done processing a packet, clear the partial state.
+               conn.receiveState.mac = nil
+               conn.receiveState.hdr = nil
+               conn.receiveState.totalLen = 0
+               conn.receiveState.payloadLen = 0
+       }
+       return rdErr
+}
+
+func (conn *ssConn) clientHandshake(kB *ssSharedSecret, sessionKey 
*uniformdh.PrivateKey) error {
+       if conn.isServer {
+               return fmt.Errorf("clientHandshake called on server connection")
+       }
+
+       // Query the Session Ticket store to see if there is a stored session
+       // ticket.
+       ticket, err := conn.ticketStore.getTicket(conn.RemoteAddr())
+       if err != nil {
+               return err
+       } else if ticket != nil {
+               // Ok, there is an existing ticket, so attempt to do a Session 
Ticket
+               // handshake.  Until we write to the network, failures are 
non-fatal as
+               // we can transition gracefully into doing a UniformDH 
handshake.
+
+               // Derive the keys from the prestored master key received with 
the
+               // ticket.  This is done before the actual handshake since the
+               // handshake uses the outgoing HMAC-SHA256-128 key for 
authentication.
+               if err = conn.initCrypto(ticket.key[:]); err != nil {
+                       goto handshakeUDH
+               }
+
+               // Generate and send the ticket handshake.  There is no 
response, since
+               // both sides have the keying material.
+               hs := newTicketClientHandshake(conn.txCrypto.mac, ticket)
+               blob, err := hs.generateHandshake()
+               if err != nil {
+                       goto handshakeUDH
+               }
+               if _, err = conn.Conn.Write(blob); err != nil {
+                       return err
+               }
+               return nil
+       }
+
+handshakeUDH:
+       // No session ticket, so take the slow path and do a UniformDH based
+       // handshake.
+
+       // Generate and send the client handshake.
+       hs := newDHClientHandshake(kB, sessionKey)
+       blob, err := hs.generateHandshake()
+       if err != nil {
+               return err
+       }
+       if _, err = conn.Conn.Write(blob); err != nil {
+               return err
+       }
+
+       // Consume the server handshake.  Since we don't actually know the 
length
+       // of the respose, we need to consume data off the network till we 
either
+       // find the tail marker + MAC digest indicating that a handshake 
response
+       // has been received, or the maximum handshake size passes without a 
valid
+       // response.
+       var hsBuf [maxHandshakeLength]byte
+       for {
+               var n int
+               if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
+                       return err
+               }
+               conn.receiveBuffer.Write(hsBuf[:n])
+
+               // Attempt to process all the data seen so far as a response.
+               var seed []byte
+               n, seed, err = 
hs.parseServerHandshake(conn.receiveBuffer.Bytes())
+               if err == errMarkNotFoundYet {
+                       // No response found yet, keep trying.
+                       continue
+               } else if err != nil {
+                       return err
+               }
+
+               // Ok, done processing the handshake, discard the response, and 
do the
+               // key derivation based off the calculated shared secret.
+               _ = conn.receiveBuffer.Next(n)
+               err = conn.initCrypto(seed)
+               return err
+       }
+}
+
+func (conn *ssConn) initCrypto(seed []byte) (err error) {
+       // Use HKDF-SHA256 (Expand only, no Extract) to generate session keys 
from
+       // initial keying material.
+       okm := hkdfExpand(sha256.New, seed, nil, kdfSecretLength)
+       if conn.txCrypto, err = newCryptoState(okm[0:32], okm[32:40], 
okm[80:112]); err != nil {
+               return
+       }
+       if conn.rxCrypto, err = newCryptoState(okm[40:72], okm[72:80], 
okm[112:144]); err != nil {
+               return
+       }
+       return
+}
+
+func (conn *ssConn) padBurst(burst *bytes.Buffer, sampleLen int) (err error) {
+       // Burst contains the fully encrypted+MACed outgoing payload that will 
be
+       // written to the network.  Pad it out so that the last segment (based 
on
+       // the ScrambleSuit MTU) is sampleLen bytes.
+
+       dataLen := burst.Len() % maxSegmentLength
+       padLen := 0
+       if sampleLen >= dataLen {
+               padLen = sampleLen - dataLen
+       } else {
+               padLen = (maxSegmentLength - dataLen) + sampleLen
+       }
+       if padLen < pktOverhead {
+               // The padLen is less than the MAC + packet header in length, so
+               // two packets are required.
+               padLen += maxSegmentLength
+       }
+
+       if padLen == 0 {
+               return
+       } else if padLen > maxSegmentLength {
+               // Note: packetmorpher.py: getPadding is slightly wrong and only
+               // accounts for one of the two packet headers.
+               if err = conn.makePacket(burst, pktPayload, nil, 
700-pktOverhead); err != nil {
+                       return
+               }
+               err = conn.makePacket(burst, pktPayload, nil, 
padLen-(700+2*pktOverhead))
+       } else {
+               err = conn.makePacket(burst, pktPayload, nil, 
padLen-pktOverhead)
+       }
+       return
+}
+
+func newScrambleSuitClientConn(conn net.Conn, tStore *ssTicketStore, ca 
*ssClientArgs) (net.Conn, error) {
+       // At this point we have kB and our session key, so we can directly
+       // start handshaking and seeing what happens.
+
+       // Seed the initial polymorphism distribution.
+       seed, err := drbg.NewSeed()
+       if err != nil {
+               return nil, err
+       }
+       dist := probdist.New(seed, minLenDistLength, maxLenDistLength, true)
+
+       // Allocate the client structure.
+       c := &ssConn{conn, false, dist, bytes.NewBuffer(nil), 
bytes.NewBuffer(nil), ssRxState{}, nil, nil, tStore}
+
+       // Start the handshake timeout.
+       deadline := time.Now().Add(clientHandshakeTimeout)
+       if err := conn.SetDeadline(deadline); err != nil {
+               return nil, err
+       }
+
+       // Attempt to handshake.
+       if err := c.clientHandshake(ca.kB, ca.sessionKey); err != nil {
+               return nil, err
+       }
+
+       // Stop the handshake timeout.
+       if err := conn.SetDeadline(time.Time{}); err != nil {
+               return nil, err
+       }
+
+       return c, nil
+}
diff --git a/transports/scramblesuit/handshake_ticket.go 
b/transports/scramblesuit/handshake_ticket.go
new file mode 100644
index 0000000..ad9b4d4
--- /dev/null
+++ b/transports/scramblesuit/handshake_ticket.go
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package scramblesuit
+
+import (
+       "bytes"
+       "encoding/base32"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "hash"
+       "io/ioutil"
+       "net"
+       "os"
+       "path"
+       "strconv"
+       "sync"
+       "time"
+
+       "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+)
+
+const (
+       ticketFile = "scramblesuit_tickets.json"
+
+       ticketKeyLength = 32
+       ticketLength    = 112
+       ticketLifetime  = 60 * 60 * 24 * 7
+
+       ticketMinPadLength = 0
+       ticketMaxPadLength = 1388
+)
+
+var (
+       errInvalidTicket = errors.New("scramblesuit: invalid serialized ticket")
+)
+
+type ssTicketStore struct {
+       sync.Mutex
+
+       filePath string
+       store    map[string]*ssTicket
+}
+
+type ssTicket struct {
+       key      [ticketKeyLength]byte
+       ticket   [ticketLength]byte
+       issuedAt int64
+}
+
+type ssTicketJSON struct {
+       KeyTicket string `json:"key-ticket"`
+       IssuedAt  int64  `json:"issuedAt"`
+}
+
+func (t *ssTicket) isValid() bool {
+       return t.issuedAt+ticketLifetime > time.Now().Unix()
+}
+
+func newTicket(raw []byte) (*ssTicket, error) {
+       if len(raw) != ticketKeyLength+ticketLength {
+               return nil, errInvalidTicket
+       }
+       t := &ssTicket{issuedAt: time.Now().Unix()}
+       copy(t.key[:], raw[0:])
+       copy(t.ticket[:], raw[ticketKeyLength:])
+       return t, nil
+}
+
+func (s *ssTicketStore) storeTicket(addr net.Addr, rawT []byte) {
+       t, err := newTicket(rawT)
+       if err != nil {
+               // Silently ignore ticket store failures.
+               return
+       }
+
+       s.Lock()
+       defer s.Unlock()
+
+       // Add the ticket to the map, and checkpoint to disk.  Serialization 
errors
+       // are ignored because the handshake code will just use UniformDH if a
+       // ticket is not available.
+       s.store[addr.String()] = t
+       s.serialize()
+}
+
+func (s *ssTicketStore) getTicket(addr net.Addr) (*ssTicket, error) {
+       aStr := addr.String()
+
+       s.Lock()
+       defer s.Unlock()
+
+       t, ok := s.store[aStr]
+       if ok && t != nil {
+               // Tickets are one use only, so remove tickets from the map, and
+               // checkpoint the map to disk.
+               delete(s.store, aStr)
+               err := s.serialize()
+               if !t.isValid() {
+                       // Expired ticket, ignore it.
+                       return nil, err
+               }
+               return t, err
+       }
+
+       // No ticket was found, that's fine.
+       return nil, nil
+}
+
+func (s *ssTicketStore) serialize() error {
+       encMap := make(map[string]*ssTicketJSON)
+       for k, v := range s.store {
+               kt := make([]byte, 0, ticketKeyLength+ticketLength)
+               kt = append(kt, v.key[:]...)
+               kt = append(kt, v.ticket[:]...)
+               ktStr := base32.StdEncoding.EncodeToString(kt)
+               jsonObj := &ssTicketJSON{KeyTicket: ktStr, IssuedAt: v.issuedAt}
+               encMap[k] = jsonObj
+       }
+       jsonStr, err := json.Marshal(encMap)
+       if err != nil {
+               return err
+       }
+       return ioutil.WriteFile(s.filePath, jsonStr, 0600)
+}
+
+func loadTicketStore(stateDir string) (*ssTicketStore, error) {
+       fPath := path.Join(stateDir, ticketFile)
+       s := &ssTicketStore{filePath: fPath}
+       s.store = make(map[string]*ssTicket)
+
+       f, err := ioutil.ReadFile(fPath)
+       if err != nil {
+               // No ticket store is fine.
+               if os.IsNotExist(err) {
+                       return s, nil
+               }
+
+               // But a file read error is not.
+               return nil, err
+       }
+
+       encMap := make(map[string]*ssTicketJSON)
+       if err = json.Unmarshal(f, &encMap); err != nil {
+               return nil, fmt.Errorf("failed to load ticket store '%s': 
'%s'", fPath, err)
+       }
+       for k, v := range encMap {
+               raw, err := base32.StdEncoding.DecodeString(v.KeyTicket)
+               if err != nil || len(raw) != ticketKeyLength+ticketLength {
+                       // Just silently skip corrupted tickets.
+                       continue
+               }
+               t := &ssTicket{issuedAt: v.IssuedAt}
+               if !t.isValid() {
+                       // Just ignore expired tickets.
+                       continue
+               }
+               copy(t.key[:], raw[0:])
+               copy(t.ticket[:], raw[ticketKeyLength:])
+               s.store[k] = t
+       }
+       return s, nil
+}
+
+type ssTicketClientHandshake struct {
+       mac    hash.Hash
+       ticket *ssTicket
+       padLen int
+}
+
+func (hs *ssTicketClientHandshake) generateHandshake() ([]byte, error) {
+       var buf bytes.Buffer
+       hs.mac.Reset()
+
+       // The client handshake is T | P | M | MAC(T | P | M | E)
+       hs.mac.Write(hs.ticket.ticket[:])
+       m := hs.mac.Sum(nil)[:macLength]
+       p, err := makePad(hs.padLen)
+       if err != nil {
+               return nil, err
+       }
+
+       // Write T, P, M.
+       buf.Write(hs.ticket.ticket[:])
+       buf.Write(p)
+       buf.Write(m)
+
+       // Calculate and write the MAC.
+       e := []byte(strconv.FormatInt(getEpochHour(), 10))
+       hs.mac.Write(p)
+       hs.mac.Write(m)
+       hs.mac.Write(e)
+       buf.Write(hs.mac.Sum(nil)[:macLength])
+
+       hs.mac.Reset()
+       return buf.Bytes(), nil
+}
+
+func newTicketClientHandshake(mac hash.Hash, ticket *ssTicket) 
*ssTicketClientHandshake {
+       hs := &ssTicketClientHandshake{mac: mac, ticket: ticket}
+       hs.padLen = csrand.IntRange(ticketMinPadLength, ticketMaxPadLength)
+       return hs
+}
diff --git a/transports/scramblesuit/handshake_uniformdh.go 
b/transports/scramblesuit/handshake_uniformdh.go
new file mode 100644
index 0000000..4345d65
--- /dev/null
+++ b/transports/scramblesuit/handshake_uniformdh.go
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package scramblesuit
+
+import (
+       "bytes"
+       "crypto/hmac"
+       "crypto/sha256"
+       "errors"
+       "hash"
+       "strconv"
+       "time"
+
+       "git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
+       "git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
+)
+
+const (
+       minHandshakeLength = uniformdh.Size + macLength*2
+       maxHandshakeLength = 1532
+       dhMinPadLength     = 0
+       dhMaxPadLength     = 1308
+       macLength          = 128 / 8 // HMAC-SHA256-128()
+
+       kdfSecretLength = keyLength * 2
+)
+
+var (
+       errMarkNotFoundYet = errors.New("mark not found yet")
+
+       // ErrInvalidHandshake is the error returned when the handshake fails.
+       ErrInvalidHandshake = errors.New("invalid handshake")
+)
+
+type ssDHClientHandshake struct {
+       mac       hash.Hash
+       keypair   *uniformdh.PrivateKey
+       epochHour []byte
+       padLen    int
+
+       serverPublicKey *uniformdh.PublicKey
+       serverMark      []byte
+}
+
+func (hs *ssDHClientHandshake) generateHandshake() ([]byte, error) {
+       var buf bytes.Buffer
+       hs.mac.Reset()
+
+       // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E)
+       x, err := hs.keypair.PublicKey.Bytes()
+       if err != nil {
+               return nil, err
+       }
+       hs.mac.Write(x)
+       mC := hs.mac.Sum(nil)[:macLength]
+       pC, err := makePad(hs.padLen)
+       if err != nil {
+               return nil, err
+       }
+
+       // Write X, P_C, M_C.
+       buf.Write(x)
+       buf.Write(pC)
+       buf.Write(mC)
+
+       // Calculate and write the MAC.
+       hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
+       hs.mac.Write(pC)
+       hs.mac.Write(mC)
+       hs.mac.Write(hs.epochHour)
+       buf.Write(hs.mac.Sum(nil)[:macLength])
+
+       return buf.Bytes(), nil
+}
+
+func (hs *ssDHClientHandshake) parseServerHandshake(resp []byte) (int, []byte, 
error) {
+       if len(resp) < minHandshakeLength {
+               return 0, nil, errMarkNotFoundYet
+       }
+
+       // The server response is Y | P_S | M_S | MAC(Y | P_S | M_S | E).
+       if hs.serverPublicKey == nil {
+               y := resp[:uniformdh.Size]
+
+               // Pull out the public key, and derive the server mark.
+               hs.serverPublicKey = &uniformdh.PublicKey{}
+               err := hs.serverPublicKey.SetBytes(y)
+               if err != nil {
+                       return 0, nil, err
+               }
+               hs.mac.Reset()
+               hs.mac.Write(y)
+               hs.serverMark = hs.mac.Sum(nil)[:macLength]
+       }
+
+       // Find the mark+MAC, if it exits.
+       endPos := len(resp)
+       if endPos > maxHandshakeLength-macLength {
+               endPos = maxHandshakeLength - macLength
+       }
+       pos := bytes.Index(resp[uniformdh.Size:endPos], hs.serverMark)
+       if pos == -1 {
+               if len(resp) >= maxHandshakeLength {
+                       // Couldn't find the mark in a maximum length response.
+                       return 0, nil, ErrInvalidHandshake
+               }
+               return 0, nil, errMarkNotFoundYet
+       } else if len(resp) < pos+2*macLength {
+               // Didn't receive the full M_S.
+               return 0, nil, errMarkNotFoundYet
+       }
+       pos += uniformdh.Size
+
+       // Validate the MAC.
+       hs.mac.Write(resp[uniformdh.Size : pos+macLength])
+       hs.mac.Write(hs.epochHour)
+       macCmp := hs.mac.Sum(nil)[:macLength]
+       macRx := resp[pos+macLength : pos+2*macLength]
+       if !hmac.Equal(macCmp, macRx) {
+               return 0, nil, ErrInvalidHandshake
+       }
+
+       // Derive the shared secret.
+       ss, err := uniformdh.Handshake(hs.keypair, hs.serverPublicKey)
+       if err != nil {
+               return 0, nil, err
+       }
+       seed := sha256.Sum256(ss)
+       return pos + 2*macLength, seed[:], nil
+}
+
+func newDHClientHandshake(kB *ssSharedSecret, sessionKey 
*uniformdh.PrivateKey) *ssDHClientHandshake {
+       hs := &ssDHClientHandshake{keypair: sessionKey}
+       hs.mac = hmac.New(sha256.New, kB[:])
+       hs.padLen = csrand.IntRange(dhMinPadLength, dhMaxPadLength)
+       return hs
+}
+
+func getEpochHour() int64 {
+       return time.Now().Unix() / 3600
+}
+
+func makePad(padLen int) ([]byte, error) {
+       pad := make([]byte, padLen)
+       if err := csrand.Bytes(pad); err != nil {
+               return nil, err
+       }
+       return pad, nil
+}
diff --git a/transports/scramblesuit/hkdf_expand.go 
b/transports/scramblesuit/hkdf_expand.go
new file mode 100644
index 0000000..9626b38
--- /dev/null
+++ b/transports/scramblesuit/hkdf_expand.go
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package scramblesuit
+
+import (
+       "crypto/hmac"
+       "hash"
+)
+
+func hkdfExpand(hashFn func() hash.Hash, prk []byte, info []byte, l int) 
[]byte {
+       // Why, yes.  golang.org/x/crypto/hkdf exists, and is a fine
+       // implementation of HKDF.  However it does both the extract
+       // and expand, while ScrambleSuit only does extract, with no
+       // way to separate the two steps.
+
+       h := hmac.New(hashFn, prk)
+       digestSz := h.Size()
+       if l > 255*digestSz {
+               panic("hkdf: requested OKM length > 255*HashLen")
+       }
+
+       var t []byte
+       okm := make([]byte, 0, l)
+       toAppend := l
+       ctr := byte(1)
+       for toAppend > 0 {
+               h.Reset()
+               h.Write(t)
+               h.Write(info)
+               h.Write([]byte{ctr})
+               t = h.Sum(nil)
+               ctr++
+
+               aLen := digestSz
+               if toAppend < digestSz {
+                       aLen = toAppend
+               }
+               okm = append(okm, t[:aLen]...)
+               toAppend -= aLen
+       }
+       return okm
+}
diff --git a/transports/transports.go b/transports/transports.go
index 6b80bdc..ba4e099 100644
--- a/transports/transports.go
+++ b/transports/transports.go
@@ -37,6 +37,7 @@ import (
        "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"
+       
"git.torproject.org/pluggable-transports/obfs4.git/transports/scramblesuit"
 )
 
 var transportMapLock sync.Mutex
@@ -88,4 +89,5 @@ func init() {
        Register(new(obfs2.Transport))
        Register(new(obfs3.Transport))
        Register(new(obfs4.Transport))
+       Register(new(scramblesuit.Transport))
 }



_______________________________________________
tor-commits mailing list
[email protected]
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits

Reply via email to