mirror of
https://gitlab.com/yawning/obfs4.git
synced 2024-11-15 12:12:53 +00:00
Add preliminary support for packet length obfuscation.
The same algorithm as ScrambleSuit is used, except: * SipHash-2-4 in OFB mode is used to create the distribution. * The system CSPRNG is used when sampling the distribution. This fixes most of #3, all that remains is generating and sending a persistent distribution on the server side to the client.
This commit is contained in:
parent
51a8dd5a86
commit
9bfdd77f72
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
86
obfs4.go
86
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,21 +70,32 @@ 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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delaySecs := randRange(minCloseInterval, maxCloseInterval)
|
||||
toDiscard := randRange(minCloseThreshold, maxCloseThreshold)
|
||||
|
||||
delay := time.Duration(delaySecs) * time.Second
|
||||
err = c.conn.SetReadDeadline(time.Now().Add(delay))
|
||||
err := c.conn.SetReadDeadline(time.Now().Add(delay))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Consume and discard data on this connection until either the specified
|
||||
// interval passes or a certain size has been reached.
|
||||
@ -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
|
||||
}
|
||||
|
24
packet.go
24
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.
|
||||
}
|
||||
|
35
utils.go
35
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
|
||||
}
|
||||
|
155
weighted_dist.go
Normal file
155
weighted_dist.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user