mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-25 06:35:18 +00:00
110 lines
4.0 KiB
Python
110 lines
4.0 KiB
Python
"""
|
|
Monkey-patch aioice to use a fixed UDP port range for ICE candidates,
|
|
and optionally rewrite SDP to advertise a different host IP.
|
|
|
|
This allows running the server in Docker with bridge networking
|
|
(no network_mode: host) by:
|
|
1. Restricting ICE UDP ports to a known range that can be mapped in Docker
|
|
2. Replacing container-internal IPs with the Docker host IP in SDP answers
|
|
"""
|
|
|
|
import asyncio
|
|
import socket
|
|
|
|
import aioice.ice
|
|
|
|
from reflector.logger import logger
|
|
|
|
|
|
def parse_port_range(range_str: str) -> tuple[int, int]:
|
|
"""Parse a 'min-max' string into (min_port, max_port)."""
|
|
parts = range_str.split("-")
|
|
if len(parts) != 2:
|
|
raise ValueError(f"WEBRTC_PORT_RANGE must be 'min-max', got: {range_str!r}")
|
|
min_port, max_port = int(parts[0]), int(parts[1])
|
|
if not (1024 <= min_port <= max_port <= 65535):
|
|
raise ValueError(
|
|
f"Invalid port range: {min_port}-{max_port} "
|
|
"(must be 1024-65535 with min <= max)"
|
|
)
|
|
return min_port, max_port
|
|
|
|
|
|
def patch_aioice_port_range(min_port: int, max_port: int) -> None:
|
|
"""
|
|
Monkey-patch aioice so that ICE candidate UDP sockets bind to ports
|
|
within [min_port, max_port] instead of OS-assigned ephemeral ports.
|
|
|
|
Works by temporarily wrapping loop.create_datagram_endpoint() during
|
|
aioice's get_component_candidates() to intercept bind(addr, 0) calls.
|
|
"""
|
|
_original = aioice.ice.Connection.get_component_candidates
|
|
_state = {"next_port": min_port}
|
|
|
|
async def _patched_get_component_candidates(self, component, addresses, timeout=5):
|
|
loop = asyncio.get_event_loop()
|
|
_orig_create = loop.create_datagram_endpoint
|
|
|
|
async def _create_with_port_range(*args, **kwargs):
|
|
local_addr = kwargs.get("local_addr")
|
|
if local_addr and local_addr[1] == 0:
|
|
addr = local_addr[0]
|
|
# Try each port in the range (wrapping around)
|
|
attempts = max_port - min_port + 1
|
|
for _ in range(attempts):
|
|
port = _state["next_port"]
|
|
_state["next_port"] = (
|
|
min_port
|
|
if _state["next_port"] >= max_port
|
|
else _state["next_port"] + 1
|
|
)
|
|
try:
|
|
kwargs["local_addr"] = (addr, port)
|
|
return await _orig_create(*args, **kwargs)
|
|
except OSError:
|
|
continue
|
|
# All ports exhausted, fall back to OS assignment
|
|
logger.warning(
|
|
"All WebRTC ports in range exhausted, falling back to OS",
|
|
min_port=min_port,
|
|
max_port=max_port,
|
|
)
|
|
kwargs["local_addr"] = (addr, 0)
|
|
return await _orig_create(*args, **kwargs)
|
|
|
|
loop.create_datagram_endpoint = _create_with_port_range
|
|
try:
|
|
return await _original(self, component, addresses, timeout)
|
|
finally:
|
|
loop.create_datagram_endpoint = _orig_create
|
|
|
|
aioice.ice.Connection.get_component_candidates = _patched_get_component_candidates
|
|
logger.info(
|
|
"aioice patched for WebRTC port range",
|
|
min_port=min_port,
|
|
max_port=max_port,
|
|
)
|
|
|
|
|
|
def resolve_webrtc_host(host: str) -> str:
|
|
"""Resolve a hostname or IP to an IP address for ICE candidate rewriting."""
|
|
try:
|
|
ip = socket.gethostbyname(host)
|
|
logger.info("Resolved WEBRTC_HOST", host=host, ip=ip)
|
|
return ip
|
|
except socket.gaierror:
|
|
logger.warning("Could not resolve WEBRTC_HOST, using as-is", host=host)
|
|
return host
|
|
|
|
|
|
def rewrite_sdp_host(sdp: str, target_ip: str) -> str:
|
|
"""
|
|
Replace container-internal IPs in SDP with target_ip so that
|
|
ICE candidates advertise a routable address.
|
|
"""
|
|
container_ips = aioice.ice.get_host_addresses(use_ipv4=True, use_ipv6=False)
|
|
for ip in container_ips:
|
|
if ip != "127.0.0.1" and ip != target_ip:
|
|
sdp = sdp.replace(ip, target_ip)
|
|
return sdp
|