diff --git a/README.md b/README.md index ef38532e7d91a955bd6604ea3929351c3b6ace65..549e27c71bdcfb8ec834b64aea353962c9783186 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ is much closer to ScrambleSuit than obfs2/obfs3. The notable differences between ScrambleSuit and obfs4: * The handshake always does a full key exchange (no such thing as a Session - Ticket Handshake). (TODO: Reconsider this.) + Ticket Handshake). * The handshake uses the Tor Project's ntor handshake with public keys obfuscated via the Elligator mapping. * The link layer encryption uses NaCl secret boxes (Poly1305/XSalsa20). @@ -32,7 +32,6 @@ handshake variants without being obscenely slow is non-trivial. ### TODO - * Packet length obfuscation. * (Maybe) Make it resilient to transient connection loss. * (Maybe) Use IP_MTU/TCP_MAXSEG to tweak frame size. * Write a detailed protocol spec. diff --git a/handshake_ntor.go b/handshake_ntor.go index ea9de71747defdd42a1b52481dd49a8eba8a2ac5..84cd93cfe4035e95eed9c4dbc744292db60e4ed1 100644 --- a/handshake_ntor.go +++ b/handshake_ntor.go @@ -363,13 +363,10 @@ func findMark(mark, buf []byte, startPos, maxPos int) int { return pos + startPos } -func makePad(min, max int64) ([]byte, error) { - padLen, err := randRange(min, max) - if err != nil { - return nil, err - } +func makePad(min, max int) ([]byte, error) { + padLen := randRange(min, max) pad := make([]byte, padLen) - _, err = rand.Read(pad) + _, err := rand.Read(pad) if err != nil { return nil, err } diff --git a/obfs4.go b/obfs4.go index e69c7b7928da3d540338b0c2a1d5a00934dfd989..cd6f75da5ddbd7da93bf60589905a52473f159e4 100644 --- a/obfs4.go +++ b/obfs4.go @@ -40,6 +40,7 @@ import ( ) const ( + headerLength = framing.FrameOverhead + packetOverhead defaultReadSize = framing.MaximumSegmentLength connectionTimeout = time.Duration(15) * time.Second @@ -54,6 +55,8 @@ const ( type Obfs4Conn struct { conn net.Conn + probDist *wDist + encoder *framing.Encoder decoder *framing.Decoder @@ -67,22 +70,33 @@ type Obfs4Conn struct { listener *Obfs4Listener } +func (c *Obfs4Conn) calcPadLen(burstLen int) int { + tailLen := burstLen % framing.MaximumSegmentLength + toPadTo := c.probDist.sample() + + ret := 0 + if toPadTo >= tailLen { + ret = toPadTo - tailLen + } else { + ret = (framing.MaximumSegmentLength - tailLen) + toPadTo + } + + return ret +} + func (c *Obfs4Conn) closeAfterDelay() { // I-it's not like I w-wanna handshake with you or anything. B-b-baka! defer c.conn.Close() - delaySecs, err := randRange(minCloseInterval, maxCloseInterval) - if err != nil { - return - } - toDiscard, err := randRange(minCloseThreshold, maxCloseThreshold) + delaySecs := randRange(minCloseInterval, maxCloseInterval) + toDiscard := randRange(minCloseThreshold, maxCloseThreshold) + + delay := time.Duration(delaySecs) * time.Second + err := c.conn.SetReadDeadline(time.Now().Add(delay)) if err != nil { return } - delay := time.Duration(delaySecs) * time.Second - err = c.conn.SetReadDeadline(time.Now().Add(delay)) - // Consume and discard data on this connection until either the specified // interval passes or a certain size has been reached. discarded := 0 @@ -286,7 +300,6 @@ func (c *Obfs4Conn) Read(b []byte) (int, error) { func (c *Obfs4Conn) Write(b []byte) (int, error) { chopBuf := bytes.NewBuffer(b) buf := make([]byte, maxPacketPayloadLength) - pkt := make([]byte, framing.MaximumFramePayloadLength) nSent := 0 var frameBuf bytes.Buffer @@ -295,26 +308,52 @@ func (c *Obfs4Conn) Write(b []byte) (int, error) { n, err := chopBuf.Read(buf) if err != nil { c.isOk = false - return nSent, err + return 0, err } else if n == 0 { panic(fmt.Sprintf("BUG: Write(), chopping length was 0")) } nSent += n - // Wrap the payload in a packet. - n = makePacket(pkt[:], packetTypePayload, buf[:n], 0) - - // Encode the packet in an AEAD frame. - _, frame, err := c.encoder.Encode(pkt[:n]) + _, frame, err := c.makeAndEncryptPacket(packetTypePayload, buf[:n], 0) if err != nil { c.isOk = false - return nSent, err + return 0, err } frameBuf.Write(frame) } - // TODO: Insert random padding. + // Insert random padding. In theory it's possible to inline padding for + // certain framesizes into the last AEAD packet, but always sending 1 or 2 + // padding frames is considerably easier. + padLen := c.calcPadLen(frameBuf.Len()) + if padLen > 0 { + if padLen > headerLength { + _, frame, err := c.makeAndEncryptPacket(packetTypePayload, []byte{}, + uint16(padLen-headerLength)) + if err != nil { + c.isOk = false + return 0, err + } + frameBuf.Write(frame) + } else { + _, frame, err := c.makeAndEncryptPacket(packetTypePayload, []byte{}, + maxPacketPayloadLength) + if err != nil { + c.isOk = false + return 0, err + } + frameBuf.Write(frame) + + _, frame, err = c.makeAndEncryptPacket(packetTypePayload, []byte{}, + uint16(padLen)) + if err != nil { + c.isOk = false + return 0, err + } + frameBuf.Write(frame) + } + } // Send the frame(s). _, err := c.conn.Write(frameBuf.Bytes()) @@ -323,7 +362,7 @@ func (c *Obfs4Conn) Write(b []byte) (int, error) { // at this point. It's possible to keep frameBuf around, but fuck it. // Someone that wants write timeouts can change this. c.isOk = false - return nSent, err // XXX: nSent is a dirty lie here. + return 0, err } return nSent, nil @@ -384,6 +423,10 @@ func Dial(network, address, nodeID, publicKey string) (net.Conn, error) { // Connect to the peer. c := new(Obfs4Conn) + c.probDist, err = newWDist(nil, 0, framing.MaximumSegmentLength) + if err != nil { + return nil, err + } c.conn, err = net.Dial(network, address) if err != nil { return nil, err @@ -420,6 +463,11 @@ func (l *Obfs4Listener) Accept() (net.Conn, error) { cObfs.conn = c cObfs.isServer = true cObfs.listener = l + cObfs.probDist, err = newWDist(nil, 0, framing.MaximumSegmentLength) + if err != nil { + c.Close() + return nil, err + } return cObfs, nil } diff --git a/packet.go b/packet.go index afccc47e940430d97f9661060f46c2471bdf2475..7bf4a6c829d9452b7c54cdb97316b0ad9750e7b4 100644 --- a/packet.go +++ b/packet.go @@ -79,12 +79,25 @@ func makePacket(pkt []byte, pktType uint8, data []byte, padLen uint16) int { pkt[0] = pktType binary.BigEndian.PutUint16(pkt[1:], uint16(len(data))) - copy(pkt[3:], data[:]) + if len(data) > 0 { + copy(pkt[3:], data[:]) + } copy(pkt[3+len(data):], zeroPadBytes[:padLen]) return pktLen } +func (c *Obfs4Conn) makeAndEncryptPacket(pktType uint8, data []byte, padLen uint16) (int, []byte, error) { + var pkt [framing.MaximumFramePayloadLength]byte + + // Wrap the payload in a packet. + n := makePacket(pkt[:], pktType, data[:], padLen) + + // Encode the packet in an AEAD frame. + n, frame, err := c.encoder.Encode(pkt[:n]) + return n, frame, err +} + func (c *Obfs4Conn) decodePacket(pkt []byte) error { if len(pkt) < packetOverhead { return InvalidPacketLengthError(len(pkt)) @@ -99,8 +112,13 @@ func (c *Obfs4Conn) decodePacket(pkt []byte) error { payload := pkt[3 : 3+payloadLen] switch pktType { case packetTypePayload: - // packetTypePayload - c.receiveDecodedBuffer.Write(payload) + if len(payload) > 0 { + c.receiveDecodedBuffer.Write(payload) + } + case packetTypePrngSeed: + if len(payload) == distSeedLength { + c.probDist.reset(payload) + } default: // Ignore unrecognised packet types. } diff --git a/utils.go b/utils.go index ae7bc41c057e617f77472ffb3bf70053f7d6e6b3..600a9252f39f81939d322ba6c9daedc6bc583802 100644 --- a/utils.go +++ b/utils.go @@ -28,21 +28,40 @@ package obfs4 import ( - "crypto/rand" + csrand "crypto/rand" "fmt" "math/big" + "math/rand" ) -func randRange(min, max int64) (int64, error) { +var ( + csRandSourceInstance csRandSource + csRandInstance = rand.New(csRandSourceInstance) +) + +type csRandSource struct { + // This does not keep any state as it is backed by crypto/rand. +} + +func (r csRandSource) Int63() int64 { + ret, err := csrand.Int(csrand.Reader, big.NewInt(int64((1<<63)-1))) + if err != nil { + panic(err) + } + + return ret.Int64() +} + +func (r csRandSource) Seed(seed int64) { + // No-op. +} + +func randRange(min, max int) int { if max < min { panic(fmt.Sprintf("randRange: min > max (%d, %d)", min, max)) } r := (max + 1) - min - ret, err := rand.Int(rand.Reader, big.NewInt(r)) - if err != nil { - return 0, err - } - - return ret.Int64() + min, nil + ret := csRandInstance.Intn(r) + return ret + min } diff --git a/weighted_dist.go b/weighted_dist.go new file mode 100644 index 0000000000000000000000000000000000000000..2fd39a06452c46078e8c9317124b91b31efd8518 --- /dev/null +++ b/weighted_dist.go @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at schwanenlied dot me> + * 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 ( + csrand "crypto/rand" + "encoding/binary" + "fmt" + "hash" + "math/rand" + + "github.com/dchest/siphash" +) + +const distSeedLength = 16 + +// InvalidSeedLengthError is the error returned when the seed provided to the +// DRBG is an invalid length. +type InvalidSeedLengthError int + +func (e InvalidSeedLengthError) Error() string { + return fmt.Sprintf("hashDrbg: Invalid seed length: %d", int(e)) +} + +// hashDrbg is a CSDRBG based off of SipHash-2-4 in OFB mode. +type hashDrbg struct { + sip hash.Hash64 + ofb [siphash.Size]byte +} + +// newHashDrbg makes a hashDrbg instance based off an optional seed. The seed +// is truncated to distSeedLength. +func newHashDrbg(seed []byte) *hashDrbg { + drbg := new(hashDrbg) + drbg.sip = siphash.New(seed) + + return drbg +} + +// Int63 returns a uniformly distributed random integer [0, 1 << 63). +func (drbg *hashDrbg) Int63() int64 { + // Use SipHash-2-4 in OFB mode to generate random numbers. + drbg.sip.Write(drbg.ofb[:]) + copy(drbg.ofb[:], drbg.sip.Sum(nil)) + + ret := binary.BigEndian.Uint64(drbg.ofb[:]) + ret &= (1<<63 - 1) + + return int64(ret) +} + +// Seed does nothing, call newHashDrbg if you want to reseed. +func (drbg *hashDrbg) Seed(seed int64) { + // No-op. +} + +// wDist is a weighted distribution. +type wDist struct { + minValue int + maxValue int + values []int + buckets []float64 +} + +// newWDist creates a weighted distribution of values ranging from min to max +// based on a CSDRBG initialized with the optional 128 bit seed. +func newWDist(seed []byte, min, max int) (*wDist, error) { + w := new(wDist) + w.minValue = min + w.maxValue = max + + if max <= min { + panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max)) + } + + err := w.reset(seed) + if err != nil { + return nil, err + } + + return w, nil +} + +// sample generates a random value according to the distribution. +func (w *wDist) sample() int { + retIdx := 0 + totalProb := 0.0 + prob := csRandInstance.Float64() + for i, bucketProb := range w.buckets { + totalProb += bucketProb + if prob <= totalProb { + retIdx = i + break + } + } + + return w.minValue + w.values[retIdx] +} + +// reset generates a new distribution with the same min/max based on a new seed. +func (w *wDist) reset(seed []byte) error { + if seed == nil { + seed = make([]byte, distSeedLength) + _, err := csrand.Read(seed) + if err != nil { + return err + } + } + if len(seed) != distSeedLength { + return InvalidSeedLengthError(len(seed)) + } + + // Initialize the deterministic random number generator. + drbg := newHashDrbg(seed) + dRng := rand.New(drbg) + + nBuckets := (w.maxValue + 1) - w.minValue + w.values = dRng.Perm(nBuckets) + + w.buckets = make([]float64, nBuckets) + var totalProb float64 + for i, _ := range w.buckets { + prob := dRng.Float64() * (1.0 - totalProb) + w.buckets[i] = prob + totalProb += prob + } + w.buckets[len(w.buckets)-1] = 1.0 + + return nil +}