]> Sergey Matveev's repositories - btrtrc.git/blobdiff - client_test.go
Also change go requirement for fs module
[btrtrc.git] / client_test.go
index 2fedb5a952bc52a8db90cfa98b1cd118385bf580..d2a88e9e7a769abcd1871092c006e37b33573a51 100644 (file)
@@ -2,58 +2,51 @@ package torrent
 
 import (
        "encoding/binary"
-       "errors"
        "fmt"
        "io"
-       "io/ioutil"
-       "log"
-       "math/rand"
        "net"
+       "net/netip"
        "os"
-       "strings"
-       "sync"
+       "path/filepath"
+       "reflect"
        "testing"
+       "testing/iotest"
        "time"
 
-       _ "github.com/anacrolix/envpprof"
-       "github.com/anacrolix/missinggo"
-       "github.com/anacrolix/missinggo/filecache"
-       "github.com/anacrolix/missinggo/pubsub"
-       "github.com/anacrolix/utp"
-       "github.com/bradfitz/iter"
+       "github.com/anacrolix/dht/v2"
+       "github.com/anacrolix/log"
+       "github.com/anacrolix/missinggo/v2"
+       "github.com/anacrolix/missinggo/v2/filecache"
+       "github.com/frankban/quicktest"
+       qt "github.com/frankban/quicktest"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 
        "github.com/anacrolix/torrent/bencode"
-       "github.com/anacrolix/torrent/dht"
        "github.com/anacrolix/torrent/internal/testutil"
        "github.com/anacrolix/torrent/iplist"
        "github.com/anacrolix/torrent/metainfo"
        "github.com/anacrolix/torrent/storage"
 )
 
