|
1 | 1 | import importlib.resources |
| 2 | +import socket |
2 | 3 | import subprocess |
3 | 4 | import tempfile |
4 | 5 | from asyncio import Lock |
@@ -297,18 +298,65 @@ def certificate_exists(domain: str) -> bool: |
297 | 298 | def get_config_name(domain: str) -> str: |
298 | 299 | return f"443-{domain}.conf" |
299 | 300 |
|
| 301 | + @staticmethod |
| 302 | + def _is_port_available(port: int) -> bool: |
| 303 | + """Check if a port is actually available (not in use by any process). |
| 304 | +
|
| 305 | + Tries to bind to the port to see if it's available. |
| 306 | + """ |
| 307 | + try: |
| 308 | + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| 309 | + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 310 | + try: |
| 311 | + sock.bind(("127.0.0.1", port)) |
| 312 | + # If bind succeeds, port is available |
| 313 | + return True |
| 314 | + except OSError: |
| 315 | + # If bind fails (e.g., Address already in use), port is not available |
| 316 | + return False |
| 317 | + except Exception: |
| 318 | + # If we can't check, assume port is not available to be safe |
| 319 | + logger.debug("Error checking port %s availability, assuming in use", port) |
| 320 | + return False |
| 321 | + |
300 | 322 | def _allocate_router_port(self) -> int: |
301 | | - """Allocate next available router port in range 10001-11999.""" |
| 323 | + """Allocate next available router port in range 10001-11999. |
| 324 | +
|
| 325 | + Checks both our internal allocation map and actual port availability |
| 326 | + to avoid conflicts with other services (e.g., Prometheus). |
| 327 | + """ |
302 | 328 | port = self._next_router_port |
303 | | - # Check if port is already allocated |
304 | | - while port in self._router_port_to_domain: |
| 329 | + max_attempts = 1999 # Maximum ports in range 10001-11999 |
| 330 | + attempts = 0 |
| 331 | + |
| 332 | + while attempts < max_attempts: |
| 333 | + # Check if port is already allocated by us |
| 334 | + if port in self._router_port_to_domain: |
| 335 | + port += 1 |
| 336 | + if port > 11999: |
| 337 | + port = 10001 # Wrap around |
| 338 | + attempts += 1 |
| 339 | + continue |
| 340 | + |
| 341 | + # Check if port is actually available on the system |
| 342 | + if self._is_port_available(port): |
| 343 | + # Port is available, allocate it |
| 344 | + self._next_router_port = port + 1 |
| 345 | + if self._next_router_port > 11999: |
| 346 | + self._next_router_port = 10001 # Wrap around |
| 347 | + logger.debug("Allocated router port %s", port) |
| 348 | + return port |
| 349 | + |
| 350 | + # Port is in use, try next one |
| 351 | + logger.debug("Port %s is in use, trying next port", port) |
305 | 352 | port += 1 |
306 | 353 | if port > 11999: |
307 | | - raise UnexpectedProxyError("Router port range exhausted (10001-11999)") |
308 | | - self._next_router_port = port + 1 |
309 | | - if self._next_router_port > 11999: |
310 | | - self._next_router_port = 10001 # Wrap around |
311 | | - return port |
| 354 | + port = 10001 # Wrap around |
| 355 | + attempts += 1 |
| 356 | + |
| 357 | + raise UnexpectedProxyError( |
| 358 | + "Router port range exhausted (10001-11999). All ports in range appear to be in use." |
| 359 | + ) |
312 | 360 |
|
313 | 361 | def write_global_conf(self) -> None: |
314 | 362 | conf = read_package_resource("00-log-format.conf") |
|
0 commit comments