Repository: pion/ice (go)

Issue:
Gather TURN/STUN candidates async
Currently we gather them one at a time. 

However if one TURN server is slow it can block gathering all the other ones.

@AeroNotix can offer more details/has context also

Pull request description:
Gather STUN/TURN URLs concurrently
Gather candidates of each type concurrently, this way a single slow
TURN/STUN server doesn't block the others.

We should make the entire gather process more concurrent and spawn each
gather in a separate goroutine eventually

Resolves #118 

Patch:
diff --git a/agent.go b/agent.go
index f163389..f1152ad 100644
--- a/agent.go
+++ b/agent.go
@@ -4,7 +4,6 @@ package ice
 
 import (
 	"context"
-	"fmt"
 	"math/rand"
 	"net"
 	"strings"
@@ -885,25 +884,35 @@ func (a *Agent) addRemoteCandidate(c Candidate) {
 	a.requestConnectivityCheck()
 }
 
-// addCandidate assumes you are holding the lock (must be execute using a.run)
-func (a *Agent) addCandidate(c Candidate) {
-	set := a.localCandidates[c.NetworkType()]
-	for _, candidate := range set {
-		if candidate.Equal(c) {
-			return
+func (a *Agent) addCandidate(c Candidate, candidateConn net.PacketConn) error {
+	return a.run(func(agent *Agent) {
+		c.start(a, candidateConn)
+
+		set := a.localCandidates[c.NetworkType()]
+		for _, candidate := range set {
+			if candidate.Equal(c) {
+				if err := c.close(); err != nil {
+					a.log.Warnf("Failed to close duplicate candidate: %v", err)
+				}
+				return
+			}
 		}
-	}
 
-	set = append(set, c)
-	a.localCandidates[c.NetworkType()] = set
+		set = append(set, c)
+		a.localCandidates[c.NetworkType()] = set
 
-	if remoteCandidates, ok := a.remoteCandidates[c.NetworkType()]; ok {
-		for _, remoteCandidate := range remoteCandidates {
-			a.addPair(c, remoteCandidate)
+		if remoteCandidates, ok := a.remoteCandidates[c.NetworkType()]; ok {
+			for _, remoteCandidate := range remoteCandidates {
+				a.addPair(c, remoteCandidate)
+			}
 		}
-	}
 
-	a.requestConnectivityCheck()
+		a.requestConnectivityCheck()
+
+		if a.onCandidateHdlr != nil {
+			go a.onCandidateHdlr(c)
+		}
+	})
 }
 
 // GetLocalCandidates returns the local candidates
@@ -1296,41 +1305,3 @@ func (a *Agent) GetRemoteCandidatesStats() []CandidateStats {
 	}
 	return <-resultChan
 }
-
-// Role represents ICE agent role, which can be controlling or controlled.
-type Role byte
-
-// UnmarshalText implements TextUnmarshaler.
-func (r *Role) UnmarshalText(text []byte) error {
-	switch string(text) {
-	case "controlling":
-		*r = Controlling
-	case "controlled":
-		*r = Controlled
-	default:
-		return fmt.Errorf("unknown role %q", text)
-	}
-	return nil
-}
-
-// MarshalText implements TextMarshaler.
-func (r Role) MarshalText() (text []byte, err error) {
-	return []byte(r.String()), nil
-}
-
-func (r Role) String() string {
-	switch r {
-	case Controlling:
-		return "controlling"
-	case Controlled:
-		return "controlled"
-	default:
-		return "unknown"
-	}
-}
-
-// Possible ICE agent roles.
-const (
-	Controlling Role = iota
-	Controlled
-)
diff --git a/gather.go b/gather.go
index 7d11565..0887bad 100644
--- a/gather.go
+++ b/gather.go
@@ -2,13 +2,11 @@ package ice
 
 import (
 	"fmt"
-	"math/rand"
 	"net"
 	"sync"
 	"time"
 
-	"github.com/pion/stun"
-	"github.com/pion/transport/vnet"
+	"github.com/pion/logging"
 	"github.com/pion/turn/v2"
 )
 
@@ -16,104 +14,16 @@ const (
 	stunGatherTimeout = time.Second * 5
 )
 
-func (a *Agent) localInterfaces(networkTypes []NetworkType) ([]net.IP, error) {
-	ips := []net.IP{}
-	ifaces, err := a.net.Interfaces()
-	if err != nil {
-		return ips, err
-	}
-
-	var IPv4Requested, IPv6Requested bool
-	for _, typ := range networkTypes {
-		if typ.IsIPv4() {
-			IPv4Requested = true
-		}
-
-		if typ.IsIPv6() {
-			IPv6Requested = true
-		}
-	}
-
-	for _, iface := range ifaces {
-		if iface.Flags&net.FlagUp == 0 {
-			continue // interface down
-		}
-		if iface.Flags&net.FlagLoopback != 0 {
-			continue // loopback interface
-		}
-
-		if a.interfaceFilter != nil && !a.interfaceFilter(iface.Name) {
-			continue
-		}
-
-		addrs, err := iface.Addrs()
-		if err != nil {
-			continue
-		}
-
-		for _, addr := range addrs {
-			var ip net.IP
-			switch addr := addr.(type) {
-			case *net.IPNet:
-				ip = addr.IP
-			case *net.IPAddr:
-				ip = addr.IP
-			}
-			if ip == nil || ip.IsLoopback() {
-				continue
-			}
-
-			if ipv4 := ip.To4(); ipv4 == nil {
-				if !IPv6Requested {
-					continue
-				} else if !isSupportedIPv6(ip) {
-					continue
-				}
-			} else if !IPv4Requested {
-				continue
-			}
-
-			ips = append(ips, ip)
-		}
-	}
-	return ips, nil
+type closeable interface {
+	Close() error
 }
 
-func (a *Agent) listenUDP(portMax, portMin int, network string, laddr *net.UDPAddr) (vnet.UDPPacketConn, error) {
-	if (laddr.Port != 0) || ((portMin == 0) && (portMax == 0)) {
-		return a.net.ListenUDP(network, laddr)
-	}
-	var i, j int
-	i = portMin
-	if i == 0 {
-		i = 1
-	}
-	j = portMax
-	if j == 0 {
-		j = 0xFFFF
-	}
-	if i > j {
-		return nil, ErrPort
+// Close a net.Conn and log if we have a failure
+func closeConnAndLog(c closeable, log logging.LeveledLogger, msg string) {
+	log.Warnf(msg)
+	if err := c.Close(); err != nil {
+		log.Warnf("Failed to close conn: %v", err)
 	}
-
-	portStart := rand.Intn(j-i+1) + i
-	portCurrent := portStart
-	for {
-		laddr = &net.UDPAddr{IP: laddr.IP, Port: portCurrent}
-		c, e := a.net.ListenUDP(network, laddr)
-		if e == nil {
-			return c, e
-		}
-		a.log.Debugf("failed to listen %s: %v", laddr.String(), e)
-		portCurrent++
-		if portCurrent > j {
-			portCurrent = i
-		}
-		if portCurrent == portStart {
-			break
-		}
-	}
-	return nil, ErrPort
 }
 
 // GatherCandidates initiates the trickle based gathering process.
@@ -130,7 +40,6 @@ func (a *Agent) GatherCandidates() error {
 		}
 
 		go a.gatherCandidates()
-
 		gatherErrChan <- nil
 	})
 	if runErr != nil {
@@ -156,6 +65,9 @@ func (a *Agent) gatherCandidates() {
 			a.gatherCandidatesLocal(a.networkTypes)
 		case CandidateTypeServerReflexive:
 			a.gatherCandidatesSrflx(a.urls, a.networkTypes)
+			if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeServerReflexive {
+				a.gatherCandidatesSrflxMapped(a.networkTypes)
+			}
 		case CandidateTypeRelay:
 			if err := a.gatherCandidatesRelay(a.urls); err != nil {
 				a.log.Errorf("Failed to gather relay candidates: %v\n", err)
@@ -181,16 +93,12 @@ func (a *Agent) gatherCandidates() {
 }
 
 func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) {
-	var wg sync.WaitGroup
-	defer wg.Wait()
-
-	localIPs, err := a.localInterfaces(networkTypes)
+	localIPs, err := localInterfaces(a.net, a.interfaceFilter, networkTypes)
 	if err != nil {
 		a.log.Warnf("failed to iterate local interfaces, host candidates will not be gathered %s", err)
 		return
 	}
 
-	wg.Add(len(localIPs) * len(supportedNetworks))
 	for _, ip := range localIPs {
 		mappedIP := ip
 		if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost {
@@ -201,327 +109,272 @@ func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) {
 			}
 		}
 
+		address := mappedIP.String()
+		if a.mDNSMode == MulticastDNSModeQueryAndGather {
+			address = a.mDNSName
+		}
+
 		for _, network := range supportedNetworks {
-			go func(network string, ip, mappedIP net.IP) {
-				defer wg.Done()
-				conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: ip, Port: 0})
-				if err != nil {
-					a.log.Warnf("could not listen %s %s\n", network, ip)
-					return
-				}
+			conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: ip, Port: 0})
+			if err != nil {
+				a.log.Warnf("could not listen %s %s\n", network, ip)
+				continue
+			}
 
-				address := mappedIP.String()
-				if a.mDNSMode == MulticastDNSModeQueryAndGather {
-					address = a.mDNSName
-				}
+			port := conn.LocalAddr().(*net.UDPAddr).Port
+			hostConfig := CandidateHostConfig{
+				Network:   network,
+				Address:   address,
+				Port:      port,
+				Component: ComponentRTP,
+			}
 
-				port := conn.LocalAddr().(*net.UDPAddr).Port
+			c, err := NewCandidateHost(&hostConfig)
+			if err != nil {
+				closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err))
+				continue
+			}
 
-				hostConfig := CandidateHostConfig{
-					Network:   network,
-					Address:   address,
-					Port:      port,
-					Component: ComponentRTP,
+			if a.mDNSMode == MulticastDNSModeQueryAndGather {
+				if err = c.setIP(ip); err != nil {
+					closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err))
+					continue
 				}
+			}
 
-				c, err := NewCandidateHost(&hostConfig)
-				if err != nil {
-					a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err)
-					return
+			if err := a.addCandidate(c, conn); err != nil {
+				if closeErr := c.close(); closeErr != nil {
+					a.log.Warnf("Failed to close candidate: %v", closeErr)
 				}
+				a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
+			}
+		}
+	}
+}
 