-func init() {
-       log.SetFlags(log.LstdFlags | log.Llongfile)
-}
-
-var TestingConfig = Config{
-       ListenAddr:      "localhost:0",
-       NoDHT:           true,
-       DisableTrackers: true,
-       DataDir:         "/dev/null",
-       DHTConfig: dht.ServerConfig{
-               NoDefaultBootstrap: true,
-       },
+func TestClientDefault(t *testing.T) {
+       cl, err := NewClient(TestingConfig(t))
+       require.NoError(t, err)
+       require.Empty(t, cl.Close())
 }
 
-func TestClientDefault(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+func TestClientNilConfig(t *testing.T) {
+       // The default config will put crap in the working directory.
+       origDir, _ := os.Getwd()
+       defer os.Chdir(origDir)
+       os.Chdir(t.TempDir())
+       cl, err := NewClient(nil)
        require.NoError(t, err)
-       cl.Close()
+       require.Empty(t, cl.Close())
 }
 
 func TestAddDropTorrent(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        dir, mi := testutil.GreetingTestTorrent()
@@ -61,79 +54,72 @@ func TestAddDropTorrent(t *testing.T) {
        tt, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
        require.NoError(t, err)
        assert.True(t, new)
+       tt.SetMaxEstablishedConns(0)
+       tt.SetMaxEstablishedConns(1)
        tt.Drop()
 }
 
 func TestAddTorrentNoSupportedTrackerSchemes(t *testing.T) {
+       // TODO?
        t.SkipNow()
 }
 
 func TestAddTorrentNoUsableURLs(t *testing.T) {
+       // TODO?
        t.SkipNow()
 }
 
 func TestAddPeersToUnknownTorrent(t *testing.T) {
+       // TODO?
        t.SkipNow()
 }
 
 func TestPieceHashSize(t *testing.T) {
-       if pieceHash.Size() != 20 {
-               t.FailNow()
-       }
+       assert.Equal(t, 20, pieceHash.Size())
 }
 
 func TestTorrentInitialState(t *testing.T) {
        dir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(dir)
-       tor := &Torrent{
-               infoHash:          mi.Info.Hash(),
-               pieceStateChanges: pubsub.NewPubSub(),
-       }
-       tor.chunkSize = 2
-       tor.storageOpener = storage.NewFile(dir)
-       // Needed to lock for asynchronous piece verification.
-       tor.cl = new(Client)
-       err := tor.setMetadata(&mi.Info.Info, mi.Info.Bytes)
+       var cl Client
+       cl.init(TestingConfig(t))
+       cl.initLogger()
+       tor := cl.newTorrent(
+               mi.HashInfoBytes(),
+               storage.NewFileWithCompletion(t.TempDir(), storage.NewMapPieceCompletion()),
+       )
+       tor.setChunkSize(2)
+       tor.cl.lock()
+       err := tor.setInfoBytesLocked(mi.InfoBytes)
+       tor.cl.unlock()
        require.NoError(t, err)
        require.Len(t, tor.pieces, 3)
        tor.pendAllChunkSpecs(0)
-       tor.cl.mu.Lock()
+       tor.cl.lock()
        assert.EqualValues(t, 3, tor.pieceNumPendingChunks(0))
-       tor.cl.mu.Unlock()
-       assert.EqualValues(t, chunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize))
-}
-
-func TestUnmarshalPEXMsg(t *testing.T) {
-       var m peerExchangeMessage
-       if err := bencode.Unmarshal([]byte("d5:added12:\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0ce"), &m); err != nil {
-               t.Fatal(err)
-       }
-       if len(m.Added) != 2 {
-               t.FailNow()
-       }
-       if m.Added[0].Port != 0x506 {
-               t.FailNow()
-       }
+       tor.cl.unlock()
+       assert.EqualValues(t, ChunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize))
 }
 
 func TestReducedDialTimeout(t *testing.T) {
+       cfg := NewDefaultClientConfig()
        for _, _case := range []struct {
                Max             time.Duration
                HalfOpenLimit   int
                PendingPeers    int
                ExpectedReduced time.Duration
        }{
-               {nominalDialTimeout, 40, 0, nominalDialTimeout},
-               {nominalDialTimeout, 40, 1, nominalDialTimeout},
-               {nominalDialTimeout, 40, 39, nominalDialTimeout},
-               {nominalDialTimeout, 40, 40, nominalDialTimeout / 2},
-               {nominalDialTimeout, 40, 80, nominalDialTimeout / 3},
-               {nominalDialTimeout, 40, 4000, nominalDialTimeout / 101},
+               {cfg.NominalDialTimeout, 40, 0, cfg.NominalDialTimeout},
+               {cfg.NominalDialTimeout, 40, 1, cfg.NominalDialTimeout},
+               {cfg.NominalDialTimeout, 40, 39, cfg.NominalDialTimeout},
+               {cfg.NominalDialTimeout, 40, 40, cfg.NominalDialTimeout / 2},
+               {cfg.NominalDialTimeout, 40, 80, cfg.NominalDialTimeout / 3},
+               {cfg.NominalDialTimeout, 40, 4000, cfg.NominalDialTimeout / 101},
        } {
-               reduced := reducedDialTimeout(_case.Max, _case.HalfOpenLimit, _case.PendingPeers)
+               reduced := reducedDialTimeout(cfg.MinDialTimeout, _case.Max, _case.HalfOpenLimit, _case.PendingPeers)
                expected := _case.ExpectedReduced
-               if expected < minDialTimeout {
-                       expected = minDialTimeout
+               if expected < cfg.MinDialTimeout {
+                       expected = cfg.MinDialTimeout
                }
                if reduced != expected {
                        t.Fatalf("expected %s, got %s", _case.ExpectedReduced, reduced)
@@ -141,92 +127,11 @@ func TestReducedDialTimeout(t *testing.T) {
        }
 }
 
-func TestUTPRawConn(t *testing.T) {
-       l, err := utp.NewSocket("udp", "")
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer l.Close()
-       go func() {
-               for {
-                       _, err := l.Accept()
-                       if err != nil {
-                               break
-                       }
-               }
-       }()
-       // Connect a UTP peer to see if the RawConn will still work.
-       s, _ := utp.NewSocket("udp", "")
-       defer s.Close()
-       utpPeer, err := s.Dial(fmt.Sprintf("localhost:%d", missinggo.AddrPort(l.Addr())))
-       if err != nil {
-               t.Fatalf("error dialing utp listener: %s", err)
-       }
-       defer utpPeer.Close()
-       peer, err := net.ListenPacket("udp", ":0")
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer peer.Close()
-
-       msgsReceived := 0
-       // How many messages to send. I've set this to double the channel buffer
-       // size in the raw packetConn.
-       const N = 200
-       readerStopped := make(chan struct{})
-       // The reader goroutine.
-       go func() {
-               defer close(readerStopped)
-               b := make([]byte, 500)
-               for i := 0; i < N; i++ {
-                       n, _, err := l.ReadFrom(b)
-                       if err != nil {
-                               t.Fatalf("error reading from raw conn: %s", err)
-                       }
-                       msgsReceived++
-                       var d int
-                       fmt.Sscan(string(b[:n]), &d)
-                       if d != i {
-                               log.Printf("got wrong number: expected %d, got %d", i, d)
-                       }
-               }
-       }()
-       udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("localhost:%d", missinggo.AddrPort(l.Addr())))
-       if err != nil {
-               t.Fatal(err)
-       }
-       for i := 0; i < N; i++ {
-               _, err := peer.WriteTo([]byte(fmt.Sprintf("%d", i)), udpAddr)
-               if err != nil {
-                       t.Fatal(err)
-               }
-               time.Sleep(time.Microsecond)
-       }
-       select {
-       case <-readerStopped:
-       case <-time.After(time.Second):
-               t.Fatal("reader timed out")
-       }
-       if msgsReceived != N {
-               t.Fatalf("messages received: %d", msgsReceived)
-       }
-}
-
-func TestTwoClientsArbitraryPorts(t *testing.T) {
-       for i := 0; i < 2; i++ {
-               cl, err := NewClient(&TestingConfig)
-               if err != nil {
-                       t.Fatal(err)
-               }
-               defer cl.Close()
-       }
-}
-
 func TestAddDropManyTorrents(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
-       for i := range iter.N(1000) {
+       for i := 0; i < 1000; i += 1 {
                var spec TorrentSpec
                binary.PutVarint(spec.InfoHash[:], int64(i))
                tt, new, err := cl.AddTorrentSpec(&spec)
@@ -236,196 +141,17 @@ func TestAddDropManyTorrents(t *testing.T) {
        }
 }
 
-func TestClientTransferDefault(t *testing.T) {
-       testClientTransfer(t, testClientTransferParams{
-               ExportClientStatus: true,
-       })
-}
-
-func TestClientTransferSmallCache(t *testing.T) {
-       testClientTransfer(t, testClientTransferParams{
-               SetLeecherStorageCapacity: true,
-               // Going below the piece length means it can't complete a piece so
-               // that it can be hashed.
-               LeecherStorageCapacity: 5,
-               SetReadahead:           true,
-               // Can't readahead too far or the cache will thrash and drop data we
-               // thought we had.
-               Readahead:          0,
-               ExportClientStatus: true,
-       })
-}
-
-func TestClientTransferVarious(t *testing.T) {
-       for _, ss := range []func(string) storage.I{
-               storage.NewFile,
-               storage.NewMMap,
-       } {
-               for _, responsive := range []bool{false, true} {
-                       testClientTransfer(t, testClientTransferParams{
-                               Responsive:    responsive,
-                               SeederStorage: ss,
-                       })
-                       for _, readahead := range []int64{-1, 0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 20} {
-                               testClientTransfer(t, testClientTransferParams{
-                                       SeederStorage: ss,
-                                       Responsive:    responsive,
-                                       SetReadahead:  true,
-                                       Readahead:     readahead,
-                               })
-                       }
-               }
-       }
-}
-
-type testClientTransferParams struct {
-       Responsive                bool
-       Readahead                 int64
-       SetReadahead              bool
-       ExportClientStatus        bool
-       SetLeecherStorageCapacity bool
-       LeecherStorageCapacity    int64
-       SeederStorage             func(string) storage.I
-}
-
-func testClientTransfer(t *testing.T, ps testClientTransferParams) {
-       greetingTempDir, mi := testutil.GreetingTestTorrent()
-       defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig
-       cfg.Seed = true
-       if ps.SeederStorage != nil {
-               cfg.DefaultStorage = ps.SeederStorage(greetingTempDir)
-       } else {
-               cfg.DataDir = greetingTempDir
-       }
-       seeder, err := NewClient(&cfg)
-       require.NoError(t, err)
-       defer seeder.Close()
-       if ps.ExportClientStatus {
-               testutil.ExportStatusWriter(seeder, "s")
-       }
-       _, new, err := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       require.NoError(t, err)
-       assert.True(t, new)
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       fc, err := filecache.NewCache(leecherDataDir)
-       require.NoError(t, err)
-       if ps.SetLeecherStorageCapacity {
-               fc.SetCapacity(ps.LeecherStorageCapacity)
-       }
-       cfg.DefaultStorage = storage.NewPieceFileStorage(fc.AsFileStore())
-       leecher, err := NewClient(&cfg)
-       require.NoError(t, err)
-       defer leecher.Close()
-       if ps.ExportClientStatus {
-               testutil.ExportStatusWriter(leecher, "l")
-       }
-       leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 2
-               ret.Storage = storage.NewFile(leecherDataDir)
-               return
-       }())
-       require.NoError(t, err)
-       assert.True(t, new)
-       leecherGreeting.AddPeers([]Peer{
-               Peer{
-                       IP:   missinggo.AddrIP(seeder.ListenAddr()),
-                       Port: missinggo.AddrPort(seeder.ListenAddr()),
-               },
-       })
-       r := leecherGreeting.NewReader()
-       defer r.Close()
-       if ps.Responsive {
-               r.SetResponsive()
-       }
-       if ps.SetReadahead {
-               r.SetReadahead(ps.Readahead)
-       }
-       for range iter.N(2) {
-               pos, err := r.Seek(0, os.SEEK_SET)
-               assert.NoError(t, err)
-               assert.EqualValues(t, 0, pos)
-               _greeting, err := ioutil.ReadAll(r)
-               assert.NoError(t, err)
-               assert.EqualValues(t, testutil.GreetingFileContents, _greeting)
-       }
-}
-
-// Check that after completing leeching, a leecher transitions to a seeding
-// correctly. Connected in a chain like so: Seeder <-> Leecher <-> LeecherLeecher.
-func TestSeedAfterDownloading(t *testing.T) {
-       greetingTempDir, mi := testutil.GreetingTestTorrent()
-       defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig
-       cfg.Seed = true
-       cfg.DataDir = greetingTempDir
-       seeder, err := NewClient(&cfg)
-       require.NoError(t, err)
-       defer seeder.Close()
-       testutil.ExportStatusWriter(seeder, "s")
-       seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       cfg.DataDir, err = ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(cfg.DataDir)
-       leecher, err := NewClient(&cfg)
-       require.NoError(t, err)
-       defer leecher.Close()
-       testutil.ExportStatusWriter(leecher, "l")
-       cfg.Seed = false
-       // cfg.TorrentDataOpener = nil
-       cfg.DataDir, err = ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(cfg.DataDir)
-       leecherLeecher, _ := NewClient(&cfg)
-       defer leecherLeecher.Close()
-       testutil.ExportStatusWriter(leecherLeecher, "ll")
-       leecherGreeting, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 2
-               return
-       }())
-       llg, _, _ := leecherLeecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 3
-               return
-       }())
-       // Simultaneously DownloadAll in Leecher, and read the contents
-       // consecutively in LeecherLeecher. This non-deterministically triggered a
-       // case where the leecher wouldn't unchoke the LeecherLeecher.
-       var wg sync.WaitGroup
-       wg.Add(1)
-       go func() {
-               defer wg.Done()
-               r := llg.NewReader()
-               defer r.Close()
-               b, err := ioutil.ReadAll(r)
-               require.NoError(t, err)
-               assert.EqualValues(t, testutil.GreetingFileContents, b)
-       }()
-       leecherGreeting.AddPeers([]Peer{
-               Peer{
-                       IP:   missinggo.AddrIP(seeder.ListenAddr()),
-                       Port: missinggo.AddrPort(seeder.ListenAddr()),
-               },
-               Peer{
-                       IP:   missinggo.AddrIP(leecherLeecher.ListenAddr()),
-                       Port: missinggo.AddrPort(leecherLeecher.ListenAddr()),
+func fileCachePieceResourceStorage(fc *filecache.Cache) storage.ClientImpl {
+       return storage.NewResourcePiecesOpts(
+               fc.AsResourceProvider(),
+               storage.ResourcePiecesOpts{
+                       LeaveIncompleteChunks: true,
                },
-       })
-       wg.Add(1)
-       go func() {
-               defer wg.Done()
-               leecherGreeting.DownloadAll()
-               leecher.WaitAll()
-       }()
-       wg.Wait()
+       )
 }
 
 func TestMergingTrackersByAddingSpecs(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        spec := TorrentSpec{}
@@ -435,87 +161,49 @@ func TestMergingTrackersByAddingSpecs(t *testing.T) {
        }
        spec.Trackers = [][]string{{"http://a"}, {"udp://b"}}
        _, new, _ = cl.AddTorrentSpec(&spec)
-       if new {
-               t.FailNow()
-       }
-       assert.EqualValues(t, T.trackers[0][0], "http://a")
-       assert.EqualValues(t, T.trackers[1][0], "udp://b")
-}
-
-type badStorage struct{}
-
-func (bs badStorage) OpenTorrent(*metainfo.InfoEx) (storage.Torrent, error) {
-       return bs, nil
-}
-
-func (bs badStorage) Close() error {
-       return nil
-}
-
-func (bs badStorage) Piece(p metainfo.Piece) storage.Piece {
-       return badStoragePiece{p}
-}
-
-type badStoragePiece struct {
-       p metainfo.Piece
-}
-
-func (p badStoragePiece) WriteAt(b []byte, off int64) (int, error) {
-       return 0, nil
-}
-
-func (p badStoragePiece) GetIsComplete() bool {
-       return true
-}
-
-func (p badStoragePiece) MarkComplete() error {
-       return errors.New("psyyyyyyyche")
-}
-
-func (p badStoragePiece) randomlyTruncatedDataString() string {
-       return "hello, world\n"[:rand.Intn(14)]
-}
-
-func (p badStoragePiece) ReadAt(b []byte, off int64) (n int, err error) {
-       r := strings.NewReader(p.randomlyTruncatedDataString())
-       return r.ReadAt(b, off+p.p.Offset())
+       assert.False(t, new)
+       assert.EqualValues(t, [][]string{{"http://a"}, {"udp://b"}}, T.metainfo.AnnounceList)
+       // Because trackers are disabled in TestingConfig.
+       assert.EqualValues(t, 0, len(T.trackerAnnouncers))
 }
 
 // We read from a piece which is marked completed, but is missing data.
 func TestCompletedPieceWrongSize(t *testing.T) {
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.DefaultStorage = badStorage{}
-       cl, _ := NewClient(&cfg)
+       cl, err := NewClient(cfg)
+       require.NoError(t, err)
        defer cl.Close()
-       tt, new, err := cl.AddTorrentSpec(&TorrentSpec{
-               Info: &metainfo.InfoEx{
-                       Info: metainfo.Info{
-                               PieceLength: 15,
-                               Pieces:      make([]byte, 20),
-                               Files: []metainfo.FileInfo{
-                                       metainfo.FileInfo{Path: []string{"greeting"}, Length: 13},
-                               },
-                       },
+       info := metainfo.Info{
+               PieceLength: 15,
+               Pieces:      make([]byte, 20),
+               Files: []metainfo.FileInfo{
+                       {Path: []string{"greeting"}, Length: 13},
                },
+       }
+       b, err := bencode.Marshal(info)
+       require.NoError(t, err)
+       tt, new, err := cl.AddTorrentSpec(&TorrentSpec{
+               InfoBytes: b,
+               InfoHash:  metainfo.HashBytes(b),
        })
        require.NoError(t, err)
        defer tt.Drop()
        assert.True(t, new)
        r := tt.NewReader()
        defer r.Close()
-       b, err := ioutil.ReadAll(r)
-       assert.Len(t, b, 13)
-       assert.NoError(t, err)
+       quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
 }
 
 func BenchmarkAddLargeTorrent(b *testing.B) {
-       cfg := TestingConfig
+       cfg := TestingConfig(b)
        cfg.DisableTCP = true
        cfg.DisableUTP = true
-       cfg.ListenAddr = "redonk"
-       cl, _ := NewClient(&cfg)
+       cl, err := NewClient(cfg)
+       require.NoError(b, err)
        defer cl.Close()
-       for range iter.N(b.N) {
+       b.ReportAllocs()
+       for i := 0; i < b.N; i += 1 {
                t, err := cl.AddTorrentFromFile("testdata/bootstrap.dat.torrent")
                if err != nil {
                        b.Fatal(err)
@@ -527,19 +215,62 @@ func BenchmarkAddLargeTorrent(b *testing.B) {
 func TestResponsive(t *testing.T) {
        seederDataDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(seederDataDir)
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = seederDataDir
-       seeder, err := NewClient(&cfg)
+       seeder, err := NewClient(cfg)
        require.Nil(t, err)
        defer seeder.Close()
-       seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       leecherDataDir, err := ioutil.TempDir("", "")
+       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
+       seederTorrent.VerifyData()
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
+       cfg.DataDir = leecherDataDir
+       leecher, err := NewClient(cfg)
        require.Nil(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       cfg = TestingConfig
+       defer leecher.Close()
+       leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
+               ret = TorrentSpecFromMetaInfo(mi)
+               ret.ChunkSize = 2
+               return
+       }())
+       leecherTorrent.AddClientPeer(seeder)
+       reader := leecherTorrent.NewReader()
+       defer reader.Close()
+       reader.SetReadahead(0)
+       reader.SetResponsive()
+       b := make([]byte, 2)
+       _, err = reader.Seek(3, io.SeekStart)
+       require.NoError(t, err)
+       _, err = io.ReadFull(reader, b)
+       assert.Nil(t, err)
+       assert.EqualValues(t, "lo", string(b))
+       _, err = reader.Seek(11, io.SeekStart)
+       require.NoError(t, err)
+       n, err := io.ReadFull(reader, b)
+       assert.Nil(t, err)
+       assert.EqualValues(t, 2, n)
+       assert.EqualValues(t, "d\n", string(b))
+}
+
+// TestResponsive was the first test to fail if uTP is disabled and TCP sockets dial from the
+// listening port.
+func TestResponsiveTcpOnly(t *testing.T) {
+       seederDataDir, mi := testutil.GreetingTestTorrent()
+       defer os.RemoveAll(seederDataDir)
+       cfg := TestingConfig(t)
+       cfg.DisableUTP = true
+       cfg.Seed = true
+       cfg.DataDir = seederDataDir
+       seeder, err := NewClient(cfg)
+       require.Nil(t, err)
+       defer seeder.Close()
+       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
+       seederTorrent.VerifyData()
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
        cfg.DataDir = leecherDataDir
-       leecher, err := NewClient(&cfg)
+       leecher, err := NewClient(cfg)
        require.Nil(t, err)
        defer leecher.Close()
        leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
@@ -547,23 +278,18 @@ func TestResponsive(t *testing.T) {
                ret.ChunkSize = 2
                return
        }())
-       leecherTorrent.AddPeers([]Peer{
-               Peer{
-                       IP:   missinggo.AddrIP(seeder.ListenAddr()),
-                       Port: missinggo.AddrPort(seeder.ListenAddr()),
-               },
-       })
+       leecherTorrent.AddClientPeer(seeder)
        reader := leecherTorrent.NewReader()
        defer reader.Close()
        reader.SetReadahead(0)
        reader.SetResponsive()
        b := make([]byte, 2)
-       _, err = reader.Seek(3, os.SEEK_SET)
+       _, err = reader.Seek(3, io.SeekStart)
        require.NoError(t, err)
        _, err = io.ReadFull(reader, b)
        assert.Nil(t, err)
        assert.EqualValues(t, "lo", string(b))
-       _, err = reader.Seek(11, os.SEEK_SET)
+       _, err = reader.Seek(11, io.SeekStart)
        require.NoError(t, err)
        n, err := io.ReadFull(reader, b)
        assert.Nil(t, err)
@@ -574,19 +300,18 @@ func TestResponsive(t *testing.T) {
 func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
        seederDataDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(seederDataDir)
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = seederDataDir
-       seeder, err := NewClient(&cfg)
+       seeder, err := NewClient(cfg)
        require.Nil(t, err)
        defer seeder.Close()
-       seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.Nil(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       cfg = TestingConfig
+       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
+       seederTorrent.VerifyData()
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
        cfg.DataDir = leecherDataDir
-       leecher, err := NewClient(&cfg)
+       leecher, err := NewClient(cfg)
        require.Nil(t, err)
        defer leecher.Close()
        leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
@@ -594,52 +319,53 @@ func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
                ret.ChunkSize = 2
                return
        }())
-       leecherTorrent.AddPeers([]Peer{
-               Peer{
-                       IP:   missinggo.AddrIP(seeder.ListenAddr()),
-                       Port: missinggo.AddrPort(seeder.ListenAddr()),
-               },
-       })
+       leecherTorrent.AddClientPeer(seeder)
        reader := leecherTorrent.NewReader()
        defer reader.Close()
        reader.SetReadahead(0)
        reader.SetResponsive()
        b := make([]byte, 2)
-       _, err = reader.Seek(3, os.SEEK_SET)
+       _, err = reader.Seek(3, io.SeekStart)
        require.NoError(t, err)
        _, err = io.ReadFull(reader, b)
        assert.Nil(t, err)
        assert.EqualValues(t, "lo", string(b))
-       go leecherTorrent.Drop()
-       _, err = reader.Seek(11, os.SEEK_SET)
+       _, err = reader.Seek(11, io.SeekStart)
        require.NoError(t, err)
+       leecherTorrent.Drop()
        n, err := reader.Read(b)
        assert.EqualError(t, err, "torrent closed")
        assert.EqualValues(t, 0, n)
 }
 
-func TestDHTInheritBlocklist(t *testing.T) {
+func TestDhtInheritBlocklist(t *testing.T) {
        ipl := iplist.New(nil)
        require.NotNil(t, ipl)
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.IPBlocklist = ipl
        cfg.NoDHT = false
-       cl, err := NewClient(&cfg)
+       cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
-       require.Equal(t, ipl, cl.DHT().IPBlocklist())
+       numServers := 0
+       cl.eachDhtServer(func(s DhtServer) {
+               t.Log(s)
+               assert.Equal(t, ipl, s.(AnacrolixDhtServerWrapper).Server.IPBlocklist())
+               numServers++
+       })
+       assert.EqualValues(t, 2, numServers)
 }
 
 // Check that stuff is merged in subsequent AddTorrentSpec for the same
 // infohash.
 func TestAddTorrentSpecMerging(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        dir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(dir)
        tt, new, err := cl.AddTorrentSpec(&TorrentSpec{
-               InfoHash: mi.Info.Hash(),
+               InfoHash: mi.HashInfoBytes(),
        })
        require.NoError(t, err)
        require.True(t, new)
@@ -653,10 +379,10 @@ func TestAddTorrentSpecMerging(t *testing.T) {
 func TestTorrentDroppedBeforeGotInfo(t *testing.T) {
        dir, mi := testutil.GreetingTestTorrent()
        os.RemoveAll(dir)
-       cl, _ := NewClient(&TestingConfig)
+       cl, _ := NewClient(TestingConfig(t))
        defer cl.Close()
        tt, _, _ := cl.AddTorrentSpec(&TorrentSpec{
-               InfoHash: mi.Info.Hash(),
+               InfoHash: mi.HashInfoBytes(),
        })
        tt.Drop()
        assert.EqualValues(t, 0, len(cl.Torrents()))
@@ -667,40 +393,40 @@ func TestTorrentDroppedBeforeGotInfo(t *testing.T) {
        }
 }
 
-func writeTorrentData(ts storage.Torrent, info *metainfo.InfoEx, b []byte) {
-       for i := range iter.N(info.NumPieces()) {
-               n, _ := ts.Piece(info.Piece(i)).WriteAt(b, 0)
-               b = b[n:]
+func writeTorrentData(ts *storage.Torrent, info metainfo.Info, b []byte) {
+       for i := 0; i < info.NumPieces(); i += 1 {
+               p := info.Piece(i)
+               ts.Piece(p).WriteAt(b[p.Offset():p.Offset()+p.Length()], 0)
        }
 }
 
-func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool) {
-       fileCacheDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(fileCacheDir)
+func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf func(*filecache.Cache) storage.ClientImpl) {
+       fileCacheDir := t.TempDir()
        fileCache, err := filecache.NewCache(fileCacheDir)
        require.NoError(t, err)
        greetingDataTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingDataTempDir)
-       filePieceStore := storage.NewPieceFileStorage(fileCache.AsFileStore())
-       greetingData, err := filePieceStore.OpenTorrent(&greetingMetainfo.Info)
+       filePieceStore := csf(fileCache)
+       info, err := greetingMetainfo.UnmarshalInfo()
+       require.NoError(t, err)
+       ih := greetingMetainfo.HashInfoBytes()
+       greetingData, err := storage.NewClient(filePieceStore).OpenTorrent(&info, ih)
        require.NoError(t, err)
-       writeTorrentData(greetingData, &greetingMetainfo.Info, []byte(testutil.GreetingFileContents))
+       writeTorrentData(greetingData, info, []byte(testutil.GreetingFileContents))
        // require.Equal(t, len(testutil.GreetingFileContents), written)
        // require.NoError(t, err)
-       for i := 0; i < greetingMetainfo.Info.NumPieces(); i++ {
-               p := greetingMetainfo.Info.Piece(i)
+       for i := 0; i < info.NumPieces(); i++ {
+               p := info.Piece(i)
                if alreadyCompleted {
-                       err := greetingData.Piece(p).MarkComplete()
-                       assert.NoError(t, err)
+                       require.NoError(t, greetingData.Piece(p).MarkComplete())
                }
        }
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        // TODO: Disable network option?
        cfg.DisableTCP = true
        cfg.DisableUTP = true
        cfg.DefaultStorage = filePieceStore
-       cl, err := NewClient(&cfg)
+       cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
        tt, err := cl.AddTorrent(greetingMetainfo)
@@ -711,42 +437,48 @@ func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool) {
        assert.Equal(t, alreadyCompleted, psrs[0].Complete)
        if alreadyCompleted {
                r := tt.NewReader()
-               b, err := ioutil.ReadAll(r)
-               assert.NoError(t, err)
-               assert.EqualValues(t, testutil.GreetingFileContents, b)
+               quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
        }
 }
 
 func TestAddTorrentPiecesAlreadyCompleted(t *testing.T) {
-       testAddTorrentPriorPieceCompletion(t, true)
+       testAddTorrentPriorPieceCompletion(t, true, fileCachePieceResourceStorage)
 }
 
 func TestAddTorrentPiecesNotAlreadyCompleted(t *testing.T) {
-       testAddTorrentPriorPieceCompletion(t, false)
+       testAddTorrentPriorPieceCompletion(t, false, fileCachePieceResourceStorage)
 }
 
 func TestAddMetainfoWithNodes(t *testing.T) {
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
+       cfg.ListenHost = func(string) string { return "" }
        cfg.NoDHT = false
-       // For now, we want to just jam the nodes into the table, without
-       // verifying them first. Also the DHT code doesn't support mixing secure
-       // and insecure nodes if security is enabled (yet).
-       cfg.DHTConfig.NoSecurity = true
-       cl, err := NewClient(&cfg)
+       cfg.DhtStartingNodes = func(string) dht.StartingNodesGetter { return func() ([]dht.Addr, error) { return nil, nil } }
+       // For now, we want to just jam the nodes into the table, without verifying them first. Also the
+       // DHT code doesn't support mixing secure and insecure nodes if security is enabled (yet).
+       // cfg.DHTConfig.NoSecurity = true
+       cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
-       assert.EqualValues(t, cl.DHT().NumNodes(), 0)
+       sum := func() (ret int64) {
+               cl.eachDhtServer(func(s DhtServer) {
+                       ret += s.Stats().(dht.ServerStats).OutboundQueriesAttempted
+               })
+               return
+       }
+       assert.EqualValues(t, 0, sum())
        tt, err := cl.AddTorrentFromFile("metainfo/testdata/issue_65a.torrent")
        require.NoError(t, err)
-       assert.Len(t, tt.trackers, 5)
-       assert.EqualValues(t, 6, cl.DHT().NumNodes())
+       // Nodes are not added or exposed in Torrent's metainfo. We just randomly
+       // check if the announce-list is here instead. TODO: Add nodes.
+       assert.Len(t, tt.metainfo.AnnounceList, 5)
+       // There are 6 nodes in the torrent file.
+       for sum() != int64(6*len(cl.dhtServers)) {
+               time.Sleep(time.Millisecond)
+       }
 }
 
 type testDownloadCancelParams struct {
-       Responsive                bool
-       Readahead                 int64
-       SetReadahead              bool
-       ExportClientStatus        bool
        SetLeecherStorageCapacity bool
        LeecherStorageCapacity    int64
        Cancel                    bool
@@ -755,31 +487,27 @@ type testDownloadCancelParams struct {
 func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
        greetingTempDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = greetingTempDir
-       seeder, err := NewClient(&cfg)
+       seeder, err := NewClient(cfg)
        require.NoError(t, err)
        defer seeder.Close()
-       if ps.ExportClientStatus {
-               testutil.ExportStatusWriter(seeder, "s")
-       }
-       seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(leecherDataDir)
+       defer testutil.ExportStatusWriter(seeder, "s", t)()
+       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
+       seederTorrent.VerifyData()
+       leecherDataDir := t.TempDir()
        fc, err := filecache.NewCache(leecherDataDir)
        require.NoError(t, err)
        if ps.SetLeecherStorageCapacity {
                fc.SetCapacity(ps.LeecherStorageCapacity)
        }
-       cfg.DefaultStorage = storage.NewPieceFileStorage(fc.AsFileStore())
+       cfg.DefaultStorage = storage.NewResourcePieces(fc.AsResourceProvider())
        cfg.DataDir = leecherDataDir
-       leecher, _ := NewClient(&cfg)
+       leecher, err := NewClient(cfg)
+       require.NoError(t, err)
        defer leecher.Close()
-       if ps.ExportClientStatus {
-               testutil.ExportStatusWriter(leecher, "l")
-       }
+       defer testutil.ExportStatusWriter(leecher, "l", t)()
        leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
                ret = TorrentSpecFromMetaInfo(mi)
                ret.ChunkSize = 2
@@ -789,35 +517,28 @@ func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
        assert.True(t, new)
        psc := leecherGreeting.SubscribePieceStateChanges()
        defer psc.Close()
-       leecherGreeting.DownloadAll()
+
+       leecherGreeting.cl.lock()
+       leecherGreeting.downloadPiecesLocked(0, leecherGreeting.numPieces())
        if ps.Cancel {
-               leecherGreeting.CancelPieces(0, leecherGreeting.NumPieces())
+               leecherGreeting.cancelPiecesLocked(0, leecherGreeting.NumPieces(), "")
        }
-       leecherGreeting.AddPeers([]Peer{
-               Peer{
-                       IP:   missinggo.AddrIP(seeder.ListenAddr()),
-                       Port: missinggo.AddrPort(seeder.ListenAddr()),
-               },
-       })
+       leecherGreeting.cl.unlock()
+       done := make(chan struct{})
+       defer close(done)
+       go leecherGreeting.AddClientPeer(seeder)
        completes := make(map[int]bool, 3)
-values:
-       for {
-               // started := time.Now()
-               select {
-               case _v := <-psc.Values:
-                       // log.Print(time.Since(started))
-                       v := _v.(PieceStateChange)
-                       completes[v.Index] = v.Complete
-               case <-time.After(100 * time.Millisecond):
-                       break values
+       expected := func() map[int]bool {
+               if ps.Cancel {
+                       return map[int]bool{0: false, 1: false, 2: false}
+               } else {
+                       return map[int]bool{0: true, 1: true, 2: true}
                }
+       }()
+       for !reflect.DeepEqual(completes, expected) {
+               v := <-psc.Values
+               completes[v.Index] = v.Complete
        }
-       if ps.Cancel {
-               assert.EqualValues(t, map[int]bool{0: false, 1: false, 2: false}, completes)
-       } else {
-               assert.EqualValues(t, map[int]bool{0: true, 1: true, 2: true}, completes)
-       }
-
 }
 
 func TestTorrentDownloadAll(t *testing.T) {
@@ -832,24 +553,34 @@ func TestTorrentDownloadAllThenCancel(t *testing.T) {
 
 // Ensure that it's an error for a peer to send an invalid have message.
 func TestPeerInvalidHave(t *testing.T) {
-       cl, err := NewClient(&TestingConfig)
+       cfg := TestingConfig(t)
+       cfg.DropMutuallyCompletePeers = false
+       cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
+       info := metainfo.Info{
+               PieceLength: 1,
+               Pieces:      make([]byte, 20),
+               Files:       []metainfo.FileInfo{{Length: 1}},
+       }
+       infoBytes, err := bencode.Marshal(info)
+       require.NoError(t, err)
        tt, _new, err := cl.AddTorrentSpec(&TorrentSpec{
-               Info: &metainfo.InfoEx{
-                       Info: metainfo.Info{
-                               PieceLength: 1,
-                               Pieces:      make([]byte, 20),
-                               Files:       []metainfo.FileInfo{{Length: 1}},
-                       },
-               },
+               InfoBytes: infoBytes,
+               InfoHash:  metainfo.HashBytes(infoBytes),
+               Storage:   badStorage{},
        })
        require.NoError(t, err)
        assert.True(t, _new)
        defer tt.Drop()
-       cn := &connection{
-               t: tt,
-       }
+       cn := &PeerConn{Peer: Peer{
+               t:         tt,
+               callbacks: &cfg.Callbacks,
+       }}
+       tt.conns[cn] = struct{}{}
+       cn.peerImpl = cn
+       cl.lock()
+       defer cl.unlock()
        assert.NoError(t, cn.peerSentHave(0))
        assert.Error(t, cn.peerSentHave(1))
 }
@@ -857,11 +588,322 @@ func TestPeerInvalidHave(t *testing.T) {
 func TestPieceCompletedInStorageButNotClient(t *testing.T) {
        greetingTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig
+       cfg := TestingConfig(t)
        cfg.DataDir = greetingTempDir
-       seeder, err := NewClient(&TestingConfig)
+       seeder, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
+       defer seeder.Close()
        seeder.AddTorrentSpec(&TorrentSpec{
-               Info: &greetingMetainfo.Info,
+               InfoBytes: greetingMetainfo.InfoBytes,
        })
 }
+
+// Check that when the listen port is 0, all the protocols listened on have
+// the same port, and it isn't zero.
+func TestClientDynamicListenPortAllProtocols(t *testing.T) {
+       cl, err := NewClient(TestingConfig(t))
+       require.NoError(t, err)
+       defer cl.Close()
+       port := cl.LocalPort()
+       assert.NotEqual(t, 0, port)
+       cl.eachListener(func(s Listener) bool {
+               assert.Equal(t, port, missinggo.AddrPort(s.Addr()))
+               return true
+       })
+}
+
+func TestClientDynamicListenTCPOnly(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.DisableUTP = true
+       cfg.DisableTCP = false
+       cl, err := NewClient(cfg)
+       require.NoError(t, err)
+       defer cl.Close()
+       assert.NotEqual(t, 0, cl.LocalPort())
+}
+
+func TestClientDynamicListenUTPOnly(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.DisableTCP = true
+       cfg.DisableUTP = false
+       cl, err := NewClient(cfg)
+       require.NoError(t, err)
+       defer cl.Close()
+       assert.NotEqual(t, 0, cl.LocalPort())
+}
+
+func totalConns(tts []*Torrent) (ret int) {
+       for _, tt := range tts {
+               tt.cl.lock()
+               ret += len(tt.conns)
+               tt.cl.unlock()
+       }
+       return
+}
+
+func TestSetMaxEstablishedConn(t *testing.T) {
+       var tts []*Torrent
+       ih := testutil.GreetingMetaInfo().HashInfoBytes()
+       cfg := TestingConfig(t)
+       cfg.DisableAcceptRateLimiting = true
+       cfg.DropDuplicatePeerIds = true
+       for i := 0; i < 3; i += 1 {
+               cl, err := NewClient(cfg)
+               require.NoError(t, err)
+               defer cl.Close()
+               tt, _ := cl.AddTorrentInfoHash(ih)
+               tt.SetMaxEstablishedConns(2)
+               defer testutil.ExportStatusWriter(cl, fmt.Sprintf("%d", i), t)()
+               tts = append(tts, tt)
+       }
+       addPeers := func() {
+               for _, tt := range tts {
+                       for _, _tt := range tts {
+                               // if tt != _tt {
+                               tt.AddClientPeer(_tt.cl)
+                               // }
+                       }
+               }
+       }
+       waitTotalConns := func(num int) {
+               for totalConns(tts) != num {
+                       addPeers()
+                       time.Sleep(time.Millisecond)
+               }
+       }
+       addPeers()
+       waitTotalConns(6)
+       tts[0].SetMaxEstablishedConns(1)
+       waitTotalConns(4)
+       tts[0].SetMaxEstablishedConns(0)
+       waitTotalConns(2)
+       tts[0].SetMaxEstablishedConns(1)
+       addPeers()
+       waitTotalConns(4)
+       tts[0].SetMaxEstablishedConns(2)
+       addPeers()
+       waitTotalConns(6)
+}
+
+// Creates a file containing its own name as data. Make a metainfo from that, adds it to the given
+// client, and returns a magnet link.
+func makeMagnet(t *testing.T, cl *Client, dir, name string) string {
+       os.MkdirAll(dir, 0o770)
+       file, err := os.Create(filepath.Join(dir, name))
+       require.NoError(t, err)
+       file.Write([]byte(name))
+       file.Close()
+       mi := metainfo.MetaInfo{}
+       mi.SetDefaults()
+       info := metainfo.Info{PieceLength: 256 * 1024}
+       err = info.BuildFromFilePath(filepath.Join(dir, name))
+       require.NoError(t, err)
+       mi.InfoBytes, err = bencode.Marshal(info)
+       require.NoError(t, err)
+       magnet := mi.Magnet(nil, &info).String()
+       tr, err := cl.AddTorrent(&mi)
+       require.NoError(t, err)
+       require.True(t, tr.Seeding())
+       tr.VerifyData()
+       return magnet
+}
+
+// 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, leecher func(*ClientConfig)) {
+       cfg := TestingConfig(t)
+       cfg.Seed = true
+       cfg.DataDir = filepath.Join(cfg.DataDir, "server")
+       os.Mkdir(cfg.DataDir, 0o755)
+       seeder(cfg)
+       server, err := NewClient(cfg)
+       require.NoError(t, err)
+       defer server.Close()
+       defer testutil.ExportStatusWriter(server, "s", t)()
+       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(t)
+       cfg.DataDir = filepath.Join(cfg.DataDir, "client")
+       leecher(cfg)
+       client, err := NewClient(cfg)
+       require.NoError(t, err)
+       defer client.Close()
+       defer testutil.ExportStatusWriter(client, "c", t)()
+       tr, err := client.AddMagnet(magnet1)
+       require.NoError(t, err)
+       tr.AddClientPeer(server)
+       <-tr.GotInfo()
+       tr.DownloadAll()
+       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", "localhost:50007", nil, log.Default)
+       if s != nil {
+               defer s.Close()
+       }
+       cfg := TestingConfig(t).SetListenAddr("localhost:50007")
+       cfg.DisableUTP = false
+       cl, err := NewClient(cfg)
+       if err == nil {
+               assert.Nil(t, cl.Close())
+       }
+       require.Error(t, err)
+       require.Nil(t, cl)
+}
+
+func TestClientHasDhtServersWhenUtpDisabled(t *testing.T) {
+       cc := TestingConfig(t)
+       cc.DisableUTP = true
+       cc.NoDHT = false
+       cl, err := NewClient(cc)
+       require.NoError(t, err)
+       defer cl.Close()
+       assert.NotEmpty(t, cl.DhtServers())
+}
+
+func TestClientDisabledImplicitNetworksButDhtEnabled(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.DisableTCP = true
+       cfg.DisableUTP = true
+       cfg.NoDHT = false
+       cl, err := NewClient(cfg)
+       require.NoError(t, err)
+       defer cl.Close()
+       assert.Empty(t, cl.listeners)
+       assert.NotEmpty(t, cl.DhtServers())
+}
+
+func TestBadPeerIpPort(t *testing.T) {
+       for _, tc := range []struct {
+               title      string
+               ip         net.IP
+               port       int
+               expectedOk bool
+               setup      func(*Client)
+       }{
+               {"empty both", nil, 0, true, func(*Client) {}},
+               {"empty/nil ip", nil, 6666, true, func(*Client) {}},
+               {
+                       "empty port",
+                       net.ParseIP("127.0.0.1/32"),
+                       0, true,
+                       func(*Client) {},
+               },
+               {
+                       "in doppleganger addresses",
+                       net.ParseIP("127.0.0.1/32"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               cl.dopplegangerAddrs["10.0.0.1:2322"] = struct{}{}
+                       },
+               },
+               {
+                       "in IP block list",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               cl.ipBlockList = iplist.New([]iplist.Range{
+                                       {First: net.ParseIP("10.0.0.1"), Last: net.ParseIP("10.0.0.255")},
+                               })
+                       },
+               },
+               {
+                       "in bad peer IPs",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               ipAddr, ok := netip.AddrFromSlice(net.ParseIP("10.0.0.1"))
+                               require.True(t, ok)
+                               cl.badPeerIPs = map[netip.Addr]struct{}{}
+                               cl.badPeerIPs[ipAddr] = struct{}{}
+                       },
+               },
+               {
+                       "good",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       false,
+                       func(cl *Client) {},
+               },
+       } {
+               t.Run(tc.title, func(t *testing.T) {
+                       cfg := TestingConfig(t)
+                       cfg.DisableTCP = true
+                       cfg.DisableUTP = true
+                       cfg.NoDHT = false
+                       cl, err := NewClient(cfg)
+                       require.NoError(t, err)
+                       defer cl.Close()
+
+                       tc.setup(cl)
+                       require.Equal(t, tc.expectedOk, cl.badPeerIPPort(tc.ip, tc.port))
+               })
+       }
+}
+
+// https://github.com/anacrolix/torrent/issues/837
+func TestClientConfigSetHandlerNotIgnored(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.Logger.SetHandlers(log.DiscardHandler)
+       c := qt.New(t)
+       cl, err := NewClient(cfg)
+       c.Assert(err, qt.IsNil)
+       defer cl.Close()
+       c.Assert(cl.logger.Handlers, qt.HasLen, 1)
+       h := cl.logger.Handlers[0].(log.StreamHandler)
+       c.Check(h.W, qt.Equals, io.Discard)
+}