Files
docker-migrate/discover/network.py

266 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 <pid> -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