Skip to content

lablink_allocator_service.tunnel_manager

Reverse-tunnel connectivity's server-side bookkeeping.

One wstunnel server runs for the whole deployment (started by start.sh), and each client attaches to it over a WebSocket carrying its own path prefix. This module owns the two things the allocator must control:

  1. The restrictions file. wstunnel's reverse-tunnel bind address is chosen by the CLIENT, so without restrictions any client holding a valid client_secret could bind another client's loopback alias and receive that client's student sessions. Each rule pins one path prefix to one /32 alias and the two ports we tunnel. wstunnel reloads the file automatically when it changes, so no restart or signal is needed.

  2. Which aliases actually have a client attached. A bound port is NOT evidence of an attached client: wstunnel keeps a reverse-tunnel listener bound for --remote-to-local-server-idle-timeout after the client leaves, and connections to an orphaned listener hang rather than being refused (measured: 12s, no response). Health must therefore observe the pairing, not the process or the port.

Two facts measured against the pinned wstunnel v10.6.2 binary, both of which the rendered file's shape depends on -- re-verify when bumping it:

  • allow[].port takes a list of port-number STRINGS, each parsed as a single-port range (port: [6080..=6080, ...] under --log-lvl DEBUG). A [{start: N, end: N}] form is rejected outright.
  • !PathPrefix compares ONLY the client's first path segment, so a match value like tunnel/<prefix> can never fire and the failure is a silent deny-all. The match value must BE the whole first segment, which is why path_prefix() returns tun-<client_id>-<digest> at the root rather than nesting under a shared "tunnel/". Confirmed end to end: a client presenting another client's alias in -R was refused with "Rejecting connection with not allowed destination".

