From: Matt Joiner Date: Wed, 13 Sep 2017 08:20:20 +0000 (+1000) Subject: Support plaintext crypto method for protocol header encryption X-Git-Tag: v1.0.0~400 X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=881f1a7e35f4d738f8ebea6d9c5e78cccc7cebb4;p=btrtrc.git Support plaintext crypto method for protocol header encryption --- diff --git a/client.go b/client.go index 41fa89e2..b87ce4ea 100644 --- a/client.go +++ b/client.go @@ -586,9 +586,9 @@ func (cl *Client) noLongerHalfOpen(t *Torrent, addr string) { // Performs initiator handshakes and returns a connection. Returns nil // *connection if no connection for valid reasons. -func (cl *Client) handshakesConnection(ctx context.Context, nc net.Conn, t *Torrent, encrypted, utp bool) (c *connection, err error) { +func (cl *Client) handshakesConnection(ctx context.Context, nc net.Conn, t *Torrent, encryptHeader, utp bool) (c *connection, err error) { c = cl.newConnection(nc) - c.encrypted = encrypted + c.headerEncrypted = encryptHeader c.uTP = utp ctx, cancel := context.WithTimeout(ctx, handshakesTimeout) defer cancel() @@ -616,8 +616,8 @@ func (cl *Client) establishOutgoingConn(t *Torrent, addr string) (c *connection, if nc == nil { return } - encryptFirst := !cl.config.DisableEncryption && !cl.config.PreferNoEncryption - c, err = cl.handshakesConnection(ctx, nc, t, encryptFirst, utp) + obfuscatedHeaderFirst := !cl.config.DisableEncryption && !cl.config.PreferNoEncryption + c, err = cl.handshakesConnection(ctx, nc, t, obfuscatedHeaderFirst, utp) if err != nil { nc.Close() return @@ -625,8 +625,12 @@ func (cl *Client) establishOutgoingConn(t *Torrent, addr string) (c *connection, return } nc.Close() - if cl.config.DisableEncryption || cl.config.ForceEncryption { - // There's no alternate encryption case to try. + if cl.config.ForceEncryption { + // We should have just tried with an obfuscated header. A plaintext + // header can't result in an encrypted connection, so we're done. + if !obfuscatedHeaderFirst { + panic(cl.config.EncryptionPolicy) + } return } // Try again with encryption if we didn't earlier, or without if we did, @@ -640,7 +644,7 @@ func (cl *Client) establishOutgoingConn(t *Torrent, addr string) (c *connection, err = fmt.Errorf("error dialing for unencrypted connection: %s", err) return } - c, err = cl.handshakesConnection(ctx, nc, t, !encryptFirst, utp) + c, err = cl.handshakesConnection(ctx, nc, t, !obfuscatedHeaderFirst, utp) if err != nil || c == nil { nc.Close() } @@ -829,34 +833,74 @@ func (r deadlineReader) Read(b []byte) (n int, err error) { return } -func maybeReceiveEncryptedHandshake(rw io.ReadWriter, skeys mse.SecretKeyIter) (ret io.ReadWriter, encrypted bool, err error) { - var protocol [len(pp.Protocol)]byte - _, err = io.ReadFull(rw, protocol[:]) - if err != nil { - return - } - ret = struct { - io.Reader - io.Writer - }{ - io.MultiReader(bytes.NewReader(protocol[:]), rw), - rw, - } - if string(protocol[:]) == pp.Protocol { - return +func handleEncryption( + rw io.ReadWriter, + skeys mse.SecretKeyIter, + policy EncryptionPolicy, +) ( + ret io.ReadWriter, + headerEncrypted bool, + cryptoMethod uint32, + err error, +) { + if !policy.ForceEncryption { + var protocol [len(pp.Protocol)]byte + _, err = io.ReadFull(rw, protocol[:]) + if err != nil { + return + } + rw = struct { + io.Reader + io.Writer + }{ + io.MultiReader(bytes.NewReader(protocol[:]), rw), + rw, + } + if string(protocol[:]) == pp.Protocol { + ret = rw + return + } } - encrypted = true - ret, err = mse.ReceiveHandshakeLazy(ret, skeys) + headerEncrypted = true + ret, err = mse.ReceiveHandshake(rw, skeys, func(provides uint32) uint32 { + cryptoMethod = func() uint32 { + switch { + case policy.ForceEncryption: + return mse.CryptoMethodRC4 + case policy.DisableEncryption: + return mse.CryptoMethodPlaintext + case policy.PreferNoEncryption && provides&mse.CryptoMethodPlaintext != 0: + return mse.CryptoMethodPlaintext + default: + return mse.DefaultCryptoSelector(provides) + } + }() + return cryptoMethod + }) return } func (cl *Client) initiateHandshakes(c *connection, t *Torrent) (ok bool, err error) { - if c.encrypted { + if c.headerEncrypted { var rw io.ReadWriter - rw, err = mse.InitiateHandshake(struct { - io.Reader - io.Writer - }{c.r, c.w}, t.infoHash[:], nil) + rw, err = mse.InitiateHandshake( + struct { + io.Reader + io.Writer + }{c.r, c.w}, + t.infoHash[:], + nil, + func() uint32 { + switch { + case cl.config.ForceEncryption: + return mse.CryptoMethodRC4 + case cl.config.DisableEncryption: + return mse.CryptoMethodPlaintext + default: + return mse.AllSupportedCrypto + } + }(), + ) c.setRW(rw) if err != nil { return @@ -882,18 +926,16 @@ func (cl *Client) forSkeys(f func([]byte) bool) { // Do encryption and bittorrent handshakes as receiver. func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) { - if !cl.config.DisableEncryption { - var rw io.ReadWriter - rw, c.encrypted, err = maybeReceiveEncryptedHandshake(c.rw(), cl.forSkeys) - c.setRW(rw) - if err != nil { - if err == mse.ErrNoSecretKeyMatch { - err = nil - } - return + var rw io.ReadWriter + rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.EncryptionPolicy) + c.setRW(rw) + if err != nil { + if err == mse.ErrNoSecretKeyMatch { + err = nil } + return } - if cl.config.ForceEncryption && !c.encrypted { + if cl.config.ForceEncryption && !c.headerEncrypted { err = errors.New("connection not encrypted") return } diff --git a/cmd/torrent/main.go b/cmd/torrent/main.go index bb614264..172c752f 100644 --- a/cmd/torrent/main.go +++ b/cmd/torrent/main.go @@ -23,21 +23,6 @@ import ( "github.com/anacrolix/torrent/storage" ) -func resolvedPeerAddrs(ss []string) (ret []torrent.Peer, err error) { - for _, s := range ss { - var addr *net.TCPAddr - addr, err = net.ResolveTCPAddr("tcp", s) - if err != nil { - return - } - ret = append(ret, torrent.Peer{ - IP: addr.IP, - Port: addr.Port, - }) - } - return -} - func torrentBar(t *torrent.Torrent) { bar := uiprogress.AddBar(1) bar.AppendCompleted() diff --git a/config.go b/config.go index 8ff15b60..ea947de1 100644 --- a/config.go +++ b/config.go @@ -51,12 +51,16 @@ type Config struct { // used. DefaultStorage storage.ClientImpl - DisableEncryption bool `long:"disable-encryption"` - ForceEncryption bool // Don't allow unobfuscated connections. - PreferNoEncryption bool + EncryptionPolicy IPBlocklist iplist.Ranger DisableIPv6 bool `long:"disable-ipv6"` // Perform logging and any other behaviour that will help debug. Debug bool `help:"enable debug logging"` } + +type EncryptionPolicy struct { + DisableEncryption bool + ForceEncryption bool // Don't allow unobfuscated connections. + PreferNoEncryption bool +} diff --git a/connection.go b/connection.go index f519ac6e..dc7b8390 100644 --- a/connection.go +++ b/connection.go @@ -14,6 +14,8 @@ import ( "sync" "time" + "github.com/anacrolix/torrent/mse" + "github.com/anacrolix/missinggo" "github.com/anacrolix/missinggo/bitmap" "github.com/anacrolix/missinggo/iter" @@ -43,10 +45,11 @@ type connection struct { w io.Writer r io.Reader // True if the connection is operating over MSE obfuscation. - encrypted bool - Discovery peerSource - uTP bool - closed missinggo.Event + headerEncrypted bool + cryptoMethod uint32 + Discovery peerSource + uTP bool + closed missinggo.Event stats ConnStats UnwantedChunksReceived int @@ -148,8 +151,10 @@ func (cn *connection) connectionFlags() (ret string) { c := func(b byte) { ret += string([]byte{b}) } - if cn.encrypted { + if cn.cryptoMethod == mse.CryptoMethodRC4 { c('E') + } else if cn.headerEncrypted { + c('e') } ret += string(cn.Discovery) if cn.uTP { diff --git a/mse/mse.go b/mse/mse.go index 8127b815..57fcc058 100644 --- a/mse/mse.go +++ b/mse/mse.go @@ -24,9 +24,9 @@ import ( const ( maxPadLen = 512 - cryptoMethodPlaintext = 1 - cryptoMethodRC4 = 2 - AllSupportedCrypto = cryptoMethodPlaintext | cryptoMethodRC4 + CryptoMethodPlaintext = 1 + CryptoMethodRC4 = 2 + AllSupportedCrypto = CryptoMethodPlaintext | CryptoMethodRC4 ) var ( @@ -409,9 +409,9 @@ func (h *handshake) initerSteps() (ret io.ReadWriter, err error) { return } switch method & h.cryptoProvides { - case cryptoMethodRC4: + case CryptoMethodRC4: ret = readWriter{r, &cipherWriter{e, h.conn, nil}} - case cryptoMethodPlaintext: + case CryptoMethodPlaintext: ret = h.conn default: err = fmt.Errorf("receiver chose unsupported method: %x", method) @@ -482,12 +482,12 @@ func (h *handshake) receiverSteps() (ret io.ReadWriter, err error) { return } switch chosen { - case cryptoMethodRC4: + case CryptoMethodRC4: ret = readWriter{ io.MultiReader(bytes.NewReader(h.ia), r), &cipherWriter{w.c, h.conn, nil}, } - case cryptoMethodPlaintext: + case CryptoMethodPlaintext: ret = readWriter{ io.MultiReader(bytes.NewReader(h.ia), h.conn), h.conn, @@ -538,11 +538,11 @@ func InitiateHandshake(rw io.ReadWriter, skey []byte, initialPayload []byte, cry return h.Do() } -func ReceiveHandshake(rw io.ReadWriter, skeys [][]byte, selectCrypto func(uint32) uint32) (ret io.ReadWriter, err error) { +func ReceiveHandshake(rw io.ReadWriter, skeys SecretKeyIter, selectCrypto func(uint32) uint32) (ret io.ReadWriter, err error) { h := handshake{ conn: rw, initer: false, - skeys: sliceIter(skeys), + skeys: skeys, chooseMethod: selectCrypto, } return h.Do() @@ -562,22 +562,11 @@ func sliceIter(skeys [][]byte) SecretKeyIter { // returns false or exhausted. type SecretKeyIter func(callback func(skey []byte) (more bool)) -// Doesn't unpack the secret keys until it needs to, and through the passed -// function. -func ReceiveHandshakeLazy(rw io.ReadWriter, skeys SecretKeyIter) (ret io.ReadWriter, err error) { - h := handshake{ - conn: rw, - initer: false, - skeys: skeys, - } - return h.Do() -} - func DefaultCryptoSelector(provided uint32) uint32 { - if provided&cryptoMethodRC4 != 0 { - return cryptoMethodRC4 + if provided&CryptoMethodRC4 != 0 { + return CryptoMethodRC4 } - return cryptoMethodPlaintext + return CryptoMethodPlaintext } type CryptoSelector func(uint32) uint32 diff --git a/mse/mse_test.go b/mse/mse_test.go index d9ef7b68..79cabb18 100644 --- a/mse/mse_test.go +++ b/mse/mse_test.go @@ -71,7 +71,7 @@ func handshakeTest(t testing.TB, ia []byte, aData, bData string, cryptoProvides }() go func() { defer wg.Done() - b, err := ReceiveHandshake(b, [][]byte{[]byte("nope"), []byte("yep"), []byte("maybe")}, cryptoSelect) + b, err := ReceiveHandshake(b, sliceIter([][]byte{[]byte("nope"), []byte("yep"), []byte("maybe")}), cryptoSelect) if err != nil { t.Fatal(err) return @@ -103,7 +103,7 @@ func TestHandshakeDefault(t *testing.T) { } func TestHandshakeSelectPlaintext(t *testing.T) { - allHandshakeTests(t, AllSupportedCrypto, func(uint32) uint32 { return cryptoMethodPlaintext }) + allHandshakeTests(t, AllSupportedCrypto, func(uint32) uint32 { return CryptoMethodPlaintext }) } func BenchmarkHandshakeDefault(b *testing.B) { @@ -180,7 +180,7 @@ func benchmarkStream(t *testing.B, crypto uint32) { }() func() { defer bc.Close() - rw, err := ReceiveHandshake(bc, [][]byte{[]byte("cats")}, func(uint32) uint32 { return crypto }) + rw, err := ReceiveHandshake(bc, sliceIter([][]byte{[]byte("cats")}), func(uint32) uint32 { return crypto }) require.NoError(t, err) require.NoError(t, readAndWrite(rw, br, b)) }() @@ -201,11 +201,11 @@ func benchmarkStream(t *testing.B, crypto uint32) { } func BenchmarkStreamRC4(t *testing.B) { - benchmarkStream(t, cryptoMethodRC4) + benchmarkStream(t, CryptoMethodRC4) } func BenchmarkStreamPlaintext(t *testing.B) { - benchmarkStream(t, cryptoMethodPlaintext) + benchmarkStream(t, CryptoMethodPlaintext) } func BenchmarkPipeRC4(t *testing.B) {