Support plaintext crypto method for protocol header encryption

This commit is contained in:
Matt Joiner 2017-09-13 18:20:20 +10:00
parent 29e06fb83c
commit 881f1a7e35
6 changed files with 115 additions and 90 deletions

View File

@ -586,9 +586,9 @@ func (cl *Client) noLongerHalfOpen(t *Torrent, addr string) {
// Performs initiator handshakes and returns a connection. Returns nil // Performs initiator handshakes and returns a connection. Returns nil
// *connection if no connection for valid reasons. // *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 = cl.newConnection(nc)
c.encrypted = encrypted c.headerEncrypted = encryptHeader
c.uTP = utp c.uTP = utp
ctx, cancel := context.WithTimeout(ctx, handshakesTimeout) ctx, cancel := context.WithTimeout(ctx, handshakesTimeout)
defer cancel() defer cancel()
@ -616,8 +616,8 @@ func (cl *Client) establishOutgoingConn(t *Torrent, addr string) (c *connection,
if nc == nil { if nc == nil {
return return
} }
encryptFirst := !cl.config.DisableEncryption && !cl.config.PreferNoEncryption obfuscatedHeaderFirst := !cl.config.DisableEncryption && !cl.config.PreferNoEncryption
c, err = cl.handshakesConnection(ctx, nc, t, encryptFirst, utp) c, err = cl.handshakesConnection(ctx, nc, t, obfuscatedHeaderFirst, utp)
if err != nil { if err != nil {
nc.Close() nc.Close()
return return
@ -625,8 +625,12 @@ func (cl *Client) establishOutgoingConn(t *Torrent, addr string) (c *connection,
return return
} }
nc.Close() nc.Close()
if cl.config.DisableEncryption || cl.config.ForceEncryption { if cl.config.ForceEncryption {
// There's no alternate encryption case to try. // 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 return
} }
// Try again with encryption if we didn't earlier, or without if we did, // 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) err = fmt.Errorf("error dialing for unencrypted connection: %s", err)
return return
} }
c, err = cl.handshakesConnection(ctx, nc, t, !encryptFirst, utp) c, err = cl.handshakesConnection(ctx, nc, t, !obfuscatedHeaderFirst, utp)
if err != nil || c == nil { if err != nil || c == nil {
nc.Close() nc.Close()
} }
@ -829,13 +833,23 @@ func (r deadlineReader) Read(b []byte) (n int, err error) {
return return
} }
func maybeReceiveEncryptedHandshake(rw io.ReadWriter, skeys mse.SecretKeyIter) (ret io.ReadWriter, encrypted bool, err error) { 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 var protocol [len(pp.Protocol)]byte
_, err = io.ReadFull(rw, protocol[:]) _, err = io.ReadFull(rw, protocol[:])
if err != nil { if err != nil {
return return
} }
ret = struct { rw = struct {
io.Reader io.Reader
io.Writer io.Writer
}{ }{
@ -843,20 +857,50 @@ func maybeReceiveEncryptedHandshake(rw io.ReadWriter, skeys mse.SecretKeyIter) (
rw, rw,
} }
if string(protocol[:]) == pp.Protocol { if string(protocol[:]) == pp.Protocol {
ret = rw
return 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 return
} }
func (cl *Client) initiateHandshakes(c *connection, t *Torrent) (ok bool, err error) { func (cl *Client) initiateHandshakes(c *connection, t *Torrent) (ok bool, err error) {
if c.encrypted { if c.headerEncrypted {
var rw io.ReadWriter var rw io.ReadWriter
rw, err = mse.InitiateHandshake(struct { rw, err = mse.InitiateHandshake(
struct {
io.Reader io.Reader
io.Writer io.Writer
}{c.r, c.w}, t.infoHash[:], nil) }{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) c.setRW(rw)
if err != nil { if err != nil {
return return
@ -882,9 +926,8 @@ func (cl *Client) forSkeys(f func([]byte) bool) {
// Do encryption and bittorrent handshakes as receiver. // Do encryption and bittorrent handshakes as receiver.
func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) { func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) {
if !cl.config.DisableEncryption {
var rw io.ReadWriter var rw io.ReadWriter
rw, c.encrypted, err = maybeReceiveEncryptedHandshake(c.rw(), cl.forSkeys) rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.EncryptionPolicy)
c.setRW(rw) c.setRW(rw)
if err != nil { if err != nil {
if err == mse.ErrNoSecretKeyMatch { if err == mse.ErrNoSecretKeyMatch {
@ -892,8 +935,7 @@ func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) {
} }
return return
} }
} if cl.config.ForceEncryption && !c.headerEncrypted {
if cl.config.ForceEncryption && !c.encrypted {
err = errors.New("connection not encrypted") err = errors.New("connection not encrypted")
return return
} }

View File

@ -23,21 +23,6 @@ import (
"github.com/anacrolix/torrent/storage" "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) { func torrentBar(t *torrent.Torrent) {
bar := uiprogress.AddBar(1) bar := uiprogress.AddBar(1)
bar.AppendCompleted() bar.AppendCompleted()

View File

@ -51,12 +51,16 @@ type Config struct {
// used. // used.
DefaultStorage storage.ClientImpl DefaultStorage storage.ClientImpl
DisableEncryption bool `long:"disable-encryption"` EncryptionPolicy
ForceEncryption bool // Don't allow unobfuscated connections.
PreferNoEncryption bool
IPBlocklist iplist.Ranger IPBlocklist iplist.Ranger
DisableIPv6 bool `long:"disable-ipv6"` DisableIPv6 bool `long:"disable-ipv6"`
// Perform logging and any other behaviour that will help debug. // Perform logging and any other behaviour that will help debug.
Debug bool `help:"enable debug logging"` Debug bool `help:"enable debug logging"`
} }
type EncryptionPolicy struct {
DisableEncryption bool
ForceEncryption bool // Don't allow unobfuscated connections.
PreferNoEncryption bool
}

View File

@ -14,6 +14,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/anacrolix/torrent/mse"
"github.com/anacrolix/missinggo" "github.com/anacrolix/missinggo"
"github.com/anacrolix/missinggo/bitmap" "github.com/anacrolix/missinggo/bitmap"
"github.com/anacrolix/missinggo/iter" "github.com/anacrolix/missinggo/iter"
@ -43,7 +45,8 @@ type connection struct {
w io.Writer w io.Writer
r io.Reader r io.Reader
// True if the connection is operating over MSE obfuscation. // True if the connection is operating over MSE obfuscation.
encrypted bool headerEncrypted bool
cryptoMethod uint32
Discovery peerSource Discovery peerSource
uTP bool uTP bool
closed missinggo.Event closed missinggo.Event
@ -148,8 +151,10 @@ func (cn *connection) connectionFlags() (ret string) {
c := func(b byte) { c := func(b byte) {
ret += string([]byte{b}) ret += string([]byte{b})
} }
if cn.encrypted { if cn.cryptoMethod == mse.CryptoMethodRC4 {
c('E') c('E')
} else if cn.headerEncrypted {
c('e')
} }
ret += string(cn.Discovery) ret += string(cn.Discovery)
if cn.uTP { if cn.uTP {

View File

@ -24,9 +24,9 @@ import (
const ( const (
maxPadLen = 512 maxPadLen = 512
cryptoMethodPlaintext = 1 CryptoMethodPlaintext = 1
cryptoMethodRC4 = 2 CryptoMethodRC4 = 2
AllSupportedCrypto = cryptoMethodPlaintext | cryptoMethodRC4 AllSupportedCrypto = CryptoMethodPlaintext | CryptoMethodRC4
) )
var ( var (
@ -409,9 +409,9 @@ func (h *handshake) initerSteps() (ret io.ReadWriter, err error) {
return return
} }
switch method & h.cryptoProvides { switch method & h.cryptoProvides {
case cryptoMethodRC4: case CryptoMethodRC4:
ret = readWriter{r, &cipherWriter{e, h.conn, nil}} ret = readWriter{r, &cipherWriter{e, h.conn, nil}}
case cryptoMethodPlaintext: case CryptoMethodPlaintext:
ret = h.conn ret = h.conn
default: default:
err = fmt.Errorf("receiver chose unsupported method: %x", method) err = fmt.Errorf("receiver chose unsupported method: %x", method)
@ -482,12 +482,12 @@ func (h *handshake) receiverSteps() (ret io.ReadWriter, err error) {
return return
} }
switch chosen { switch chosen {
case cryptoMethodRC4: case CryptoMethodRC4:
ret = readWriter{ ret = readWriter{
io.MultiReader(bytes.NewReader(h.ia), r), io.MultiReader(bytes.NewReader(h.ia), r),
&cipherWriter{w.c, h.conn, nil}, &cipherWriter{w.c, h.conn, nil},
} }
case cryptoMethodPlaintext: case CryptoMethodPlaintext:
ret = readWriter{ ret = readWriter{
io.MultiReader(bytes.NewReader(h.ia), h.conn), io.MultiReader(bytes.NewReader(h.ia), h.conn),
h.conn, h.conn,
@ -538,11 +538,11 @@ func InitiateHandshake(rw io.ReadWriter, skey []byte, initialPayload []byte, cry
return h.Do() 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{ h := handshake{
conn: rw, conn: rw,
initer: false, initer: false,
skeys: sliceIter(skeys), skeys: skeys,
chooseMethod: selectCrypto, chooseMethod: selectCrypto,
} }
return h.Do() return h.Do()
@ -562,22 +562,11 @@ func sliceIter(skeys [][]byte) SecretKeyIter {
// returns false or exhausted. // returns false or exhausted.
type SecretKeyIter func(callback func(skey []byte) (more bool)) 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 { func DefaultCryptoSelector(provided uint32) uint32 {
if provided&cryptoMethodRC4 != 0 { if provided&CryptoMethodRC4 != 0 {
return cryptoMethodRC4 return CryptoMethodRC4
} }
return cryptoMethodPlaintext return CryptoMethodPlaintext
} }
type CryptoSelector func(uint32) uint32 type CryptoSelector func(uint32) uint32

View File

@ -71,7 +71,7 @@ func handshakeTest(t testing.TB, ia []byte, aData, bData string, cryptoProvides
}() }()
go func() { go func() {
defer wg.Done() 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
return return
@ -103,7 +103,7 @@ func TestHandshakeDefault(t *testing.T) {
} }
func TestHandshakeSelectPlaintext(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) { func BenchmarkHandshakeDefault(b *testing.B) {
@ -180,7 +180,7 @@ func benchmarkStream(t *testing.B, crypto uint32) {
}() }()
func() { func() {
defer bc.Close() 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, err)
require.NoError(t, readAndWrite(rw, br, b)) require.NoError(t, readAndWrite(rw, br, b))
}() }()
@ -201,11 +201,11 @@ func benchmarkStream(t *testing.B, crypto uint32) {
} }
func BenchmarkStreamRC4(t *testing.B) { func BenchmarkStreamRC4(t *testing.B) {
benchmarkStream(t, cryptoMethodRC4) benchmarkStream(t, CryptoMethodRC4)
} }
func BenchmarkStreamPlaintext(t *testing.B) { func BenchmarkStreamPlaintext(t *testing.B) {
benchmarkStream(t, cryptoMethodPlaintext) benchmarkStream(t, CryptoMethodPlaintext)
} }
func BenchmarkPipeRC4(t *testing.B) { func BenchmarkPipeRC4(t *testing.B) {