266 lines
10 KiB
Python
266 lines
10 KiB
Python
# -*- 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
|