First Build alpha 0.1
This commit is contained in:
110
app/services/port_manager.py
Normal file
110
app/services/port_manager.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""UDP port allocation service for NetBird relay/STUN ports.
|
||||
|
||||
Manages the range starting at relay_base_port (default 3478). Each customer
|
||||
gets one unique UDP port. The manager checks both the database and the OS
|
||||
to avoid collisions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Deployment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_udp_port_in_use(port: int) -> bool:
|
||||
"""Check whether a UDP port is currently bound on the host.
|
||||
|
||||
Args:
|
||||
port: UDP port number to probe.
|
||||
|
||||
Returns:
|
||||
True if the port is in use.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
sock.bind(("0.0.0.0", port))
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def get_allocated_ports(db: Session) -> set[int]:
|
||||
"""Return the set of relay UDP ports already assigned in the database.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
|
||||
Returns:
|
||||
Set of port numbers.
|
||||
"""
|
||||
rows = db.query(Deployment.relay_udp_port).all()
|
||||
return {r[0] for r in rows}
|
||||
|
||||
|
||||
def allocate_port(db: Session, base_port: int = 3478, max_ports: int = 100) -> int:
|
||||
"""Find and return the next available relay UDP port.
|
||||
|
||||
Scans from *base_port* to *base_port + max_ports - 1*, skipping ports
|
||||
that are either already in the database or currently bound on the host.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
base_port: Start of the port range.
|
||||
max_ports: Number of ports in the range.
|
||||
|
||||
Returns:
|
||||
An available port number.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no port in the range is available.
|
||||
"""
|
||||
allocated = get_allocated_ports(db)
|
||||
for port in range(base_port, base_port + max_ports):
|
||||
if port in allocated:
|
||||
continue
|
||||
if _is_udp_port_in_use(port):
|
||||
logger.warning("Port %d is in use on the host, skipping.", port)
|
||||
continue
|
||||
logger.info("Allocated relay UDP port %d.", port)
|
||||
return port
|
||||
|
||||
raise RuntimeError(
|
||||
f"No available relay ports in range {base_port}-{base_port + max_ports - 1}. "
|
||||
"All 100 ports are allocated."
|
||||
)
|
||||
|
||||
|
||||
def release_port(db: Session, port: int) -> None:
|
||||
"""Mark a port as released (informational logging only).
|
||||
|
||||
The actual release happens when the Deployment row is deleted. This
|
||||
helper exists for explicit logging in rollback scenarios.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
port: The port to release.
|
||||
"""
|
||||
logger.info("Released relay UDP port %d.", port)
|
||||
|
||||
|
||||
def validate_port_available(db: Session, port: int) -> bool:
|
||||
"""Check if a specific port is available both in DB and on the host.
|
||||
|
||||
Args:
|
||||
db: Active SQLAlchemy session.
|
||||
port: Port number to check.
|
||||
|
||||
Returns:
|
||||
True if the port is available.
|
||||
"""
|
||||
allocated = get_allocated_ports(db)
|
||||
if port in allocated:
|
||||
return False
|
||||
return not _is_udp_port_in_use(port)
|
||||
Reference in New Issue
Block a user