// 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) require.NoError(t, err) defer cl.Close() assert.NotEqual(t, 0, missinggo.AddrPort(cl.ListenAddr())) assert.Equal(t, missinggo.AddrPort(cl.utpSock.Addr()), missinggo.AddrPort(cl.tcpListener.Addr())) }
func (cl *Client) acceptConnections(l net.Listener, utp bool) { for { cl.waitAccept() conn, err := l.Accept() conn = pproffd.WrapNetConn(conn) if cl.closed.IsSet() { if conn != nil { conn.Close() } return } if err != nil { log.Print(err) // I think something harsher should happen here? Our accept // routine just f****d off. return } if utp { acceptUTP.Add(1) } else { acceptTCP.Add(1) } cl.mu.RLock() reject := cl.badPeerIPPort( missinggo.AddrIP(conn.RemoteAddr()), missinggo.AddrPort(conn.RemoteAddr())) cl.mu.RUnlock() if reject { acceptReject.Add(1) conn.Close() continue } go cl.incomingConnection(conn, utp) } }
func addClientPeer(t *Torrent, cl *Client) { t.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(cl.ListenAddr()), Port: missinggo.AddrPort(cl.ListenAddr()), }, }) }
func TestClientTransfer(t *testing.T) { greetingTempDir, mi := testutil.GreetingTestTorrent() defer os.RemoveAll(greetingTempDir) cfg := TestingConfig cfg.Seed = true cfg.DataDir = greetingTempDir seeder, err := NewClient(&cfg) if err != nil { t.Fatal(err) } defer seeder.Close() seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) leecherDataDir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(leecherDataDir) // cfg.TorrentDataOpener = func(info *metainfo.Info) (data.Data, error) { // return blob.TorrentData(info, leecherDataDir), nil // } blobStore := blob.NewStore(leecherDataDir) cfg.TorrentDataOpener = func(info *metainfo.Info) Data { return blobStore.OpenTorrent(info) } leecher, _ := NewClient(&cfg) defer leecher.Close() leecherGreeting, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { ret = TorrentSpecFromMetaInfo(mi) ret.ChunkSize = 2 return }()) // TODO: The piece state publishing is kinda jammed in here until I have a // more thorough test. go func() { s := leecherGreeting.pieceStateChanges.Subscribe() defer s.Close() for i := range s.Values { log.Print(i) } log.Print("finished") }() leecherGreeting.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) r := leecherGreeting.NewReader() defer r.Close() _greeting, err := ioutil.ReadAll(r) if err != nil { t.Fatalf("%q %s", string(_greeting), err) } greeting := string(_greeting) if greeting != testutil.GreetingFileContents { t.Fatal(":(") } }
func (cl *Client) sendInitialMessages(conn *connection, torrent *Torrent) { if conn.PeerExtensionBytes.SupportsExtended() && cl.extensionBytes.SupportsExtended() { conn.Post(pp.Message{ Type: pp.Extended, ExtendedID: pp.HandshakeExtendedID, ExtendedPayload: func() []byte { d := map[string]interface{}{ "m": func() (ret map[string]int) { ret = make(map[string]int, 2) ret["ut_metadata"] = metadataExtendedId if !cl.config.DisablePEX { ret["ut_pex"] = pexExtendedId } return }(), "v": extendedHandshakeClientVersion, // No upload queue is implemented yet. "reqq": 64, } if !cl.config.DisableEncryption { d["e"] = 1 } if torrent.metadataSizeKnown() { d["metadata_size"] = torrent.metadataSize() } if p := cl.incomingPeerPort(); p != 0 { d["p"] = p } yourip, err := addrCompactIP(conn.remoteAddr()) if err != nil { log.Printf("error calculating yourip field value in extension handshake: %s", err) } else { d["yourip"] = yourip } // log.Printf("sending %v", d) b, err := bencode.Marshal(d) if err != nil { panic(err) } return b }(), }) } if torrent.haveAnyPieces() { conn.Bitfield(torrent.bitfield()) } else if cl.extensionBytes.SupportsFast() && conn.PeerExtensionBytes.SupportsFast() { conn.Post(pp.Message{ Type: pp.HaveNone, }) } if conn.PeerExtensionBytes.SupportsDHT() && cl.extensionBytes.SupportsDHT() && cl.dHT != nil { conn.Post(pp.Message{ Type: pp.Port, Port: uint16(missinggo.AddrPort(cl.dHT.Addr())), }) } }
func TestClientDynamicListenUTPOnly(t *testing.T) { cfg := TestingConfig cfg.DisableTCP = true cl, err := NewClient(&cfg) require.NoError(t, err) defer cl.Close() assert.NotEqual(t, 0, missinggo.AddrPort(cl.ListenAddr())) assert.Nil(t, cl.tcpListener) }
func TestDownloadOnDemand(t *testing.T) { layout, err := newGreetingLayout() require.NoError(t, err) defer layout.Destroy() seeder, err := torrent.NewClient(&torrent.Config{ DataDir: layout.Completed, DisableTrackers: true, NoDHT: true, ListenAddr: "localhost:0", Seed: true, // Ensure that the metainfo is obtained over the wire, since we added // the torrent to the seeder by magnet. DisableMetainfoCache: true, }) require.NoError(t, err) defer seeder.Close() testutil.ExportStatusWriter(seeder, "s") _, err = seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%s", layout.Metainfo.Info.Hash.HexString())) require.NoError(t, err) leecher, err := torrent.NewClient(&torrent.Config{ DisableTrackers: true, NoDHT: true, ListenAddr: "localhost:0", DisableTCP: true, DefaultStorage: storage.NewMMap(filepath.Join(layout.BaseDir, "download")), // This can be used to check if clients can connect to other clients // with the same ID. // PeerID: seeder.PeerID(), }) require.NoError(t, err) testutil.ExportStatusWriter(leecher, "l") defer leecher.Close() leecherTorrent, _ := leecher.AddTorrent(layout.Metainfo) leecherTorrent.AddPeers([]torrent.Peer{ torrent.Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) fs := New(leecher) defer fs.Destroy() root, _ := fs.Root() node, _ := root.(fusefs.NodeStringLookuper).Lookup(netContext.Background(), "greeting") var attr fuse.Attr node.Attr(netContext.Background(), &attr) size := attr.Size resp := &fuse.ReadResponse{ Data: make([]byte, size), } node.(fusefs.HandleReader).Read(netContext.Background(), &fuse.ReadRequest{ Size: int(size), }, resp) assert.EqualValues(t, testutil.GreetingFileContents, resp.Data) }
// Writes the node info to its compact binary representation in b. See // CompactNodeInfoLen. func (ni *NodeInfo) PutCompact(b []byte) error { if n := copy(b[:], ni.ID[:]); n != 20 { panic(n) } ip := missinggo.AddrIP(ni.Addr).To4() if len(ip) != 4 { return errors.New("expected ipv4 address") } if n := copy(b[20:], ip); n != 4 { panic(n) } binary.BigEndian.PutUint16(b[24:], uint16(missinggo.AddrPort(ni.Addr))) return nil }
// Add response nodes to node table. func (s *Server) liftNodes(d Msg) { if d.Y != "r" { return } for _, cni := range d.R.Nodes { if missinggo.AddrPort(cni.Addr) == 0 { // TODO: Why would people even do this? continue } if s.ipBlocked(missinggo.AddrIP(cni.Addr)) { continue } n := s.getNode(cni.Addr, string(cni.ID[:])) n.SetIDFromBytes(cni.ID[:]) } }
func TestTorrentDroppedDuringResponsiveRead(t *testing.T) { seederDataDir, mi := testutil.GreetingTestTorrent() defer os.RemoveAll(seederDataDir) cfg := TestingConfig cfg.Seed = true cfg.DataDir = seederDataDir 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 cfg.DataDir = leecherDataDir leecher, err := NewClient(&cfg) require.Nil(t, err) defer leecher.Close() leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { ret = TorrentSpecFromMetaInfo(mi) ret.ChunkSize = 2 return }()) leecherTorrent.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) reader := leecherTorrent.NewReader() defer reader.Close() reader.SetReadahead(0) reader.SetResponsive() b := make([]byte, 2) _, err = reader.Seek(3, os.SEEK_SET) 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) require.NoError(t, err) n, err := reader.Read(b) assert.EqualError(t, err, "torrent closed") assert.EqualValues(t, 0, n) }
func TestClientTransfer(t *testing.T) { greetingTempDir, mi := testutil.GreetingTestTorrent() defer os.RemoveAll(greetingTempDir) cfg := TestingConfig cfg.Seed = true cfg.DataDir = greetingTempDir seeder, err := NewClient(&cfg) if err != nil { t.Fatal(err) } defer seeder.Close() seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) leecherDataDir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(leecherDataDir) // cfg.TorrentDataOpener = func(info *metainfo.Info) (data.Data, error) { // return blob.TorrentData(info, leecherDataDir), nil // } cfg.TorrentDataOpener = blob.NewStore(leecherDataDir).OpenTorrent leecher, _ := NewClient(&cfg) defer leecher.Close() leecherGreeting, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { ret = TorrentSpecFromMetaInfo(mi) ret.ChunkSize = 2 return }()) leecherGreeting.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) r := leecherGreeting.NewReader() defer r.Close() _greeting, err := ioutil.ReadAll(r) if err != nil { t.Fatalf("%q %s", string(_greeting), err) } greeting := string(_greeting) if greeting != testutil.GreetingFileContents { t.Fatal(":(") } }
func (me *Announce) gotNodeAddr(addr dHTAddr) { if missinggo.AddrPort(addr) == 0 { // Not a contactable address. return } if me.triedAddrs.Test([]byte(addr.String())) { return } if me.server.ipBlocked(addr.UDPAddr().IP) { return } me.server.mu.Lock() if me.server.badNodes.Test([]byte(addr.String())) { me.server.mu.Unlock() return } me.server.mu.Unlock() me.contact(addr) }
func connRemoteAddrIP(network, laddr string, dialHost string) net.IP { l, err := net.Listen(network, laddr) if err != nil { panic(err) } go func() { c, err := net.Dial(network, net.JoinHostPort(dialHost, fmt.Sprintf("%d", missinggo.AddrPort(l.Addr())))) if err != nil { panic(err) } defer c.Close() }() c, err := l.Accept() if err != nil { panic(err) } defer c.Close() ret := missinggo.AddrIP(c.RemoteAddr()) return ret }
func TestResponsive(t *testing.T) { seederDataDir, mi := testutil.GreetingTestTorrent() defer os.RemoveAll(seederDataDir) cfg := TestingConfig cfg.Seed = true cfg.DataDir = seederDataDir 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 cfg.DataDir = leecherDataDir leecher, err := NewClient(&cfg) require.Nil(t, err) defer leecher.Close() leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { ret = TorrentSpecFromMetaInfo(mi) ret.ChunkSize = 2 return }()) leecherTorrent.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) reader := leecherTorrent.NewReader() reader.SetReadahead(0) reader.SetResponsive() b := make([]byte, 2) _, err = reader.ReadAt(b, 3) assert.Nil(t, err) assert.EqualValues(t, "lo", string(b)) n, err := reader.ReadAt(b, 11) assert.Nil(t, err) assert.EqualValues(t, 2, n) assert.EqualValues(t, "d\n", string(b)) }
func TestTCPAddrString(t *testing.T) { l, err := net.Listen("tcp4", "localhost:0") if err != nil { t.Fatal(err) } defer l.Close() c, err := net.Dial("tcp", l.Addr().String()) if err != nil { t.Fatal(err) } defer c.Close() ras := c.RemoteAddr().String() ta := &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: missinggo.AddrPort(l.Addr()), } s := ta.String() if ras != s { t.FailNow() } }
func (me *packetConn) WriteTo(b []byte, na net.Addr) (n int, err error) { mu.Lock() defer mu.Unlock() if me.closed { err = errors.New("closed") return } if me.writeDeadline.exceeded() { err = errTimeout return } n = len(b) port := missinggo.AddrPort(na) c, ok := conns[port] if !ok { // log.Printf("no conn for port %d", port) return } c.reads = append(c.reads, packet{append([]byte(nil), b...), me.addr}) cond.Broadcast() return }
// Writes out a human readable status of the client, such as for writing to a // HTTP status page. func (cl *Client) WriteStatus(_w io.Writer) { cl.mu.RLock() defer cl.mu.RUnlock() w := bufio.NewWriter(_w) defer w.Flush() if addr := cl.ListenAddr(); addr != nil { fmt.Fprintf(w, "Listening on %s\n", cl.ListenAddr()) } else { fmt.Fprintln(w, "Not listening!") } fmt.Fprintf(w, "Peer ID: %+q\n", cl.peerID) fmt.Fprintf(w, "Banned IPs: %d\n", len(cl.badPeerIPs)) if cl.dHT != nil { dhtStats := cl.dHT.Stats() fmt.Fprintf(w, "DHT nodes: %d (%d good, %d banned)\n", dhtStats.Nodes, dhtStats.GoodNodes, dhtStats.BadNodes) fmt.Fprintf(w, "DHT Server ID: %x\n", cl.dHT.ID()) fmt.Fprintf(w, "DHT port: %d\n", missinggo.AddrPort(cl.dHT.Addr())) fmt.Fprintf(w, "DHT announces: %d\n", dhtStats.ConfirmedAnnounces) fmt.Fprintf(w, "Outstanding transactions: %d\n", dhtStats.OutstandingTransactions) } fmt.Fprintf(w, "# Torrents: %d\n", len(cl.torrents)) fmt.Fprintln(w) for _, t := range cl.sortedTorrents() { if t.name() == "" { fmt.Fprint(w, "<unknown name>") } else { fmt.Fprint(w, t.name()) } fmt.Fprint(w, "\n") if t.haveInfo() { fmt.Fprintf(w, "%f%% of %d bytes (%s)", 100*(1-float64(t.bytesLeft())/float64(t.length)), t.length, humanize.Bytes(uint64(t.length))) } else { w.WriteString("<missing metainfo>") } fmt.Fprint(w, "\n") t.writeStatus(w, cl) fmt.Fprintln(w) } }
func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) { 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() if ps.ExportClientStatus { testutil.ExportStatusWriter(seeder, "s") } seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) 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.NewFileStorePieces(fc.AsFileStore()) cfg.DataDir = leecherDataDir leecher, _ := NewClient(&cfg) defer leecher.Close() if ps.ExportClientStatus { testutil.ExportStatusWriter(leecher, "l") } leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { ret = TorrentSpecFromMetaInfo(mi) ret.ChunkSize = 2 return }()) require.NoError(t, err) assert.True(t, new) psc := leecherGreeting.SubscribePieceStateChanges() defer psc.Close() leecherGreeting.DownloadAll() if ps.Cancel { leecherGreeting.CancelPieces(0, leecherGreeting.NumPieces()) } leecherGreeting.AddPeers([]Peer{ Peer{ IP: missinggo.AddrIP(seeder.ListenAddr()), Port: missinggo.AddrPort(seeder.ListenAddr()), }, }) 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 } } 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) } }
// 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()), }, }) wg.Add(1) go func() { defer wg.Done() leecherGreeting.DownloadAll() leecher.WaitAll() }() wg.Wait() }
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 = ps.LeecherFileCachePieceStorageFactory(fc) 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) } }
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) } }