@@ -282,6 +282,8 @@ class Connection:
282282 :param turn_transport: The transport for TURN server, `"udp"` or `"tcp"`.
283283 :param use_ipv4: Whether to use IPv4 candidates.
284284 :param use_ipv6: Whether to use IPv6 candidates.
285+ :param port_lower: The lowest port to bind to.
286+ :param port_upper: The highest port to bind to.
285287 """
286288
287289 def __init__ (
@@ -296,6 +298,8 @@ def __init__(
296298 turn_transport : str = "udp" ,
297299 use_ipv4 : bool = True ,
298300 use_ipv6 : bool = True ,
301+ port_lower : int = 0 ,
302+ port_upper : int = 0 ,
299303 ) -> None :
300304 self .ice_controlling = ice_controlling
301305 #: Local username, automatically set to a random value.
@@ -340,6 +344,8 @@ def __init__(
340344 self ._tie_breaker = secrets .randbits (64 )
341345 self ._use_ipv4 = use_ipv4
342346 self ._use_ipv6 = use_ipv6
347+ self ._port_lower = port_lower
348+ self ._port_upper = port_upper
343349
344350 @property
345351 def local_candidates (self ) -> List [Candidate ]:
@@ -843,20 +849,42 @@ def _find_pair(
843849 return pair
844850 return None
845851
852+ async def _create_datagram_endpoint (self , address ):
853+ """
854+ Create a datagram endpoint on the specified address,
855+ with port optionally in the range [port_lower, port_upper]
856+ """
857+ # when port_lower and port_upper are both zero: ports = [0]
858+ # which will let the OS pick any random free port
859+ ports = list (range (
860+ self ._port_lower if self ._port_lower or not self ._port_upper else 1 ,
861+ (65535 if self ._port_lower and not self ._port_upper else self ._port_upper ) + 1
862+ ))
863+ random .shuffle (ports )
864+ loop = asyncio .get_event_loop ()
865+ for port in ports :
866+ try :
867+ transport , protocol = await loop .create_datagram_endpoint (
868+ lambda : StunProtocol (self ), local_addr = (address , port )
869+ )
870+ return transport , protocol
871+ except OSError as exc :
872+ if port == ports [- 1 ]:
873+ # this was the last port, give up
874+ raise exc
875+ raise ValueError ("Illegal port range: [%d, %d]" % (self ._port_lower , self ._port_upper ))
876+
846877 async def get_component_candidates (
847878 self , component : int , addresses : List [str ], timeout : int = 5
848879 ) -> List [Candidate ]:
849880 candidates = []
850- loop = asyncio .get_event_loop ()
851881
852882 # gather host candidates
853883 host_protocols = []
854884 for address in addresses :
855885 # create transport
856886 try :
857- transport , protocol = await loop .create_datagram_endpoint (
858- lambda : StunProtocol (self ), local_addr = (address , 0 )
859- )
887+ transport , protocol = await self ._create_datagram_endpoint (address )
860888 sock = transport .get_extra_info ("socket" )
861889 if sock is not None :
862890 sock .setsockopt (
0 commit comments