-				if a.mDNSMode == MulticastDNSModeQueryAndGather {
-					if err = c.setIP(ip); err != nil {
-						a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err)
-						return
-					}
-				}
+func (a *Agent) gatherCandidatesSrflxMapped(networkTypes []NetworkType) {
+	for _, networkType := range networkTypes {
+		network := networkType.String()
 
-				if err := a.run(func(agent *Agent) {
-					c.start(a, conn)
-					a.addCandidate(c)
+		conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
+		if err != nil {
+			a.log.Warnf("Failed to listen %s: %v\n", network, err)
+			continue
+		}
 
-					if a.onCandidateHdlr != nil {
-						go a.onCandidateHdlr(c)
-					}
-				}); err != nil {
-					a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
-				}
-			}(network, ip, mappedIP)
+		laddr := conn.LocalAddr().(*net.UDPAddr)
+		mappedIP, err := a.extIPMapper.findExternalIP(laddr.IP.String())
+		if err != nil {
+			closeConnAndLog(conn, a.log, fmt.Sprintf("1:1 NAT mapping is enabled but no external IP is found for %s\n", laddr.IP.String()))
+			continue
 		}
-	}
-}
 
-func (a *Agent) gatherCandidatesSrflx(urls []*URL, networkTypes []NetworkType) {
-	var stunURLs []*URL
-	for _, url := range urls {
-		if url.Scheme == SchemeTypeSTUN {
-			stunURLs = append(stunURLs, url)
+		srflxConfig := CandidateServerReflexiveConfig{
+			Network:   network,
+			Address:   mappedIP.String(),
+			Port:      laddr.Port,
+			Component: ComponentRTP,
+			RelAddr:   laddr.IP.String(),
+			RelPort:   laddr.Port,
+		}
+		c, err := NewCandidateServerReflexive(&srflxConfig)
+		if err != nil {
+			closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create server reflexive candidate: %s %s %d: %v\n",
+				network,
+				mappedIP.String(),
+				laddr.Port,
+				err))
+			continue
+		}
+
+		if err := a.addCandidate(c, conn); err != nil {
+			if closeErr := c.close(); closeErr != nil {
+				a.log.Warnf("Failed to close candidate: %v", closeErr)
+			}
+			a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
 		}
 	}
