ronantakizawa/codereview-bench huggingface dataset:
Repository: syncthing/syncthing
File: lib/protocol/bep_extensions.go (Go)

Code before:
	// Mask out the ignored local flags before checking IsInvalid() below
	f.LocalFlags &^= ignoreFlags
	other.LocalFlags &^= ignoreFlags

	if f.Name != other.Name || f.Type != other.Type || f.Deleted != other.Deleted || f.IsInvalid() != other.IsInvalid() {
		return false
	}

	if !ignorePerms && !f.NoPermissions && !other.NoPermissions && !PermsEqual(f.Permissions, other.Permissions) {
		return false
	}

	switch f.Type {
	case FileInfoTypeFile:
		return f.Size == other.Size && ModTimeEqual(f.ModTime(), other.ModTime(), modTimeWindowS) && (ignoreBlocks || BlocksEqual(f.Blocks, other.Blocks))
	case FileInfoTypeSymlink:
		return f.SymlinkTarget == other.SymlinkTarget
	case FileInfoTypeDirectory:
		return true
	}

	return false
}

func ModTimeEqual(a, b time.Time, modTimeWindowS int) bool {
	if a == b {
		return true
	}
	diff := a.Sub(b)
	if diff < 0 {
		diff *= -1
	}
	return diff < time.Duration(modTimeWindowS)*time.Second
}

func PermsEqual(a, b uint32) bool {
	switch runtime.GOOS {
	case "windows":
		// There is only writeable and read only, represented for user, group
		// and other equally. We only compare against user.
		return a&0600 == b&0600
	default:
		// All bits count
		return a&0777 == b&0777
	}
}

// BlocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func BlocksEqual(a, b []BlockInfo) bool {
	if len(b) != len(a) {

Review comment (suggestion):
This should be `a.Equal(b)` to correctly handle time zones and monotonic counters and whatnot. (Not that it matters super much given the following check where the diff will be zero for those case...)

Code after:
	// Mask out the ignored local flags before checking IsInvalid() below
	f.LocalFlags &^= ignoreFlags
	other.LocalFlags &^= ignoreFlags

	if f.Name != other.Name || f.Type != other.Type || f.Deleted != other.Deleted || f.IsInvalid() != other.IsInvalid() {
		return false
	}

	if !ignorePerms && !f.NoPermissions && !other.NoPermissions && !PermsEqual(f.Permissions, other.Permissions) {
		return false
	}

	switch f.Type {
	case FileInfoTypeFile:
		return f.Size == other.Size && ModTimeEqual(f.ModTime(), other.ModTime(), modTimeWindow) && (ignoreBlocks || BlocksEqual(f.Blocks, other.Blocks))
	case FileInfoTypeSymlink:
		return f.SymlinkTarget == other.SymlinkTarget
	case FileInfoTypeDirectory:
		return true
	}

	return false
}

func ModTimeEqual(a, b time.Time, modTimeWindow time.Duration) bool {
	if a.Equal(b) {
		return true
	}
	diff := a.Sub(b)
	if diff < 0 {
		diff *= -1
	}
	return diff < modTimeWindow
}

func PermsEqual(a, b uint32) bool {
	switch runtime.GOOS {
	case "windows":
		// There is only writeable and read only, represented for user, group
		// and other equally. We only compare against user.
		return a&0600 == b&0600
	default:
		// All bits count
		return a&0777 == b&0777
	}
}

// BlocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func BlocksEqual(a, b []BlockInfo) bool {
	if len(b) != len(a) {
Repository: syncthing/syncthing
File: lib/config/folderconfiguration.go (Go)

Code before:

// RequiresRestartOnly returns a copy with only the attributes that require
// restart on change.
func (f FolderConfiguration) RequiresRestartOnly() FolderConfiguration {
	copy := f

	// Manual handling for things that are not taken care of by the tag
	// copier, yet should not cause a restart.
	copy.cachedFilesystem = nil

	blank := FolderConfiguration{}
	util.CopyMatchingTag(&blank, &copy, "restart", func(v string) bool {
		if len(v) > 0 && v != "false" {
			panic(fmt.Sprintf(`unexpected tag value: %s. expected untagged or "false"`, v))
		}
		return v == "false"
	})
	return copy
}

func (f *FolderConfiguration) SharedWith(device protocol.DeviceID) bool {
	for _, dev := range f.Devices {
		if dev.DeviceID == device {
			return true
		}
	}
	return false
}

func (f *FolderConfiguration) CheckAvailableSpace(req int64) error {
	val := f.MinDiskFree.BaseValue()
	if val <= 0 {
		return nil
	}
	fs := f.Filesystem()
	usage, err := fs.Usage(".")
	if err != nil {
		return nil
	}
	usage.Free -= req
	if usage.Free > 0 {
		if err := CheckFreeSpace(f.MinDiskFree, usage); err == nil {
			return nil
		}
	}
	return fmt.Errorf("insufficient space in %v %v", fs.Type(), fs.URI())
}


Review comment (suggestion):
I would have expexted that err being nil and fsttpe being "" is impossible. that's something to ask about upstream. for us it means we need to check both error and fstype not being an empty string for now.

Code after:
	}

	if f.WeakHashThresholdPct == 0 {
		f.WeakHashThresholdPct = 25
	}

	if f.MarkerName == "" {
		f.MarkerName = DefaultMarkerName
	}

	switch {
	case f.RawModTimeWindowS > 0:
		f.cachedModTimeWindow = time.Duration(f.RawModTimeWindowS) * time.Second
	case runtime.GOOS == "android":
		usage, err := disk.Usage(f.Filesystem().URI())
		if err != nil {
			l.Debugf("Error detecting FS at %v on android, setting mtime window to 2s: %v", f.Path, err)
			f.cachedModTimeWindow = 2 * time.Second
			break
		}
		if strings.Contains(strings.ToLower(usage.Fstype), "fat") {
			l.Debugf("Detecting FS at %v on android, found %v, thus setting mtime window to 2s", f.Path, usage.Fstype)
			f.cachedModTimeWindow = 2 * time.Second
			break
		}
		l.Debugf("Detecting FS at %v on android, found %v, thus leaving mtime window at 0", f.Path, usage.Fstype)
	}
}

// RequiresRestartOnly returns a copy with only the attributes that require
// restart on change.
func (f FolderConfiguration) RequiresRestartOnly() FolderConfiguration {
	copy := f

	// Manual handling for things that are not taken care of by the tag
	// copier, yet should not cause a restart.
	copy.cachedFilesystem = nil

	blank := FolderConfiguration{}
	util.CopyMatchingTag(&blank, &copy, "restart", func(v string) bool {
		if len(v) > 0 && v != "false" {
			panic(fmt.Sprintf(`unexpected tag value: %s. expected untagged or "false"`, v))
		}
		return v == "false"
	})
	return copy
}