Security note: client_id is the DB hostname, which arrives from client-controlled registration input. Two independent guards, both load-bearing: _is_safe() rejects the charset (what stops a multi-line hostname reaching the renderer at all), and yaml.safe_dump does the rendering (what stops a single-line-but-special value -- quotes, colons, # -- forging YAML structure if it ever slipped past).

Alias octets with a listening reverse-tunnel socket right now.

Parses /proc/net/tcp because there is no wstunnel API for it. State "0A" is TCP_LISTEN; local_address is <hex ip>:<hex port> with the IP in network-reversed byte order, so 127.0.0.10 is 0A00007F.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def attached_aliases() -> set[int]:
    """Alias octets with a listening reverse-tunnel socket right now.

    Parses /proc/net/tcp because there is no wstunnel API for it. State
    "0A" is TCP_LISTEN; local_address is `<hex ip>:<hex port>` with the IP
    in network-reversed byte order, so 127.0.0.10 is 0A00007F.
    """
    found: set[int] = set()
    for line in _proc_net_tcp().splitlines()[1:]:
        parts = line.split()
        if len(parts) < 4 or parts[3] != "0A":
            continue
        hex_ip, _, hex_port = parts[1].partition(":")
        if len(hex_ip) != 8 or not hex_ip.upper().endswith("00007F"):
            continue
        if int(hex_port, 16) not in TUNNEL_PORTS:
            continue
        found.add(int(hex_ip[:2], 16))
    return found

Permit exactly this client to bind exactly its own alias.

Raises ValueError -- before any file work -- if client_id/prefix carry characters this module can't safely render, or if alias_octet is outside the range a /32 loopback rule can express. This is the trust boundary: client_id is attacker-controlled (see the module docstring), so it's validated here rather than assumed clean by callers.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def authorize_client(*, client_id: str, alias_octet: int, prefix: str) -> None:
    """Permit exactly this client to bind exactly its own alias.

    Raises ValueError -- before any file work -- if client_id/prefix carry
    characters this module can't safely render, or if alias_octet is
    outside the range a /32 loopback rule can express. This is the trust
    boundary: client_id is attacker-controlled (see the module docstring),
    so it's validated here rather than assumed clean by callers.
    """
    _hydrate_from_disk()
    if not _is_safe(client_id, _MAX_CLIENT_ID_LEN):
        raise ValueError(f"unsafe client_id for tunnel restrictions: {client_id!r}")
    if not _is_safe(prefix, _MAX_PREFIX_LEN):
        raise ValueError(f"unsafe prefix for tunnel restrictions: {prefix!r}")
    if not is_valid_alias_octet(alias_octet):
        raise ValueError(
            f"alias_octet {alias_octet!r} out of range "
            f"[{_MIN_ALIAS_OCTET}, {_MAX_ALIAS_OCTET}]"
        )
    _restrictions[client_id] = (alias_octet, prefix)
    _write()

True iff octet is in the range a /32 loopback rule can express.

Exposed so callers (routes/registration.py) can reject an out-of-range allocation with a clean error response before writing a client row, instead of finding out only when authorize_client raises.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def is_valid_alias_octet(octet: int) -> bool:
    """True iff *octet* is in the range a /32 loopback rule can express.

    Exposed so callers (routes/registration.py) can reject an
    out-of-range allocation with a clean error response before writing a
    client row, instead of finding out only when authorize_client raises.
    """
    return _MIN_ALIAS_OCTET <= octet <= _MAX_ALIAS_OCTET

Per-client value the client must dial as its first path segment.

tun-<client_id>-<digest>. Must BE the first path segment, not a sub-path under some shared root: wstunnel's !PathPrefix restriction matcher only inspects the first segment (see the module docstring), so a value like "tunnel/" could never match. The tun- marker is what lets nginx tell a tunnel attach apart from an application route now that the prefix sits at the URL root instead of under a fixed "tunnel/" segment.

Derived from the client's own secret so it is stable across restarts and needs no storage of its own. It identifies the client to /internal/tunnel_auth; the bearer token is what authenticates.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def path_prefix(client_id: str, secret: str) -> str:
    """Per-client value the client must dial as its first path segment.

    `tun-<client_id>-<digest>`. Must BE the first path segment, not a
    sub-path under some shared root: wstunnel's `!PathPrefix` restriction
    matcher only inspects the first segment (see the module docstring), so
    a value like "tunnel/<this>" could never match. The `tun-` marker is
    what lets nginx tell a tunnel attach apart from an application route
    now that the prefix sits at the URL root instead of under a fixed
    "tunnel/" segment.

    Derived from the client's own secret so it is stable across restarts
    and needs no storage of its own. It identifies the client to
    /internal/tunnel_auth; the bearer token is what authenticates.
    """
    digest = hashlib.sha256(f"{client_id}:{secret}".encode()).hexdigest()[:16]
    return f"tun-{client_id}-{digest}"

Drop this client's permission. No-op if it was never a tunnel client.

No client_id validation here: an unsafe id can never have made it into _restrictions in the first place (authorize_client rejects it before writing), so the plain dict.pop miss already gives the right no-op behavior. This also has to stay a no-op, not raise -- it runs on every client unregister.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def revoke_client(client_id: str) -> None:
    """Drop this client's permission. No-op if it was never a tunnel client.

    No client_id validation here: an unsafe id can never have made it into
    _restrictions in the first place (authorize_client rejects it before
    writing), so the plain dict.pop miss already gives the right no-op
    behavior. This also has to stay a no-op, not raise -- it runs on every
    client unregister.
    """
    _hydrate_from_disk()
    if _restrictions.pop(client_id, None) is None:
        return
    _write()

"ok" iff the shared tunnel server is accepting connections.

Source code in packages/allocator/src/lablink_allocator_service/tunnel_manager.py
def tunnel_status() -> str:
    """"ok" iff the shared tunnel server is accepting connections."""
    try:
        with socket.create_connection(("127.0.0.1", TUNNEL_SERVER_PORT), timeout=2):
            return "ok"
    except OSError:
        return "not running"