+}
 
+func (a *Agent) gatherCandidatesSrflx(urls []*URL, networkTypes []NetworkType) {
+	var wg sync.WaitGroup
 	for _, networkType := range networkTypes {
-		network := networkType.String()
-		if len(stunURLs) > 0 {
-			for _, url := range stunURLs {
-				if url.Scheme != SchemeTypeSTUN {
-					continue
-				}
+		for i := range urls {
+			if urls[i].Scheme != SchemeTypeSTUN {
+				continue
+			}
 
+			wg.Add(1)
+			go func(url URL, network string) {
+				defer wg.Done()
 				hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port)
 				serverAddr, err := a.net.ResolveUDPAddr(network, hostPort)
 				if err != nil {
 					a.log.Warnf("failed to resolve stun host: %s: %v", hostPort, err)
-					continue
+					return
 				}
 
-				conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
+				conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
 				if err != nil {
-					a.log.Warnf("Failed to listen for %s: %v\n", serverAddr.String(), err)
-					continue
+					closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to listen for %s: %v\n", serverAddr.String(), err))
+					return
 				}
 
 				xoraddr, err := getXORMappedAddr(conn, serverAddr, stunGatherTimeout)
 				if err != nil {
-					a.log.Warnf("could not get server reflexive address %s %s: %v\n", network, url, err)
-					continue
+					closeConnAndLog(conn, a.log, fmt.Sprintf("could not get server reflexive address %s %s: %v\n", network, url, err))
+					return
 				}
 
-				laddr := conn.LocalAddr().(*net.UDPAddr)
 				ip := xoraddr.IP
 				port := xoraddr.Port
-				relIP := laddr.IP.String()
-				relPort := laddr.Port
 
+				laddr := conn.LocalAddr().(*net.UDPAddr)
 				srflxConfig := CandidateServerReflexiveConfig{
 					Network:   network,
 					Address:   ip.String(),
 					Port:      port,
 					Component: ComponentRTP,
-					RelAddr:   relIP,
-					RelPort:   relPort,
+					RelAddr:   laddr.IP.String(),
+					RelPort:   laddr.Port,
 				}
 				c, err := NewCandidateServerReflexive(&srflxConfig)
 				if err != nil {
-					a.log.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err)
-					continue
+					closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err))
+					return
 				}
 
-				if err := a.run(func(agent *Agent) {
-					c.start(a, conn)
-					a.addCandidate(c)
-
-					if a.onCandidateHdlr != nil {
-						go a.onCandidateHdlr(c)
+				if err := a.addCandidate(c, conn); err != nil {
+					if closeErr := c.close(); closeErr != nil {
+						a.log.Warnf("Failed to close candidate: %v", closeErr)
 					}
-				}); err != nil {
 					a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
 				}
-			}
-		} else if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeServerReflexive {
-			conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
-			if err != nil {
-				a.log.Warnf("Failed to listen %s: %v\n", network, err)
-				continue
-			}
-
-			laddr := conn.LocalAddr().(*net.UDPAddr)
-			mappedIP, err := a.extIPMapper.findExternalIP(laddr.IP.String())
-			if err != nil {
-				a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s\n", laddr.IP.String())
-				continue
-			}
-
-			srflxConfig := CandidateServerReflexiveConfig{
-				Network:   network,
-				Address:   mappedIP.String(),
-				Port:      laddr.Port,
-				Component: ComponentRTP,
-				RelAddr:   laddr.IP.String(),
-				RelPort:   laddr.Port,
-			}
-			c, err := NewCandidateServerReflexive(&srflxConfig)
-			if err != nil {
-				a.log.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n",
-					network,
-					mappedIP.String(),
-					laddr.Port,
-					err)
-				continue
-			}
-
-			if err := a.run(func(agent *Agent) {
-				c.start(a, conn)
-				a.addCandidate(c)
-
-				if a.onCandidateHdlr != nil {
-					go a.onCandidateHdlr(c)
-				}
-			}); err != nil {
-				a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
-			}
+			}(*urls[i], networkType.String())
 		}
 	}