func (f *FolderConfiguration) SharedWith(device protocol.DeviceID) bool {
	for _, dev := range f.Devices {
		if dev.DeviceID == device {
Repository: syncthing/syncthing
File: lib/config/config.go (Go)

Code before:

	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
	DefaultPrimaryStunServers = []string{
		"stun.syncthing.net:3478",
	}
	DefaultSecondaryStunServers = []string{
		"stun.callwithus.com:3478",
		"stun.counterpath.com:3478",
		"stun.counterpath.net:3478",
		"stun.ekiga.net:3478",
		"stun.ideasip.com:3478",
		"stun.internetcalls.com:3478",
		"stun.schlund.de:3478",
		"stun.sipgate.net:10000",
		"stun.sipgate.net:3478",
		"stun.voip.aebc.com:3478",
		"stun.voiparound.com:3478",
		"stun.voipbuster.com:3478",
		"stun.voipstunt.com:3478",
		"stun.voxgratia.org:3478",
		"stun.xten.com:3478",
	}
)

var (
	errFolderIDEmpty   = errors.New("folder with empty ID in configuration")
	errFolderPathEmpty = errors.New("folder with empty path in configuration")
)

func New(myID protocol.DeviceID) Configuration {
	var cfg Configuration
	cfg.Version = CurrentVersion
	cfg.OriginalVersion = CurrentVersion

	util.SetDefaults(&cfg)
	util.SetDefaults(&cfg.Options)
	util.SetDefaults(&cfg.GUI)

	// Can't happen.
	if err := cfg.prepare(myID); err != nil {
		panic("bug: error in preparing new folder: " + err.Error())
	}

	return cfg
}

func NewWithFreePorts(myID protocol.DeviceID) (Configuration, error) {
	cfg := New(myID)

	port, err := getFreePort("127.0.0.1", DefaultGUIPort)
	if err != nil {

Review comment (suggestion):
```suggestion
	errFolderIDEmpty   = errors.New("folder has empty ID")
```

Code after:

	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
	DefaultPrimaryStunServers = []string{
		"stun.syncthing.net:3478",
	}
	DefaultSecondaryStunServers = []string{
		"stun.callwithus.com:3478",
		"stun.counterpath.com:3478",
		"stun.counterpath.net:3478",
		"stun.ekiga.net:3478",
		"stun.ideasip.com:3478",
		"stun.internetcalls.com:3478",
		"stun.schlund.de:3478",
		"stun.sipgate.net:10000",
		"stun.sipgate.net:3478",
		"stun.voip.aebc.com:3478",
		"stun.voiparound.com:3478",
		"stun.voipbuster.com:3478",
		"stun.voipstunt.com:3478",
		"stun.voxgratia.org:3478",
		"stun.xten.com:3478",
	}
)

var (
	errFolderIDEmpty     = errors.New("folder has empty ID")
	errFolderIDDuplicate = errors.New("folder has duplicate ID")
	errFolderPathEmpty   = errors.New("folder has empty path")
)

func New(myID protocol.DeviceID) Configuration {
	var cfg Configuration
	cfg.Version = CurrentVersion
	cfg.OriginalVersion = CurrentVersion

	util.SetDefaults(&cfg)
	util.SetDefaults(&cfg.Options)
	util.SetDefaults(&cfg.GUI)

	// Can't happen.
	if err := cfg.prepare(myID); err != nil {
		panic("bug: error in preparing new folder: " + err.Error())
	}

	return cfg
}

func NewWithFreePorts(myID protocol.DeviceID) (Configuration, error) {
	cfg := New(myID)

	port, err := getFreePort("127.0.0.1", DefaultGUIPort)
Repository: syncthing/syncthing
File: lib/config/config.go (Go)

Code before:
	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
	DefaultPrimaryStunServers = []string{
		"stun.syncthing.net:3478",
	}
	DefaultSecondaryStunServers = []string{
		"stun.callwithus.com:3478",
		"stun.counterpath.com:3478",
		"stun.counterpath.net:3478",
		"stun.ekiga.net:3478",
		"stun.ideasip.com:3478",
		"stun.internetcalls.com:3478",
		"stun.schlund.de:3478",
		"stun.sipgate.net:10000",
		"stun.sipgate.net:3478",
		"stun.voip.aebc.com:3478",
		"stun.voiparound.com:3478",
		"stun.voipbuster.com:3478",
		"stun.voipstunt.com:3478",
		"stun.voxgratia.org:3478",
		"stun.xten.com:3478",
	}
)

var (
	errFolderIDEmpty   = errors.New("folder with empty ID in configuration")
	errFolderPathEmpty = errors.New("folder with empty path in configuration")
)

func New(myID protocol.DeviceID) Configuration {
	var cfg Configuration
	cfg.Version = CurrentVersion
	cfg.OriginalVersion = CurrentVersion

	util.SetDefaults(&cfg)
	util.SetDefaults(&cfg.Options)
	util.SetDefaults(&cfg.GUI)

	// Can't happen.
	if err := cfg.prepare(myID); err != nil {
		panic("bug: error in preparing new folder: " + err.Error())
	}

	return cfg
}

func NewWithFreePorts(myID protocol.DeviceID) (Configuration, error) {
	cfg := New(myID)

	port, err := getFreePort("127.0.0.1", DefaultGUIPort)
	if err != nil {
		return Configuration{}, fmt.Errorf("get free port (GUI): %v", err)

Review comment (suggestion):
```suggestion
	errFolderPathEmpty = errors.New("folder has empty path")
```

Code after:
	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
	DefaultPrimaryStunServers = []string{
		"stun.syncthing.net:3478",
	}
	DefaultSecondaryStunServers = []string{
		"stun.callwithus.com:3478",
		"stun.counterpath.com:3478",
		"stun.counterpath.net:3478",
		"stun.ekiga.net:3478",
		"stun.ideasip.com:3478",
		"stun.internetcalls.com:3478",
		"stun.schlund.de:3478",
		"stun.sipgate.net:10000",
		"stun.sipgate.net:3478",
		"stun.voip.aebc.com:3478",
		"stun.voiparound.com:3478",
		"stun.voipbuster.com:3478",
		"stun.voipstunt.com:3478",
		"stun.voxgratia.org:3478",
		"stun.xten.com:3478",
	}
)

var (
	errFolderIDEmpty     = errors.New("folder has empty ID")
	errFolderIDDuplicate = errors.New("folder has duplicate ID")
	errFolderPathEmpty   = errors.New("folder has empty path")
)

func New(myID protocol.DeviceID) Configuration {
	var cfg Configuration
	cfg.Version = CurrentVersion
	cfg.OriginalVersion = CurrentVersion

	util.SetDefaults(&cfg)
	util.SetDefaults(&cfg.Options)
	util.SetDefaults(&cfg.GUI)

	// Can't happen.
	if err := cfg.prepare(myID); err != nil {
		panic("bug: error in preparing new folder: " + err.Error())
	}

	return cfg
}

func NewWithFreePorts(myID protocol.DeviceID) (Configuration, error) {
	cfg := New(myID)

	port, err := getFreePort("127.0.0.1", DefaultGUIPort)
	if err != nil {
Repository: syncthing/syncthing
File: lib/nat/registry.go (Go)

Code before:
	"sync"
	"time"
)

type DiscoverFunc func(renewal, timeout time.Duration) []Device

var providers []DiscoverFunc

func Register(provider DiscoverFunc) {
	providers = append(providers, provider)
}

func discoverAll(renewal, timeout time.Duration, stop chan struct{}) map[string]Device {
	wg := &sync.WaitGroup{}
	wg.Add(len(providers))

	c := make(chan Device)
	for _, discoverFunc := range providers {
		go func(f DiscoverFunc) {
			for _, dev := range f(renewal, timeout) {
				select {
				case c <- dev:
				case <-stop:
				}
			}
			wg.Done()
		}(discoverFunc)
	}

	nats := make(map[string]Device)

	for num := len(providers); len(nats) < num; {
		select {
		case dev := <-c:
			nats[dev.ID()] = dev
		case <-stop:
			return nil
		}
	}

	return nats
}


Review comment (suggestion):
I'd add an explicit return to avoid having to understand how long time the `f(...)` call might take for each device while exiting

Code after:
	"sync"
	"time"
)

type DiscoverFunc func(renewal, timeout time.Duration) []Device

var providers []DiscoverFunc

func Register(provider DiscoverFunc) {
	providers = append(providers, provider)
}

func discoverAll(renewal, timeout time.Duration, stop chan struct{}) map[string]Device {
	wg := &sync.WaitGroup{}
	wg.Add(len(providers))

	c := make(chan Device)
	done := make(chan struct{})

	for _, discoverFunc := range providers {
		go func(f DiscoverFunc) {
			defer wg.Done()
			for _, dev := range f(renewal, timeout) {
				select {
				case c <- dev:
				case <-stop:
					return
				}
			}
		}(discoverFunc)
	}

	nats := make(map[string]Device)

	go func() {
		defer close(done)
		for {
			select {
			case dev, ok := <-c:
				if !ok {
					return
				}
				nats[dev.ID()] = dev
			case <-stop:
				return
			}
		}
	}()

	wg.Wait()
	close(c)
Repository: syncthing/syncthing
File: lib/nat/registry.go (Go)

Code before:
	wg.Add(len(providers))

	c := make(chan Device)
	for _, discoverFunc := range providers {
		go func(f DiscoverFunc) {
			defer wg.Done()
			for _, dev := range f(renewal, timeout) {
				select {
				case c <- dev:
				case <-stop:
					return
				}
			}
		}(discoverFunc)
	}

	nats := make(map[string]Device)

	go func() {
		for {
			select {
			case dev, ok := <-c:
				if !ok {
					return
				}
				nats[dev.ID()] = dev
			case <-stop:
				return
			}
		}
	}()

	wg.Wait()
	close(c)

	return nats
}


Review comment (suggestion):
This makes a race, we don't know that the goroutine above is done modifying `nats` before returning it

Code after:
	wg.Add(len(providers))

	c := make(chan Device)
	done := make(chan struct{})

	for _, discoverFunc := range providers {
		go func(f DiscoverFunc) {
			defer wg.Done()
			for _, dev := range f(renewal, timeout) {
				select {
				case c <- dev:
				case <-stop:
					return
				}
			}
		}(discoverFunc)
	}

	nats := make(map[string]Device)

	go func() {
		defer close(done)
		for {
			select {
			case dev, ok := <-c:
				if !ok {
					return
				}
				nats[dev.ID()] = dev
			case <-stop:
				return
			}
		}
	}()

	wg.Wait()
	close(c)
	<-done

	return nats
}
Repository: syncthing/syncthing
File: lib/config/size.go (Go)

Code before:
// Copyright (C) 2017 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package config

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/syncthing/syncthing/lib/fs"
)

type Size struct {
	Value float64 `json:"value" xml:",chardata"`
	Unit  string  `json:"unit" xml:"unit,attr"`
}

func ParseSize(s string) (Size, error) {
	s = strings.TrimSpace(s)
	if len(s) == 0 {
		return Size{}, nil
	}

	var num, unit string
	for i := 0; i < len(s) && (s[i] >= '0' && s[i] <= '9' || s[i] == '.' || s[i] == ','); i++ {
		num = s[:i+1]
	}
	var i = len(num)
	for i < len(s) && s[i] == ' ' {
		i++
	}
	unit = s[i:]

	val, err := strconv.ParseFloat(num, 64)
	if err != nil {
		return Size{}, err
	}

	return Size{val, unit}, nil
}

func (s Size) BaseValue() float64 {
	unitPrefix := s.Unit
	if len(unitPrefix) > 1 {
		unitPrefix = unitPrefix[:1]
	}

	mult := 1.0
	switch unitPrefix {
	case "k", "K":
		mult = 1000
	case "m", "M":
		mult = 1000 * 1000
	case "g", "G":
		mult = 1000 * 1000 * 1000
	case "t", "T":
		mult = 1000 * 1000 * 1000 * 1000
	}

	return s.Value * mult
}

func (s Size) Percentage() bool {
	return strings.Contains(s.Unit, "%")
}

func (s Size) String() string {
	return fmt.Sprintf("%v %s", s.Value, s.Unit)
}

func (s *Size) ParseDefault(str string) error {
	sz, err := ParseSize(str)
	*s = sz
	return err
}

func CheckFreeSpace(req Size, usage fs.Usage) error {
	val := req.BaseValue()
	if val <= 0 {
		return nil
	}

	if req.Percentage() {
		freePct := (float64(usage.Free) / float64(usage.Total)) * 100
		if freePct < val {
			return fmt.Errorf("%f %% < %v", freePct, req)
		}
	} else if float64(usage.Free) < val {
		return fmt.Errorf("%v < %v", usage.Free, req)
	}

	return nil
}


Review comment (suggestion):
I'd prefer using `%.3g` instead of `%.1f` to consistently get 3 significant digits instead of a fixed digit after the comma, which will give various amounts of precision/total digits.

Code after:
}

func CheckFreeSpace(req Size, usage fs.Usage) error {
	val := req.BaseValue()
	if val <= 0 {
		return nil
	}

	if req.Percentage() {
		freePct := (float64(usage.Free) / float64(usage.Total)) * 100
		if freePct < val {
			return fmt.Errorf("%.1f %% < %v", freePct, req)
		}
	} else if float64(usage.Free) < val {
		return fmt.Errorf("%sB < %v", formatSI(usage.Free), req)
	}

	return nil
}

func formatSI(b int64) string {
	switch {
	case b < 1000:
		return fmt.Sprintf("%d ", b)
	case b < 1000*1000:
		return fmt.Sprintf("%.1f K", float64(b)/1000)
	case b < 1000*1000*1000:
		return fmt.Sprintf("%.1f M", float64(b)/(1000*1000))
	case b < 1000*1000*1000*1000:
		return fmt.Sprintf("%.1f G", float64(b)/(1000*1000*1000))
	default:
		return fmt.Sprintf("%.1f T", float64(b)/(1000*1000*1000*1000))
	}
}
Repository: syncthing/syncthing
File: lib/beacon/broadcast.go (Go)

Code before:
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package beacon

import (
	"net"
	"time"

	"github.com/syncthing/syncthing/lib/util"
)

func NewBroadcast(port int) Interface {
	c := newCast("broadcastBeacon")
	c.addReader(util.AsServiceWithError(func(stop chan struct{}) error {
		return writeBroadcasts(c.inbox, port, stop)
	}))
	c.addWriter(util.AsServiceWithError(func(stop chan struct{}) error {
		return readBroadcasts(c.outbox, port, stop)
	}))
	return c
}

func writeBroadcasts(inbox <-chan []byte, port int, stop chan struct{}) error {
	l.Debugln("starting writeBroadcasts")
	defer l.Debugln("stopping writeBroadcasts")

	conn, err := net.ListenUDP("udp4", nil)
	if err != nil {
		l.Debugln(err)
		return err
	}
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-stop:
		case <-done:
		}
		conn.Close()
	}()

	for {
		var bs []byte

Review comment (suggestion):
This could just take a function, and the addreader/writer wraps it in a service and passes in the right channel, and potentially a port (newCast could take a port)

Code after:
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package beacon

import (
	"net"
	"time"
)

func NewBroadcast(port int) Interface {
	c := newCast("broadcastBeacon")
	c.addReader(func(stop chan struct{}) error {
		return readBroadcasts(c.outbox, port, stop)
	})
	c.addWriter(func(stop chan struct{}) error {
		return writeBroadcasts(c.inbox, port, stop)
	})
	return c
}

func writeBroadcasts(inbox <-chan []byte, port int, stop chan struct{}) error {
	conn, err := net.ListenUDP("udp4", nil)
	if err != nil {
		l.Debugln(err)
		return err
	}
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-stop:
		case <-done:
		}
		conn.Close()
	}()

	for {
		var bs []byte
		select {
		case bs = <-inbox:
		case <-stop:
			return nil
		}
Repository: syncthing/syncthing
File: lib/beacon/multicast.go (Go)

Code before:
			if err != nil {
				l.Debugln(err, "on write to", gaddr, intf.Name)
				continue
			}

			l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)

			success++

			select {
			case <-stop:
				return nil
			default:
			}
		}

		if success == 0 {
			return err
		}
	}
}

