# -*- coding: utf-8 -*- """ network.py — Discovery сети хоста, loopback-proxy, sidecar, routes, iptables, sysctl """ import os import re import json import socket import ipaddress from core.runner import run, exists from core.color import info, warn, success, log_cmd def get_container_host_connections(cid): """ Смотрит внутри контейнера (через nsenter -t -n) установленные/слушающие соединения. Возвращает список: {proto, local, remote, estado} """ from discover.docker import get_container_pid pid = get_container_pid(cid) if not pid: return [] # Проверим ss внутри netns контейнера out = run(f"nsenter -t {pid} -n ss -tlnp", check=False) # и established тоже out2 = run(f"nsenter -t {pid} -n ss -tnp state established", check=False) results = [] for src in (out, out2): for ln in src.stdout.splitlines(): parts = ln.split() if len(parts) < 4: continue # Формат: State Recv-Q Send-Q Local Address:Port Peer Address:Port Process try: proto = "tcp" local = parts[3] remote = parts[4] if len(parts) > 4 else None state = parts[0] results.append({"proto": proto, "local": local, "remote": remote, "state": state}) except Exception: continue return results def find_listeners_on_host(port): """На хосте ищет, кто слушает указанный TCP порт""" listeners = [] out = run(f"ss -tlnp 'sport = :{port}'", check=False) for ln in out.stdout.splitlines(): # На некоторых системах столбец users содержит pid/process # Пример: LISTEN 0 128 127.0.0.1:40000 0.0.0.0:* users:(("warp-svc",pid=1234,fd=5)) m = re.search(r'users:\(\("([^"]+)"', ln) if m: proc = m.group(1) listeners.append({"process": proc, "line": ln.strip()}) else: # fallback: ищем pid через lsof lof = run(f"lsof -i TCP:{port} -sTCP:LISTEN -t", check=False) if lof.stdout.strip(): for pid in lof.stdout.strip().split(): try: cmdline = open(f"/proc/{pid}/comm", "r").read().strip() listeners.append({"pid": pid, "process": cmdline, "line": ln.strip()}) except Exception: pass return listeners def find_sidecar_processes(cid, container_ports): """ Ищет sidecar-процессы на хосте, к которым контейнер стучится через loopback. Дедуплицирует по (host_process, container_port_target). """ info("Ищем loopback-соединения контейнера (sidecar / proxy / WARP) ...") conns = get_container_host_connections(cid) sidecars = [] seen = set() for c in conns: local = c.get("local", "") if "127.0.0.1" in local or "localhost" in local or "::1" in local: # Извлекаем порт m = re.search(r':(\d+)$', local) if m: port = int(m.group(1)) listeners = find_listeners_on_host(port) if listeners: for l in listeners: proc = l.get("process", "?") key = (proc, port) if key in seen: continue seen.add(key) info(f" Контейнер подключается к {local} → процесс на хосте: {proc} (pid={l.get('pid', '?')})") sidecars.append({ "type": "loopback_listener", "container_port_target": port, "host_process": proc, "host_pid": l.get("pid"), "method": "ss_lsof", }) if sidecars: success(f"Найдено sidecar/loopback зависимостей: {len(sidecars)}") return sidecars def get_process_details(pid): """Собирает детали о процессе: exe, cmdline, cwd, open files, systemd unit""" try: base = f"/proc/{pid}" exe = os.readlink(f"{base}/exe") if os.path.islink(f"{base}/exe") else None cmdline = open(f"{base}/cmdline", "rb").read().replace(b'\x00', b' ').decode("utf-8", "ignore").strip() cwd = os.readlink(f"{base}/cwd") if os.path.islink(f"{base}/cwd") else None # open files fds = os.listdir(f"{base}/fd") files = [] for fd in fds: try: p = os.readlink(f"{base}/fd/{fd}") if p.startswith("/") and not p.startswith("/dev/") and not p.startswith("/proc/"): files.append(p) except Exception: pass # systemd unit unit = None try: cgroup = open(f"{base}/cgroup", "r").read() for ln in cgroup.splitlines(): if ".service" in ln: parts = ln.split(":") if len(parts) >= 3: # 0::/system.slice/nginx.service m = re.search(r'/([^/]+\.service)$', parts[-1]) if m: unit = m.group(1) except Exception: pass return {"exe": exe, "cmdline": cmdline, "cwd": cwd, "files": list(set(files)), "unit": unit} except Exception as e: return {"error": str(e)} def gather_host_network_info(): """ Собирает текущее состояние сети хоста для manifest. """ info("Собираем сетевую конфигурацию хоста ...") data = {} # routes if exists("ip"): r = run("ip route show", check=False) data["ip_routes"] = r.stdout.strip().splitlines() r2 = run("ip rule show", check=False) data["ip_rules"] = r2.stdout.strip().splitlines() # interfaces if exists("ip"): r = run("ip addr show", check=False) data["ip_addr"] = r.stdout.strip().splitlines() # iptables data["iptables"] = {} for table in ("filter", "nat", "mangle", "raw"): r = run(f"iptables -t {table} -S", check=False) data["iptables"][table] = r.stdout.strip().splitlines() # nftables if exists("nft"): r = run("nft list ruleset", check=False) data["nftables"] = r.stdout.strip().splitlines() # sysctl отличия от дефолта (берём всё, потом diff-анализ можно делать вручную) r = run("sysctl -a", check=False) data["sysctl"] = r.stdout.strip().splitlines() return data def find_systemd_units_related(procs): """ Ищет systemd unit-файлы для процессов (sidecar и других). Дедуплицирует по имени unit. """ units = [] seen = set() for p in procs: # p может быть dict с pid pid = p.get("pid") if not pid: continue # systemd unit через systemctl status try: out = run(f"systemctl status {pid}", check=False) # строка вида: ... Loaded: loaded (/lib/systemd/system/xxx.service; ...) for ln in out.stdout.splitlines(): if "Loaded:" in ln and ".service" in ln: m = re.search(r'loaded\s+\(([^)]+)\)', ln) if m: path = m.group(1).split(";")[0].strip() name = os.path.basename(path) if name not in seen: seen.add(name) units.append({"name": name, "path": path, "related_to": p.get("process", "?")}) except Exception: pass return units def gather_cron_jobs(user_hint=None): """ Ищет cron-задания, связанные с сервисом (по имени или пути). Если user_hint — список подсказок (название сервиса, путь), ищем по ним. """ jobs = [] # crontab -l для текущего пользователя и root current_user = os.environ.get('USER') or os.environ.get('LOGNAME') if not current_user: try: import pwd current_user = pwd.getpwuid(os.getuid()).pw_name except Exception: current_user = None users = [] if current_user: users.append(current_user) users.append("root") for user in users: try: out = run(f"crontab -u {user} -l", check=False) for ln in out.stdout.splitlines(): if ln.strip().startswith("#"): continue if user_hint: for hint in user_hint: if hint.lower() in ln.lower(): jobs.append({"user": user, "line": ln.strip()}) break else: jobs.append({"user": user, "line": ln.strip()}) except Exception: pass # /etc/cron.d, /etc/cron.hourly, etc. cron_dirs = ["/etc/cron.d", "/etc/cron.hourly", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly"] for d in cron_dirs: if not os.path.isdir(d): continue for f in os.listdir(d): fp = os.path.join(d, f) try: content = open(fp, "r", encoding="utf-8", errors="ignore").read() if user_hint: for hint in user_hint: if hint.lower() in content.lower(): jobs.append({"file": fp, "content": content[:500]}) break else: jobs.append({"file": fp, "content": content[:500]}) except Exception: pass return jobs