diff --git a/docker-run-commands.sh b/docker-run-commands.sh index ec3a634..084272c 100755 --- a/docker-run-commands.sh +++ b/docker-run-commands.sh @@ -6,16 +6,16 @@ set -euo pipefail # The manager normally proxies user ports; the first command block matches the manager # container creation and does not publish ports. -# User: mario - manager-equivalent container -docker volume create v-conf-mario >/dev/null +# User: tomas - manager-equivalent container +docker volume create v-conf-tomas >/dev/null docker run -d \ - --name fis-mario \ + --name fis-tomas \ --restart always \ - --volume v-conf-mario:/config:rw \ + --volume v-conf-tomas:/config:rw \ --env PUID=1000 \ --env PGID=1000 \ - --env PASSWORD=mario \ - --env CUSTOM_USER=mario \ + --env PASSWORD=tomasmali92 \ + --env CUSTOM_USER=tomas \ --memory 3g \ --memory-swap 5g \ --privileged \ @@ -23,17 +23,17 @@ docker run -d \ --interactive \ fis -# Emergency direct access for mario on host port 3001: -# docker rm -f fis-mario || true +# Emergency direct access for tomas on host port 3009: +# docker rm -f fis-tomas || true # docker run -d \ -# --name fis-mario \ +# --name fis-tomas \ # --restart always \ -# -p 3001:3000 \ -# --volume v-conf-mario:/config:rw \ +# -p 3009:3000 \ +# --volume v-conf-tomas:/config:rw \ # --env PUID=1000 \ # --env PGID=1000 \ -# --env PASSWORD=mario \ -# --env CUSTOM_USER=mario \ +# --env PASSWORD=tomasmali92 \ +# --env CUSTOM_USER=tomas \ # --memory 3g \ # --memory-swap 5g \ # --privileged \ diff --git a/manager-solution/manager/__pycache__/manager.cpython-313.pyc b/manager-solution/manager/__pycache__/manager.cpython-313.pyc index d23b70d..a6d2029 100644 Binary files a/manager-solution/manager/__pycache__/manager.cpython-313.pyc and b/manager-solution/manager/__pycache__/manager.cpython-313.pyc differ diff --git a/manager-solution/manager/config.json b/manager-solution/manager/config.json new file mode 100644 index 0000000..c6e2cb8 --- /dev/null +++ b/manager-solution/manager/config.json @@ -0,0 +1,27 @@ +{ + "defaults": { + "container_prefix": "fis-", + "environment": { + "PUID": "1000", + "PGID": "1000" + }, + "volumes": { + "v-conf-{id}": "/config" + }, + "mem_limit": "3g", + "memswap_limit": "5g", + "privileged": true, + "tmpfs": {}, + "dns": [], + "extra_hosts": {} + }, + "users": [ + { + "id": "tomas", + "port": 3009, + "environment": { + "PASSWORD": "tomasmali92" + } + } + ] +} diff --git a/manager-solution/manager/manager.py b/manager-solution/manager/manager.py index 3c2f0a5..3240cb3 100644 --- a/manager-solution/manager/manager.py +++ b/manager-solution/manager/manager.py @@ -10,6 +10,7 @@ import asyncio import json import os import socket +import subprocess import threading import time from typing import Any, Dict, Optional @@ -52,6 +53,77 @@ def format_access_url(host: str, port: int) -> str: return f"http://{host}:{port}" +def detect_lan_ip() -> str: + configured_ip = os.getenv("MANAGER_PUBLIC_IP") or os.getenv("CONFIGURATOR_PUBLIC_IP") + if configured_ip: + return configured_ip + + try: + output = subprocess.check_output( + ["ip", "route", "get", "1.1.1.1"], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + parts = output.split() + if "src" in parts: + candidate = parts[parts.index("src") + 1] + if is_public_manager_ip(candidate): + return candidate + except (OSError, subprocess.SubprocessError, IndexError): + pass + + try: + output = subprocess.check_output( + ["ip", "-o", "-4", "addr", "show", "scope", "global"], + stderr=subprocess.DEVNULL, + text=True, + ) + for line in output.splitlines(): + parts = line.split() + interface = parts[1] + candidate = parts[3].split("/", 1)[0] + if is_ignored_interface(interface): + continue + if is_public_manager_ip(candidate): + return candidate + except (OSError, subprocess.SubprocessError, IndexError): + pass + + try: + output = subprocess.check_output(["hostname", "-I"], stderr=subprocess.DEVNULL, text=True).strip() + for candidate in output.split(): + if is_public_manager_ip(candidate): + return candidate + except (OSError, subprocess.SubprocessError): + pass + + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.connect(("8.8.8.8", 80)) + candidate = sock.getsockname()[0] + if is_public_manager_ip(candidate): + return candidate + except OSError: + pass + + return "localhost" + + +def is_ignored_interface(interface: str) -> bool: + ignored_prefixes = ("docker", "br-", "veth", "virbr", "podman", "lo") + return interface.startswith(ignored_prefixes) + + +def is_public_manager_ip(candidate: str) -> bool: + if not candidate or "." not in candidate: + return False + if candidate.startswith("127.") or candidate.startswith("169.254."): + return False + if candidate.startswith("172.17."): + return False + return True + + def get_user_container_lock(user_id: str) -> threading.Lock: with user_container_locks_guard: if user_id not in user_container_locks: @@ -448,12 +520,13 @@ async def main(): log(f"Gateway port: {GATEWAY_PORT if GATEWAY_PORT else 'disabled'}") log(f"Inactivity timeout: {INACTIVITY_SECONDS}s") + public_host = detect_lan_ip() servers = [] if GATEWAY_PORT > 0: gateway_server = await asyncio.start_server(handle_gateway_connection, "0.0.0.0", GATEWAY_PORT) log( "Shared gateway ready: " - f"{format_access_url('localhost', GATEWAY_PORT)} " + f"{format_access_url(public_host, GATEWAY_PORT)} " f"(host-based routing: http://.localhost:{GATEWAY_PORT})" ) servers.append(gateway_server) @@ -463,7 +536,7 @@ async def main(): await handle_connection(reader, writer, configured_user) server = await asyncio.start_server(handler, "0.0.0.0", user["port"]) - direct_url = format_access_url("localhost", user["port"]) + direct_url = format_access_url(public_host, user["port"]) if GATEWAY_PORT > 0: gateway_url = format_access_url(f"{user['id']}.localhost", GATEWAY_PORT) log(