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

120
client.go
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,34 +833,74 @@ 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(
var protocol [len(pp.Protocol)]byte rw io.ReadWriter,
_, err = io.ReadFull(rw, protocol[:]) skeys mse.SecretKeyIter,
if err != nil { policy EncryptionPolicy,
return ) (
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
}
} }
ret = struct { headerEncrypted = true
io.Reader ret, err = mse.ReceiveHandshake(rw, skeys, func(provides uint32) uint32 {
io.Writer cryptoMethod = func() uint32 {
}{ switch {
io.MultiReader(bytes.NewReader(protocol[:]), rw), case policy.ForceEncryption:
rw, return mse.CryptoMethodRC4
} case policy.DisableEncryption:
if string(protocol[:]) == pp.Protocol { return mse.CryptoMethodPlaintext
return case policy.PreferNoEncryption && provides&mse.CryptoMethodPlaintext != 0:
} return mse.CryptoMethodPlaintext
encrypted = true default:
ret, err = mse.ReceiveHandshakeLazy(ret, skeys) 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(
io.Reader struct {
io.Writer io.Reader
}{c.r, c.w}, t.infoHash[:], nil) 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) c.setRW(rw)
if err != nil { if err != nil {
return return
@ -882,18 +926,16 @@ 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.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.EncryptionPolicy)
rw, c.encrypted, err = maybeReceiveEncryptedHandshake(c.rw(), cl.forSkeys) c.setRW(rw)
c.setRW(rw) if err != nil {
if err != nil { if err == mse.ErrNoSecretKeyMatch {
if err == mse.ErrNoSecretKeyMatch { err = nil
err = nil
}
return
} }
return
} }
if cl.config.ForceEncryption && !c.encrypted { if cl.config.ForceEncryption && !c.headerEncrypted {
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,10 +45,11 @@ 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
Discovery peerSource cryptoMethod uint32
uTP bool Discovery peerSource
closed missinggo.Event uTP bool
closed missinggo.Event
stats ConnStats stats ConnStats
UnwantedChunksReceived int UnwantedChunksReceived int
@ -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) {