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
// *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
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
}
}
ret = struct {
io.Reader
io.Writer
}{
io.MultiReader(bytes.NewReader(protocol[:]), rw),
rw,
}
if string(protocol[:]) == pp.Protocol {
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
}

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,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 {

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