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
// *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,13 +833,23 @@ 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) {
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
}
ret = struct {
rw = struct {
io.Reader
io.Writer
}{
@ -843,20 +857,50 @@ func maybeReceiveEncryptedHandshake(rw io.ReadWriter, skeys mse.SecretKeyIter) (
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 {
rw, err = mse.InitiateHandshake(
struct {
io.Reader
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)
if err != nil {
return
@ -882,9 +926,8 @@ 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)
rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.EncryptionPolicy)
c.setRW(rw)
if err != nil {
if err == mse.ErrNoSecretKeyMatch {
@ -892,8 +935,7 @@ func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) {
}
return
}
}
if cl.config.ForceEncryption && !c.encrypted {
if cl.config.ForceEncryption && !c.headerEncrypted {
err = errors.New("connection not encrypted")
return
}

View File

@ -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()

View File

@ -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
}

View File

@ -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,7 +45,8 @@ type connection struct {
w io.Writer
r io.Reader
// True if the connection is operating over MSE obfuscation.
encrypted bool
headerEncrypted bool
cryptoMethod uint32
Discovery peerSource
uTP bool
closed missinggo.Event
@ -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 {

View File

@ -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

View File

@ -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) {