From bfca92cf1627c2e380eb44f4314bb121935108ec Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Wed, 27 Aug 2014 12:30:11 +0000 Subject: Various IAT related changes. * Unbreak inbound TYPE_PRNG_SEED processing. * IAT obfuscation is now a per-bridge argument (iat-mode). * 0 (default) = Disabled. * 1 = Enabled, ScrambleSuit-style with bulk throughput optimizations. * 2 = Paranoid, Each IAT write will send a length sampled from the length distribution. (EXPENSIVE). The "iat-mode" argument is mandatory on the Bridge lines, and as a ServerTransportOption. Old statefiles will continue to load and use the default value, edit it if your hat is made of tin foil. --- transports/obfs4/obfs4.go | 86 ++++++++++++++++++++++++++++++++----------- transports/obfs4/statefile.go | 22 ++++++++++- 2 files changed, 85 insertions(+), 23 deletions(-) (limited to 'transports') diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go index f9b02ad..b862e5a 100644 --- a/transports/obfs4/obfs4.go +++ b/transports/obfs4/obfs4.go @@ -36,6 +36,7 @@ import ( "fmt" "math/rand" "net" + "strconv" "syscall" "time" @@ -55,11 +56,11 @@ const ( publicKeyArg = "public-key" privateKeyArg = "private-key" seedArg = "drbg-seed" + iatArg = "iat-mode" - iatCmdArg = "obfs4-iatObfuscation" biasCmdArg = "obfs4-distBias" - seedLength = 32 + seedLength = drbg.SeedLength headerLength = framing.FrameOverhead + packetOverhead clientHandshakeTimeout = time.Duration(60) * time.Second serverHandshakeTimeout = time.Duration(30) * time.Second @@ -70,8 +71,11 @@ const ( maxCloseDelay = 60 ) -// iatObfuscation controls if Inter-Arrival Time obfuscation will be enabled. -var iatObfuscation bool +const ( + iatNone = iota + iatEnabled + iatParanoid +) // biasedDist controls if the probability table will be ScrambleSuit style or // uniformly distributed. @@ -81,6 +85,7 @@ type obfs4ClientArgs struct { nodeID *ntor.NodeID publicKey *ntor.PublicKey sessionKey *ntor.Keypair + iatMode int } // Transport is the obfs4 implementation of the base.Transport interface. @@ -107,7 +112,7 @@ func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFa } var iatSeed *drbg.Seed - if iatObfuscation { + if st.iatMode != iatNone { iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:]) iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]) if err != nil { @@ -119,6 +124,7 @@ func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFa ptArgs := pt.Args{} ptArgs.Add(nodeIDArg, st.nodeID.Hex()) ptArgs.Add(publicKeyArg, st.identityKey.Public().Hex()) + ptArgs.Add(iatArg, strconv.Itoa(st.iatMode)) // Initialize the replay filter. filter, err := replayfilter.New(replayTTL) @@ -133,7 +139,7 @@ func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFa } rng := rand.New(drbg) - sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)} + sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, st.iatMode, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)} return sf, nil } @@ -157,6 +163,15 @@ func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { if nodeID, err = ntor.NodeIDFromHex(nodeIDStr); err != nil { return nil, err } + iatStr, ok := args.Get(iatArg) + if !ok { + return nil, fmt.Errorf("missing argument '%s'", iatArg) + } + var iatMode int + iatMode, err = strconv.Atoi(iatStr) + if err != nil || iatMode < iatNone || iatMode > iatParanoid { + return nil, fmt.Errorf("invalid iat-mode '%d'", iatMode) + } publicKeyStr, ok := args.Get(publicKeyArg) if !ok { @@ -174,7 +189,7 @@ func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { return nil, err } - return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil + return &obfs4ClientArgs{nodeID, publicKey, sessionKey, iatMode}, nil } func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { @@ -194,6 +209,7 @@ type obfs4ServerFactory struct { identityKey *ntor.Keypair lenSeed *drbg.Seed iatSeed *drbg.Seed + iatMode int replayFilter *replayfilter.ReplayFilter closeDelayBytes int @@ -228,7 +244,7 @@ func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist) } - c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + c := &obfs4Conn{conn, true, lenDist, iatDist, sf.iatMode, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} startTime := time.Now() @@ -247,6 +263,7 @@ type obfs4Conn struct { lenDist *probdist.WeightedDist iatDist *probdist.WeightedDist + iatMode int receiveBuffer *bytes.Buffer receiveDecodedBuffer *bytes.Buffer @@ -263,7 +280,7 @@ func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err } lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist) var iatDist *probdist.WeightedDist - if iatObfuscation { + if args.iatMode != iatNone { var iatSeed *drbg.Seed iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil { @@ -273,7 +290,7 @@ func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err } // Allocate the client structure. - c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + c = &obfs4Conn{conn, false, lenDist, iatDist, args.iatMode, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} // Start the handshake timeout. deadline := time.Now().Add(clientHandshakeTimeout) @@ -466,24 +483,51 @@ func (conn *obfs4Conn) Write(b []byte) (n int, err error) { } } - // 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 + if conn.iatMode != iatParanoid { + // For non-paranoid IAT, pad once per burst. Paranoid IAT handles + // things differently. + if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); 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 { + if conn.iatMode != iatNone { var iatFrame [framing.MaximumSegmentLength]byte for frameBuf.Len() > 0 { iatWrLen := 0 - iatWrLen, err = frameBuf.Read(iatFrame[:]) + + switch conn.iatMode { + case iatEnabled: + // Standard (ScrambleSuit-style) IAT obfuscation optimizes for + // bulk transport and will write ~MTU sized frames when + // possible. + iatWrLen, err = frameBuf.Read(iatFrame[:]) + + case iatParanoid: + // Paranoid IAT obfuscation throws performance out of the + // window and will sample the length distribution every time a + // write is scheduled. + targetLen := conn.lenDist.Sample() + if frameBuf.Len() < targetLen { + // There's not enough data buffered for the target write, + // so padding must be inserted. + if err = conn.padBurst(&frameBuf, targetLen); err != nil { + return 0, err + } + if frameBuf.Len() != targetLen { + // Ugh, padding came out to a value that required more + // than one frame, this is relatively unlikely so just + // resample since there's enough data to ensure that + // the next sample will be written. + continue + } + } + iatWrLen, err = frameBuf.Read(iatFrame[:targetLen]) + } if err != nil { return 0, err } else if iatWrLen == 0 { @@ -543,9 +587,8 @@ func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Ti } } -func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) { +func (conn *obfs4Conn) padBurst(burst *bytes.Buffer, toPadTo int) (err error) { tailLen := burst.Len() % framing.MaximumSegmentLength - toPadTo := conn.lenDist.Sample() padLen := 0 if toPadTo >= tailLen { @@ -577,7 +620,6 @@ func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) { } func init() { - flag.BoolVar(&iatObfuscation, iatCmdArg, false, "Enable obfs4 IAT obfuscation (expensive)") flag.BoolVar(&biasedDist, biasCmdArg, false, "Enable obfs4 using ScrambleSuit style table generation") } diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go index 7949dd9..8690178 100644 --- a/transports/obfs4/statefile.go +++ b/transports/obfs4/statefile.go @@ -33,6 +33,7 @@ import ( "io/ioutil" "os" "path" + "strconv" "git.torproject.org/pluggable-transports/goptlib.git" "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" @@ -49,12 +50,14 @@ type jsonServerState struct { PrivateKey string `json:"private-key"` PublicKey string `json:"public-key"` DrbgSeed string `json:"drbg-seed"` + IATMode int `json:"iat-mode"` } type obfs4ServerState struct { nodeID *ntor.NodeID identityKey *ntor.Keypair drbgSeed *drbg.Seed + iatMode int } func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, error) { @@ -64,8 +67,9 @@ func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, err js.NodeID, nodeIDOk = args.Get(nodeIDArg) js.PrivateKey, privKeyOk = args.Get(privateKeyArg) js.DrbgSeed, seedOk = args.Get(seedArg) + iatStr, iatOk := args.Get(iatArg) - if !privKeyOk && !nodeIDOk && !seedOk { + if !privKeyOk && !nodeIDOk && !seedOk && !iatOk { if err := jsonServerStateFromFile(stateDir, &js); err != nil { return nil, err } @@ -75,6 +79,16 @@ func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, err return nil, fmt.Errorf("missing argument '%s'", nodeIDArg) } else if !seedOk { return nil, fmt.Errorf("missing argument '%s'", seedArg) + } else if !iatOk { + // Disable IAT if not specified. + return nil, fmt.Errorf("missing argument '%s'", iatArg) + } else { + // Parse and validate the iat-mode argument. + iatMode, err := strconv.Atoi(iatStr) + if err != nil { + return nil, fmt.Errorf("malformed iat-mode '%s'", iatStr) + } + js.IATMode = iatMode } return serverStateFromJSONServerState(&js) @@ -93,6 +107,10 @@ func serverStateFromJSONServerState(js *jsonServerState) (*obfs4ServerState, err if st.drbgSeed, err = drbg.SeedFromHex(js.DrbgSeed); err != nil { return nil, err } + if js.IATMode < iatNone || js.IATMode > iatParanoid { + return nil, fmt.Errorf("invalid iat-mode '%d'", js.IATMode) + } + st.iatMode = js.IATMode return st, nil } @@ -132,12 +150,14 @@ func newJSONServerState(stateDir string, js *jsonServerState) (err error) { if st.drbgSeed, err = drbg.NewSeed(); err != nil { return } + st.iatMode = iatNone // Encode it into JSON format and write the state file. js.NodeID = st.nodeID.Hex() js.PrivateKey = st.identityKey.Private().Hex() js.PublicKey = st.identityKey.Public().Hex() js.DrbgSeed = st.drbgSeed.Hex() + js.IATMode = st.iatMode var encoded []byte if encoded, err = json.Marshal(js); err != nil { -- cgit v1.2.3