func readMulticasts(outbox chan<- recv, addr string, stop chan struct{}) error {
	l.Debugln("starting readMulticasts")
	defer l.Debugln("stopping readMulticasts")

	gaddr, err := net.ResolveUDPAddr("udp6", addr)
	if err != nil {
		l.Debugln(err)
		return err
	}

	conn, err := net.ListenPacket("udp6", addr)
	if err != nil {
		l.Debugln(err)
		return err
	}
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-stop:
		case <-done:
		}
		conn.Close()
	}()

	intfs, err := net.Interfaces()
	if err != nil {
		l.Debugln(err)
		return err

Review comment (suggestion):
The cast could log this cruft I guess? It has the name

Code after:
			l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)

			success++

			select {
			case <-stop:
				return nil
			default:
			}
		}

		if success == 0 {
			return err
		}
	}
}

func readMulticasts(outbox chan<- recv, addr string, stop chan struct{}) error {
	gaddr, err := net.ResolveUDPAddr("udp6", addr)
	if err != nil {
		l.Debugln(err)
		return err
	}

	conn, err := net.ListenPacket("udp6", addr)
	if err != nil {
		l.Debugln(err)
		return err
	}
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-stop:
		case <-done:
		}
		conn.Close()
	}()

	intfs, err := net.Interfaces()
	if err != nil {
		l.Debugln(err)
		return err
	}

	pconn := ipv6.NewPacketConn(conn)
	joined := 0
	for _, intf := range intfs {
		err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
		if err != nil {
			l.Debugln("IPv6 join", intf.Name, "failed:", err)