+
+	// Block until all STUN URLs have been gathered (or timed out)
+	wg.Wait()
 }
 
 func (a *Agent) gatherCandidatesRelay(urls []*URL) error {
+	var wg sync.WaitGroup
+
 	network := NetworkTypeUDP4.String() // TODO IPv6
-	for _, url := range urls {
+	for i := range urls {
 		switch {
-		case url.Scheme != SchemeTypeTURN:
+		case urls[i].Scheme != SchemeTypeTURN:
 			continue
-		case url.Username == "":
+		case urls[i].Username == "":
 			return ErrUsernameEmpty
-		case url.Password == "":
+		case urls[i].Password == "":
 			return ErrPasswordEmpty
 		}
 
-		TURNServerAddr := fmt.Sprintf("%s:%d", url.Host, url.Port)
-		var (
-			locConn net.PacketConn
-			err     error
-			RelAddr string
-			RelPort int
-		)
-
-		if url.Proto == ProtoTypeUDP {
-			locConn, err = a.net.ListenPacket(network, "0.0.0.0:0")
-			if err != nil {
-				return err
-			}
-
-			RelAddr = locConn.LocalAddr().(*net.UDPAddr).IP.String()
-			RelPort = locConn.LocalAddr().(*net.UDPAddr).Port
-		} else {
+		wg.Add(1)
+		go func(url URL) {
+			defer wg.Done()
+			TURNServerAddr := fmt.Sprintf("%s:%d", url.Host, url.Port)
 			var (
-				tcpAddr *net.TCPAddr
-				tcpConn *net.TCPConn
+				locConn net.PacketConn
+				err     error
+				RelAddr string
+				RelPort int
 			)
 
-			tcpAddr, err = net.ResolveTCPAddr(NetworkTypeTCP4.String(), TURNServerAddr)
-			if err != nil {
-				return err
-			}
-
-			tcpConn, err = net.DialTCP(NetworkTypeTCP4.String(), nil, tcpAddr)
-			if err != nil {
-				return err
-			}
+			if url.Proto == ProtoTypeUDP {
+				locConn, err = a.net.ListenPacket(network, "0.0.0.0:0")
+				if err != nil {
+					a.log.Warnf("Failed to listen %s: %v\n", network, err)
+					return
+				}
 
-			RelAddr = tcpConn.LocalAddr().(*net.TCPAddr).IP.String()
-			RelPort = tcpConn.LocalAddr().(*net.TCPAddr).Port
-			locConn = turn.NewSTUNConn(tcpConn)
-		}
+				RelAddr = locConn.LocalAddr().(*net.UDPAddr).IP.String()
+				RelPort = locConn.LocalAddr().(*net.UDPAddr).Port
+			} else {
+				var (
+					tcpAddr *net.TCPAddr
+					tcpConn *net.TCPConn
+				)
 
-		client, err := turn.NewClient(&turn.ClientConfig{
-			TURNServerAddr: TURNServerAddr,
-			Conn:           locConn,
-			Username:       url.Username,
-			Password:       url.Password,
-			LoggerFactory:  a.loggerFactory,
-			Net:            a.net,
-		})
-		if err != nil {
-			return err
-		}
+				tcpAddr, err = net.ResolveTCPAddr(NetworkTypeTCP4.String(), TURNServerAddr)
+				if err != nil {
+					a.log.Warnf("Failed to resolve TCP Addr %s: %v\n", TURNServerAddr, err)
+					return
+				}
 
-		err = client.Listen()
-		if err != nil {
-			return err
-		}
+				tcpConn, err = net.DialTCP(NetworkTypeTCP4.String(), nil, tcpAddr)
+				if err != nil {
+					a.log.Warnf("Failed to Dial TCP Addr %s: %v\n", TURNServerAddr, err)
+					return
+				}
 
-		relayConn, err := client.Allocate()
-		if err != nil {
-			return err
-		}
+				RelAddr = tcpConn.LocalAddr().(*net.TCPAddr).IP.String()
+				RelPort = tcpConn.LocalAddr().(*net.TCPAddr).Port
+				locConn = turn.NewSTUNConn(tcpConn)
+			}
 
-		raddr := relayConn.LocalAddr().(*net.UDPAddr)
+			client, err := turn.NewClient(&turn.ClientConfig{
+				TURNServerAddr: TURNServerAddr,
+				Conn:           locConn,
+				Username:       url.Username,
+				Password:       url.Password,
+				LoggerFactory:  a.loggerFactory,
+				Net:            a.net,
+			})
+			if err != nil {
+				closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to build new turn.Client %s %s\n", TURNServerAddr, err))
+				return
+			}
 
-		relayConfig := CandidateRelayConfig{
-			Network:   network,
-			Component: ComponentRTP,
-			Address:   raddr.IP.String(),
-			Port:      raddr.Port,
-			RelAddr:   RelAddr,
-			RelPort:   RelPort,
-			OnClose: func() error {
+			if err = client.Listen(); err != nil {
 				client.Close()
-				return locConn.Close()
-			},
-		}
-		candidate, err := NewCandidateRelay(&relayConfig)
-		if err != nil {
-			a.log.Warnf("Failed to create relay candidate: %s %s: %v\n",
-				network, raddr.String(), err)
-			continue
-		}
+				closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to listen on turn.Client %s %s\n", TURNServerAddr, err))
+				return
+			}
 
-		if err := a.run(func(agent *Agent) {
-			candidate.start(a, relayConn)
-			a.addCandidate(candidate)
+			relayConn, err := client.Allocate()
+			if err != nil {
+				client.Close()
+				closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to allocate on turn.Client %s %s\n", TURNServerAddr, err))
+				return
+			}
 
-			if a.onCandidateHdlr != nil {
-				go a.onCandidateHdlr(candidate)
+			raddr := relayConn.LocalAddr().(*net.UDPAddr)
+			relayConfig := CandidateRelayConfig{
+				Network:   network,
+				Component: ComponentRTP,
+				Address:   raddr.IP.String(),
+				Port:      raddr.Port,
+				RelAddr:   RelAddr,
+				RelPort:   RelPort,
+				OnClose: func() error {
+					client.Close()
+					return locConn.Close()
+				},
 			}
-		}); err != nil {
-			a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
-		}
-	}
+			candidate, err := NewCandidateRelay(&relayConfig)
+			if err != nil {
+				if relayConErr := relayConn.Close(); relayConErr != nil {
+					a.log.Warnf("Failed to close relay %v", relayConErr)
+				}
 
-	return nil
-}
+				client.Close()
+				closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to create relay candidate: %s %s: %v\n", network, raddr.String(), err))
+				return
+			}
 
-// getXORMappedAddr initiates a stun requests to serverAddr using conn, reads the response and returns
-// the XORMappedAddress returned by the stun server.
-//
-// Adapted from stun v0.2.
-func getXORMappedAddr(conn net.PacketConn, serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) {
-	if deadline > 0 {
-		if err := conn.SetReadDeadline(time.Now().Add(deadline)); err != nil {
-			return nil, err
-		}
-	}
-	defer func() {
-		if deadline > 0 {
-			_ = conn.SetReadDeadline(time.Time{})
-		}
-	}()
-	resp, err := stunRequest(
-		func(p []byte) (int, error) {
-			n, _, errr := conn.ReadFrom(p)
-			return n, errr
-		},
-		func(b []byte) (int, error) {
-			return conn.WriteTo(b, serverAddr)
-		},
-	)
-	if err != nil {
-		return nil, err
-	}
-	var addr stun.XORMappedAddress
-	if err = addr.GetFrom(resp); err != nil {
-		return nil, fmt.Errorf("failed to get XOR-MAPPED-ADDRESS response: %v", err)
+			if err := a.addCandidate(candidate, relayConn); err != nil {
+				if closeErr := candidate.close(); closeErr != nil {
+					a.log.Warnf("Failed to close candidate: %v", closeErr)
+				}
+				a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
+			}
+		}(*urls[i])
 	}
-	return &addr, nil
-}
 
-func stunRequest(read func([]byte) (int, error), write func([]byte) (int, error)) (*stun.Message, error) {
-	req, err := stun.Build(stun.BindingRequest, stun.TransactionID)
-	if err != nil {
-		return nil, err
-	}
-	if _, err = write(req.Raw); err != nil {
-		return nil, err
-	}
-	const maxMessageSize = 1280
-	bs := make([]byte, maxMessageSize)
-	n, err := read(bs)
-	if err != nil {
-		return nil, err
-	}
-	res := &stun.Message{Raw: bs[:n]}
-	if err := res.Decode(); err != nil {
-		return nil, err
-	}
-	return res, nil
+	// Block until all STUN URLs have been gathered (or timed out)
+	wg.Wait()
+	return nil
 }
diff --git a/role.go b/role.go
new file mode 100644
index 0000000..c4fa420
--- /dev/null
+++ b/role.go
@@ -0,0 +1,41 @@
+package ice
+
+import "fmt"
+
+// Role represents ICE agent role, which can be controlling or controlled.
+type Role byte
+
+// Possible ICE agent roles.
+const (
+	Controlling Role = iota
+	Controlled
+)
+
+// UnmarshalText implements TextUnmarshaler.
+func (r *Role) UnmarshalText(text []byte) error {
+	switch string(text) {
+	case "controlling":
+		*r = Controlling
+	case "controlled":
+		*r = Controlled
+	default:
+		return fmt.Errorf("unknown role %q", text)
+	}
+	return nil
+}
+
+// MarshalText implements TextMarshaler.
+func (r Role) MarshalText() (text []byte, err error) {
+	return []byte(r.String()), nil
+}
+
+func (r Role) String() string {
+	switch r {
+	case Controlling:
+		return "controlling"
+	case Controlled:
+		return "controlled"
+	default:
+		return "unknown"
+	}
+}
diff --git a/util.go b/util.go
index d659ff3..fae3b87 100644
--- a/util.go
+++ b/util.go
@@ -6,6 +6,10 @@ import (
 	"net"
 	"sync/atomic"
 	"time"
+
+	"github.com/pion/logging"
+	"github.com/pion/stun"
+	"github.com/pion/transport/vnet"
 )
 
 type atomicError struct{ v atomic.Value }
@@ -87,3 +91,158 @@ func generateRandString(prefix, sufix string) (string, error) {
 
 	return fmt.Sprintf("%s%X-%X-%X-%X-%X%s", prefix, b[0:4], b[4:6], b[6:8], b[8:10], b[10:], sufix), nil
 }
+
+// getXORMappedAddr initiates a stun requests to serverAddr using conn, reads the response and returns
+// the XORMappedAddress returned by the stun server.
+//
+// Adapted from stun v0.2.
+func getXORMappedAddr(conn net.PacketConn, serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) {
+	if deadline > 0 {
+		if err := conn.SetReadDeadline(time.Now().Add(deadline)); err != nil {
+			return nil, err
+		}
+	}
+	defer func() {
+		if deadline > 0 {
+			_ = conn.SetReadDeadline(time.Time{})
+		}
+	}()
+	resp, err := stunRequest(
+		func(p []byte) (int, error) {
+			n, _, errr := conn.ReadFrom(p)
+			return n, errr
+		},
+		func(b []byte) (int, error) {
+			return conn.WriteTo(b, serverAddr)
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+	var addr stun.XORMappedAddress
+	if err = addr.GetFrom(resp); err != nil {
+		return nil, fmt.Errorf("failed to get XOR-MAPPED-ADDRESS response: %v", err)
+	}
+	return &addr, nil
+}
+
+func stunRequest(read func([]byte) (int, error), write func([]byte) (int, error)) (*stun.Message, error) {
+	req, err := stun.Build(stun.BindingRequest, stun.TransactionID)
+	if err != nil {
+		return nil, err
+	}
+	if _, err = write(req.Raw); err != nil {
+		return nil, err
+	}
+	const maxMessageSize = 1280
+	bs := make([]byte, maxMessageSize)
+	n, err := read(bs)
+	if err != nil {
+		return nil, err
+	}
+	res := &stun.Message{Raw: bs[:n]}
+	if err := res.Decode(); err != nil {
+		return nil, err
+	}
+	return res, nil
+}
+
+func localInterfaces(vnet *vnet.Net, interfaceFilter func(string) bool, networkTypes []NetworkType) ([]net.IP, error) {
+	ips := []net.IP{}
+	ifaces, err := vnet.Interfaces()
+	if err != nil {
+		return ips, err
+	}
+
+	var IPv4Requested, IPv6Requested bool
+	for _, typ := range networkTypes {
+		if typ.IsIPv4() {
+			IPv4Requested = true
+		}
+
+		if typ.IsIPv6() {
+			IPv6Requested = true
+		}
+	}
+
+	for _, iface := range ifaces {
+		if iface.Flags&net.FlagUp == 0 {
+			continue // interface down
+		}
+		if iface.Flags&net.FlagLoopback != 0 {
+			continue // loopback interface
+		}
+
+		if interfaceFilter != nil && !interfaceFilter(iface.Name) {
+			continue
+		}
+
+		addrs, err := iface.Addrs()
+		if err != nil {
+			continue
+		}
+
+		for _, addr := range addrs {
+			var ip net.IP
+			switch addr := addr.(type) {
+			case *net.IPNet:
+				ip = addr.IP
+			case *net.IPAddr:
+				ip = addr.IP
+			}
+			if ip == nil || ip.IsLoopback() {
+				continue
+			}
+
+			if ipv4 := ip.To4(); ipv4 == nil {
+				if !IPv6Requested {
+					continue
+				} else if !isSupportedIPv6(ip) {
+					continue
+				}
+			} else if !IPv4Requested {
+				continue
+			}
+
+			ips = append(ips, ip)
+		}
+	}
+	return ips, nil
+}
+
+func listenUDPInPortRange(vnet *vnet.Net, log logging.LeveledLogger, portMax, portMin int, network string, laddr *net.UDPAddr) (vnet.UDPPacketConn, error) {
+	if (laddr.Port != 0) || ((portMin == 0) && (portMax == 0)) {
+		return vnet.ListenUDP(network, laddr)
+	}
+	var i, j int
+	i = portMin
+	if i == 0 {
+		i = 1
+	}
+	j = portMax
+	if j == 0 {
+		j = 0xFFFF
+	}
+	if i > j {
+		return nil, ErrPort
+	}
+
+	portStart := rand.Intn(j-i+1) + i
+	portCurrent := portStart
+	for {
+		laddr = &net.UDPAddr{IP: laddr.IP, Port: portCurrent}
+		c, e := vnet.ListenUDP(network, laddr)
+		if e == nil {
+			return c, e
+		}
+		log.Debugf("failed to listen %s: %v", laddr.String(), e)
+		portCurrent++
+		if portCurrent > j {
+			portCurrent = i
+		}
+		if portCurrent == portStart {
+			break
+		}
+	}
+	return nil, ErrPort
+}


Test patch:
diff --git a/candidate_server_reflexive_test.go b/candidate_server_reflexive_test.go
index 6183414..f2fa052 100644
--- a/candidate_server_reflexive_test.go
+++ b/candidate_server_reflexive_test.go
@@ -73,13 +73,7 @@ func TestServerReflexiveOnlyConnection(t *testing.T) {
 	<-aConnected
 	<-bConnected
 
-	if err = aAgent.Close(); err != nil {
-		t.Fatal(err)
-	}
-	if err = bAgent.Close(); err != nil {
-		t.Fatal(err)
-	}
-	if err = server.Close(); err != nil {
-		t.Fatal(err)
-	}
+	assert.NoError(t, aAgent.Close())
+	assert.NoError(t, bAgent.Close())
+	assert.NoError(t, server.Close())
 }
diff --git a/gather_test.go b/gather_test.go
index 2bd0831..96e855e 100644
--- a/gather_test.go
+++ b/gather_test.go
@@ -3,58 +3,43 @@
 package ice
 
 import (
+	"context"
 	"net"
 	"reflect"
 	"sort"
 	"strconv"
 	"testing"
+	"time"
 
+	"github.com/pion/transport/test"
+	"github.com/pion/turn/v2"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestListenUDP(t *testing.T) {
 	a, err := NewAgent(&AgentConfig{})
-	if err != nil {
-		t.Fatalf("Failed to create agent: %s", err)
-	}
+	assert.NoError(t, err)
 
-	localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
-	if len(localIPs) == 0 {
-		t.Fatal("localInterfaces found no interfaces, unable to test")
-	} else if err != nil {
-		t.Fatal(err)
-	}
+	localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
+	assert.NotEqual(t, len(localIPs), 0, "localInterfaces found no interfaces, unable to test")
+	assert.NoError(t, err)
 
 	ip := localIPs[0]
 
-	conn, err := a.listenUDP(0, 0, udp, &net.UDPAddr{IP: ip, Port: 0})
-	if err != nil {
-		t.Fatalf("listenUDP error with no port restriction %v", err)
-	} else if conn == nil {
-		t.Fatalf("listenUDP error with no port restriction return a nil conn")
-	}
+	conn, err := listenUDPInPortRange(a.net, a.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0})
+	assert.NoError(t, err, "listenUDP error with no port restriction")
+	assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
 
-	_, err = a.listenUDP(4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
-	if err == nil {
-		t.Fatal("listenUDP with invalid port range did not fail")
-	}
-	if err != ErrPort {
-		t.Fatal("listenUDP with invalid port range did not return ErrPort")
-	}
+	_, err = listenUDPInPortRange(a.net, a.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
+	assert.Equal(t, err, ErrPort, "listenUDP with invalid port range did not return ErrPort")
 
-	conn, err = a.listenUDP(5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
-	if err != nil {
-		t.Fatalf("listenUDP error with no port restriction %v", err)
-	} else if conn == nil {
-		t.Fatalf("listenUDP error with no port restriction return a nil conn")
-	}
+	conn, err = listenUDPInPortRange(a.net, a.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
+	assert.NoError(t, err, "listenUDP error with no port restriction")
+	assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
 
 	_, port, err := net.SplitHostPort(conn.LocalAddr().String())
-	if err != nil {
-		t.Fatal(err)
-	} else if port != "5000" {
-		t.Fatalf("listenUDP with port restriction of 5000 listened on incorrect port (%s)", port)
-	}
+	assert.NoError(t, err)
+	assert.Equal(t, port, "5000", "listenUDP with port restriction of 5000 listened on incorrect port")
 
 	portMin := 5100
 	portMax := 5109
@@ -62,12 +47,9 @@ func TestListenUDP(t *testing.T) {
 	result := make([]int, 0, total)
 	portRange := make([]int, 0, total)
 	for i := 0; i < total; i++ {
-		conn, err = a.listenUDP(portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
-		if err != nil {
-			t.Fatalf("listenUDP error with no port restriction %v", err)
-		} else if conn == nil {
-			t.Fatalf("listenUDP error with no port restriction return a nil conn")
-		}
+		conn, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
+		assert.NoError(t, err, "listenUDP error with no port restriction")
+		assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
 
 		_, port, err = net.SplitHostPort(conn.LocalAddr().String())
 		if err != nil {
@@ -87,13 +69,160 @@ func TestListenUDP(t *testing.T) {
 	if !reflect.DeepEqual(result, portRange) {
 		t.Fatalf("listenUDP with port restriction [%d, %d], got:%v, want:%v", portMin, portMax, result, portRange)
 	}
-	_, err = a.listenUDP(portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
-	if err == nil {
-		t.Fatalf("listenUDP with port restriction [%d, %d], should return error", portMin, portMax)
-	}
-	if err != ErrPort {
-		t.Fatalf("listenUDP with port restriction [%d, %d], did not return ErrPort", portMin, portMax)
+	_, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
+	assert.Equal(t, err, ErrPort, "listenUDP with port restriction [%d, %d], did not return ErrPort", portMin, portMax)
+
+	assert.NoError(t, a.Close())
+}
+
+// Assert that STUN gathering is done concurrently
+func TestSTUNConcurrency(t *testing.T) {
+	lim := test.TimeOut(time.Second * 30)
+	defer lim.Stop()
+
+	report := test.CheckRoutines(t)
+	defer report()
+
+	serverPort := randomPort(t)
+	serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort))
+	assert.NoError(t, err)
+
+	server, err := turn.NewServer(turn.ServerConfig{
+		Realm:       "pion.ly",
+		AuthHandler: optimisticAuthHandler,
+		PacketConnConfigs: []turn.PacketConnConfig{
+			{
+				PacketConn:            serverListener,
+				RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
+			},
+		},
+	})
+	assert.NoError(t, err)
+
+	urls := []*URL{}
+	for i := 0; i <= 10; i++ {
+		urls = append(urls, &URL{
+			Scheme: SchemeTypeSTUN,
+			Host:   "127.0.0.1",
+			Port:   serverPort + 1,
+		})
 	}
+	urls = append(urls, &URL{
+		Scheme: SchemeTypeSTUN,
+		Host:   "127.0.0.1",
+		Port:   serverPort,
+	})
+
+	a, err := NewAgent(&AgentConfig{
+		NetworkTypes:   supportedNetworkTypes,
+		Trickle:        true,
+		Urls:           urls,
+		CandidateTypes: []CandidateType{CandidateTypeServerReflexive},
+	})
+	assert.NoError(t, err)
+
+	candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
+	assert.NoError(t, a.OnCandidate(func(c Candidate) {
+		if c != nil {
+			candidateGatheredFunc()
+		}
+	}))
+	assert.NoError(t, a.GatherCandidates())
+
+	<-candidateGathered.Done()
 
 	assert.NoError(t, a.Close())
+	assert.NoError(t, server.Close())
+}
+
+// Assert that TURN gathering is done concurrently
+func TestTURNConcurrency(t *testing.T) {
+	lim := test.TimeOut(time.Second * 30)
+	defer lim.Stop()
+
+	report := test.CheckRoutines(t)
+	defer report()
+
+	runTest := func(protocol ProtoType, scheme SchemeType, packetConn net.PacketConn, listener net.Listener, serverPort int) {
+		packetConnConfigs := []turn.PacketConnConfig{}
+		if packetConn != nil {
+			packetConnConfigs = append(packetConnConfigs, turn.PacketConnConfig{
+				PacketConn:            packetConn,
+				RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
+			})
+		}
+
+		listenerConfigs := []turn.ListenerConfig{}
+		if listener != nil {
+			listenerConfigs = append(listenerConfigs, turn.ListenerConfig{
+				Listener:              listener,
+				RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
+			})
+		}
+
+		server, err := turn.NewServer(turn.ServerConfig{
+			Realm:             "pion.ly",
+			AuthHandler:       optimisticAuthHandler,
+			PacketConnConfigs: packetConnConfigs,
+			ListenerConfigs:   listenerConfigs,
+		})
+		assert.NoError(t, err)
+
+		urls := []*URL{}
+		for i := 0; i <= 10; i++ {
+			urls = append(urls, &URL{
+				Scheme:   scheme,
+				Host:     "127.0.0.1",
+				Username: "username",
+				Password: "password",
+				Proto:    protocol,
+				Port:     serverPort + 1,
+			})
+		}
+		urls = append(urls, &URL{
+			Scheme:   scheme,
+			Host:     "127.0.0.1",
+			Username: "username",
+			Password: "password",
+			Proto:    protocol,
+			Port:     serverPort,
+		})
+
+		a, err := NewAgent(&AgentConfig{
+			NetworkTypes:   supportedNetworkTypes,
+			Trickle:        true,
+			Urls:           urls,
+			CandidateTypes: []CandidateType{CandidateTypeRelay},
+		})
+		assert.NoError(t, err)
+
+		candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
+		assert.NoError(t, a.OnCandidate(func(c Candidate) {
+			if c != nil {
+				candidateGatheredFunc()
+			}
+		}))
+		assert.NoError(t, a.GatherCandidates())
+
+		<-candidateGathered.Done()
+
+		assert.NoError(t, a.Close())
+		assert.NoError(t, server.Close())
+	}
+
+	t.Run("UDP Relay", func(t *testing.T) {
+		serverPort := randomPort(t)
+		serverListener, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(serverPort))
+		assert.NoError(t, err)
+
+		runTest(ProtoTypeUDP, SchemeTypeTURN, serverListener, nil, serverPort)
+	})
+
+	t.Run("TURN Relay", func(t *testing.T) {
+		serverPort := randomPort(t)
+		serverListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(serverPort))
+		assert.NoError(t, err)
+
+		runTest(ProtoTypeTCP, SchemeTypeTURN, nil, serverListener, serverPort)
+	})
 }
diff --git a/gather_vnet_test.go b/gather_vnet_test.go
index 6ad9678..aaea4b1 100644
--- a/gather_vnet_test.go
+++ b/gather_vnet_test.go
@@ -26,7 +26,7 @@ func TestVNetGather(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
+		localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
 		if len(localIPs) > 0 {
 			t.Fatal("should return no local IP")
 		} else if err != nil {
@@ -66,7 +66,7 @@ func TestVNetGather(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
+		localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
 		if len(localIPs) == 0 {
 			t.Fatal("should have one local IP")
 		} else if err != nil {
@@ -109,7 +109,7 @@ func TestVNetGather(t *testing.T) {
 			t.Fatalf("Failed to create agent: %s", err)
 		}
 
-		localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
+		localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
 		if len(localIPs) == 0 {
 			t.Fatal("localInterfaces found no interfaces, unable to test")
 		} else if err != nil {
@@ -118,7 +118,7 @@ func TestVNetGather(t *testing.T) {
 
 		ip := localIPs[0]
 
-		conn, err := a.listenUDP(0, 0, udp, &net.UDPAddr{IP: ip, Port: 0})
+		conn, err := listenUDPInPortRange(a.net, a.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0})
 		if err != nil {
 			t.Fatalf("listenUDP error with no port restriction %v", err)
 		} else if conn == nil {
@@ -129,12 +129,12 @@ func TestVNetGather(t *testing.T) {
 			t.Fatalf("failed to close conn")
 		}
 
-		_, err = a.listenUDP(4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
+		_, err = listenUDPInPortRange(a.net, a.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
 		if err != ErrPort {
 			t.Fatal("listenUDP with invalid port range did not return ErrPort")
 		}
 
-		conn, err = a.listenUDP(5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
+		conn, err = listenUDPInPortRange(a.net, a.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
 		if err != nil {
 			t.Fatalf("listenUDP error with no port restriction %v", err)
 		} else if conn == nil {
@@ -384,7 +384,7 @@ func TestVNetGatherWithInterfaceFilter(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
+		localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
 		if err != nil {
 			t.Fatal(err)
 		} else if len(localIPs) != 0 {
@@ -404,7 +404,7 @@ func TestVNetGatherWithInterfaceFilter(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		localIPs, err := a.localInterfaces([]NetworkType{NetworkTypeUDP4})
+		localIPs, err := localInterfaces(a.net, a.interfaceFilter, []NetworkType{NetworkTypeUDP4})
 		if err != nil {
 			t.Fatal(err)
 		} else if len(localIPs) == 0 {