return t.dialTimeout()
}())
defer cancel()
- obfuscatedHeaderFirst := !cl.config.DisableEncryption && !cl.config.PreferNoEncryption
+ obfuscatedHeaderFirst := cl.config.HeaderObfuscationPolicy.Preferred
c, err = cl.establishOutgoingConnEx(t, addr, ctx, obfuscatedHeaderFirst)
if err != nil {
+ //cl.logger.Printf("error establish connection to %s (obfuscatedHeader=%t): %v", addr, obfuscatedHeaderFirst, err)
return
}
if c != nil {
torrent.Add("initiated conn with preferred header obfuscation", 1)
return
}
- 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)
- }
+ if cl.config.HeaderObfuscationPolicy.RequirePreferred {
+ // We should have just tried with the preferred header obfuscation. If it was required,
+ // there's nothing else to try.
return
}
// Try again with encryption if we didn't earlier, or without if we did.
}{c.r, c.w},
t.infoHash[:],
nil,
- func() mse.CryptoMethod {
- switch {
- case cl.config.ForceEncryption:
- return mse.CryptoMethodRC4
- case cl.config.DisableEncryption:
- return mse.CryptoMethodPlaintext
- default:
- return mse.AllSupportedCrypto
- }
- }(),
+ cl.config.CryptoProvides,
)
c.setRW(rw)
if err != nil {
func (cl *Client) receiveHandshakes(c *connection) (t *Torrent, err error) {
defer perf.ScopeTimerErr(&err)()
var rw io.ReadWriter
- rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.EncryptionPolicy)
+ rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.forSkeys, cl.config.HeaderObfuscationPolicy, cl.config.CryptoSelector)
c.setRW(rw)
if err == nil || err == mse.ErrNoSecretKeyMatch {
if c.headerEncrypted {
}
return
}
- if cl.config.ForceEncryption && !c.headerEncrypted {
- err = errors.New("connection not encrypted")
+ if cl.config.HeaderObfuscationPolicy.RequirePreferred && c.headerEncrypted != cl.config.HeaderObfuscationPolicy.Preferred {
+ err = errors.New("connection not have required header obfuscation")
return
}
ih, ok, err := cl.connBTHandshake(c, nil)
V: cl.config.ExtendedHandshakeClientVersion,
Reqq: 64, // TODO: Really?
YourIp: pp.CompactIp(conn.remoteAddr.IP),
- Encryption: !cl.config.DisableEncryption,
+ Encryption: cl.config.HeaderObfuscationPolicy.Preferred || !cl.config.HeaderObfuscationPolicy.RequirePreferred,
Port: cl.incomingPeerPort(),
MetadataSize: torrent.metadataSize(),
// TODO: We can figured these out specific to the socket
"testing"
"time"
+ "github.com/bradfitz/iter"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/time/rate"
+
"github.com/anacrolix/dht"
_ "github.com/anacrolix/envpprof"
"github.com/anacrolix/missinggo"
"github.com/anacrolix/torrent/iplist"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
- "github.com/bradfitz/iter"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/time/rate"
)
func TestingConfig() *ClientConfig {
// https://github.com/anacrolix/torrent/issues/114
func TestMultipleTorrentsWithEncryption(t *testing.T) {
+ testSeederLeecherPair(
+ t,
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.Preferred = true
+ cfg.HeaderObfuscationPolicy.RequirePreferred = true
+ },
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.RequirePreferred = false
+ },
+ )
+}
+
+// Test that the leecher can download a torrent in its entirety from the seeder. Note that the
+// seeder config is done first.
+func testSeederLeecherPair(t *testing.T, seeder func(*ClientConfig), leecher func(*ClientConfig)) {
cfg := TestingConfig()
- cfg.DisableUTP = true
cfg.Seed = true
cfg.DataDir = filepath.Join(cfg.DataDir, "server")
- cfg.ForceEncryption = true
os.Mkdir(cfg.DataDir, 0755)
+ seeder(cfg)
server, err := NewClient(cfg)
require.NoError(t, err)
defer server.Close()
defer testutil.ExportStatusWriter(server, "s")()
magnet1 := makeMagnet(t, server, cfg.DataDir, "test1")
+ // Extra torrents are added to test the seeder having to match incoming obfuscated headers
+ // against more than one torrent. See issue #114
makeMagnet(t, server, cfg.DataDir, "test2")
+ for i := 0; i < 100; i++ {
+ makeMagnet(t, server, cfg.DataDir, fmt.Sprintf("test%d", i+2))
+ }
cfg = TestingConfig()
- cfg.DisableUTP = true
cfg.DataDir = filepath.Join(cfg.DataDir, "client")
- cfg.ForceEncryption = true
+ leecher(cfg)
client, err := NewClient(cfg)
require.NoError(t, err)
defer client.Close()
client.WaitAll()
}
+// This appears to be the situation with the S3 BitTorrent client.
+func TestObfuscatedHeaderFallbackSeederDisallowsLeecherPrefers(t *testing.T) {
+ // Leecher prefers obfuscation, but the seeder does not allow it.
+ testSeederLeecherPair(
+ t,
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.Preferred = false
+ cfg.HeaderObfuscationPolicy.RequirePreferred = true
+ },
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.Preferred = true
+ cfg.HeaderObfuscationPolicy.RequirePreferred = false
+ },
+ )
+}
+
+func TestObfuscatedHeaderFallbackSeederRequiresLeecherPrefersNot(t *testing.T) {
+ // Leecher prefers no obfuscation, but the seeder enforces it.
+ testSeederLeecherPair(
+ t,
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.Preferred = true
+ cfg.HeaderObfuscationPolicy.RequirePreferred = true
+ },
+ func(cfg *ClientConfig) {
+ cfg.HeaderObfuscationPolicy.Preferred = false
+ cfg.HeaderObfuscationPolicy.RequirePreferred = false
+ },
+ )
+}
+
func TestClientAddressInUse(t *testing.T) {
s, _ := NewUtpSocket("udp", ":50007", nil)
if s != nil {
"github.com/anacrolix/missinggo/conntrack"
"github.com/anacrolix/missinggo/expect"
"github.com/anacrolix/torrent/iplist"
+ "github.com/anacrolix/torrent/mse"
"github.com/anacrolix/torrent/storage"
"golang.org/x/time/rate"
)
// used.
DefaultStorage storage.ClientImpl
- EncryptionPolicy
+ HeaderObfuscationPolicy HeaderObfuscationPolicy
+ CryptoProvides mse.CryptoMethod
+ CryptoSelector mse.CryptoSelector
// Sets usage of Socks5 Proxy. Authentication should be included in the url if needed.
// Examples: socks5://demo:demo@192.168.99.100:1080
UploadRateLimiter: unlimited,
DownloadRateLimiter: unlimited,
ConnTracker: conntrack.NewInstance(),
+ HeaderObfuscationPolicy: HeaderObfuscationPolicy{
+ Preferred: true,
+ RequirePreferred: false,
+ },
+ CryptoSelector: mse.DefaultCryptoSelector,
+ CryptoProvides: mse.AllSupportedCrypto,
}
cc.ConnTracker.SetNoMaxEntries()
cc.ConnTracker.Timeout = func(conntrack.Entry) time.Duration { return 0 }
return cc
}
-type EncryptionPolicy struct {
- DisableEncryption bool
- ForceEncryption bool // Don't allow unobfuscated connections.
- PreferNoEncryption bool
+type HeaderObfuscationPolicy struct {
+ RequirePreferred bool // Whether the value of Preferred is a strict requirement
+ Preferred bool // Whether header obfuscation is preferred
}
func handleEncryption(
rw io.ReadWriter,
skeys mse.SecretKeyIter,
- policy EncryptionPolicy,
+ policy HeaderObfuscationPolicy,
+ selector mse.CryptoSelector,
) (
ret io.ReadWriter,
headerEncrypted bool,
cryptoMethod mse.CryptoMethod,
err error,
) {
- if !policy.ForceEncryption {
+ if !policy.RequirePreferred || !policy.Preferred {
var protocol [len(pp.Protocol)]byte
_, err = io.ReadFull(rw, protocol[:])
if err != nil {
ret = rw
return
}
+ if policy.RequirePreferred {
+ err = fmt.Errorf("unexpected protocol string %q and header obfuscation disabled", protocol)
+ return
+ }
}
headerEncrypted = true
- ret, cryptoMethod, err = mse.ReceiveHandshake(rw, skeys, func(provides mse.CryptoMethod) mse.CryptoMethod {
- 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)
- }
- })
+ ret, cryptoMethod, err = mse.ReceiveHandshake(rw, skeys, selector)
return
}