Files
ai-setup/scripts/ru-bypass.sh
Виталий Никитенко d2bbcc7e33 fix: kill switch — UFW before.rules с актуальным DEV, прямые iptables, /etc/hosts для *.eltex.loc и elph
Три корневые проблемы и их исправления:

1. MANAGE_BUILTINS=no в /etc/default/ufw — цепочка ufw-before-output
   не вызывалась из OUTPUT, правила before.rules не применялись.
   → автофикс no→yes + прямые правила iptables (не зависят от UFW).

2. UFW-правила создавались однократно по маркеру — при смене DEV
   (wlp1s0→enp4s0) продолжали ссылаться на старый интерфейс.
   → теперь при каждом запуске удаляются и пересоздаются с актуальным DEV.

3. DNS через VPN для локальных доменов возвращал внешние IP вместо
   внутренних (RFC1918) — трафик уходил в VPN и не достигал серверов.
   → /etc/hosts с фиксированными IP для *.eltex.loc, mattermost, elph.
   → замена dig +short на getent hosts (уважает /etc/hosts).

Добавлены built-in KILL_SWITCH_EXCEPTIONS:
  mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:44:20 +03:00

383 lines
15 KiB
Bash
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.
#!/bin/bash
# ru-bypass.sh — .ru трафик напрямую мимо Amnezia, всё остальное через VPN + kill switch
#
# Принцип: ipset + более специфичные маршруты имеют приоритет над amn0.
# Kill switch (UFW) остаётся активным — не-.ru трафик при отвале Amnezia блокируется.
#
# Первый запуск устанавливает ipset, два systemd сервиса и NetworkManager dispatcher:
# - ru-ipset-restore.service запускается ДО UFW, восстанавливает ipset из файла
# - ru-bypass.service запускается после network-online, обновляет RIPE-список и маршруты
# Каждый запуск обновляет список .ru IP-блоков из RIPE (кэш 24ч).
#
# Использование: sudo bash ru-bypass.sh
# Сохраняем env-переменные до загрузки конфига (env имеет приоритет)
_env_gw="${GATEWAY:-}"
_env_dev="${DEV:-}"
_env_local_dns="${LOCAL_DNS:-}"
_env_amn_srv="${AMNEZIA_SERVER:-}"
_env_ks_exc="${KILL_SWITCH_EXCEPTIONS:-}"
# Загружаем сохранённый конфиг (для запуска из systemd/NM dispatcher без env)
[ -f /etc/ru-bypass.conf ] && . /etc/ru-bypass.conf
# ENV-переменные имеют приоритет над конфигом
[ -n "$_env_gw" ] && GATEWAY="$_env_gw"
[ -n "$_env_dev" ] && DEV="$_env_dev"
[ -n "$_env_local_dns" ] && LOCAL_DNS="$_env_local_dns"
[ -n "$_env_amn_srv" ] && AMNEZIA_SERVER="$_env_amn_srv"
[ -n "$_env_ks_exc" ] && KILL_SWITCH_EXCEPTIONS="$_env_ks_exc"
# Дефолты (если ни конфиг, ни env не задали значение)
GATEWAY="${GATEWAY:-192.168.1.1}"
DEV="${DEV:-wlp1s0}"
LOCAL_DNS="${LOCAL_DNS:-}"
AMNEZIA_SERVER="${AMNEZIA_SERVER:-}"
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS:-}"
# Базовые исключения, необходимые для работы корпоративных сервисов
# Добавляются автоматически, даже если не указаны в конфиге
_BUILTIN_EXCEPTIONS="mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15"
for _exc in $_BUILTIN_EXCEPTIONS; do
case " $KILL_SWITCH_EXCEPTIONS " in
*" $_exc "*) ;;
*) KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS $_exc" ;;
esac
done
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS# }"
# Сохраняем конфиг для будущих запусков (systemd, NM dispatcher)
cat > /etc/ru-bypass.conf <<_CONF
GATEWAY="$GATEWAY"
DEV="$DEV"
LOCAL_DNS="$LOCAL_DNS"
AMNEZIA_SERVER="$AMNEZIA_SERVER"
KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS"
_CONF
SETNAME="ru-direct"
CACHE="/var/cache/ru-delegations.txt"
IPSET_SAVE="/etc/ipset.conf"
RIPE_URL="https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest"
SCRIPT_DEST="/usr/local/bin/ru-bypass.sh"
UFW_BEFORE="/etc/ufw/before.rules"
if [ "$(id -u)" != "0" ]; then
echo "Запускай от root: sudo bash $0"
exit 1
fi
# --- Первичная настройка (однократно) ---
if ! command -v ipset >/dev/null 2>&1; then
echo "Устанавливаем ipset..."
apt-get install -y ipset
fi
# Копируем скрипт в /usr/local/bin (нужно для systemd + NM dispatcher)
SELF=$(realpath "$0")
if [ "$SELF" != "$SCRIPT_DEST" ]; then
cp "$SELF" "$SCRIPT_DEST"
chmod +x "$SCRIPT_DEST"
echo "Скрипт скопирован в $SCRIPT_DEST"
fi
# Сервис восстановления ipset ДО старта UFW (однократно)
RESTORE_SVC="/etc/systemd/system/ru-ipset-restore.service"
if [ ! -f "$RESTORE_SVC" ]; then
cat > "$RESTORE_SVC" <<EOF
[Unit]
Description=Restore ru-direct ipset before UFW starts
DefaultDependencies=no
Before=ufw.service network.target
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/ipset restore -exist -file $IPSET_SAVE
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ru-ipset-restore.service
echo "Сервис ru-ipset-restore установлен (стартует до UFW)."
fi
# Основной сервис обновления маршрутов (однократно)
BYPASS_SVC="/etc/systemd/system/ru-bypass.service"
if [ ! -f "$BYPASS_SVC" ]; then
cat > "$BYPASS_SVC" <<'EOF'
[Unit]
Description=Route .ru IP blocks directly (bypass Amnezia VPN)
After=network-online.target ru-ipset-restore.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ru-bypass.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ru-bypass.service
echo "Сервис ru-bypass установлен."
fi
# Timer для ежесуточного обновления (однократно)
BYPASS_TIMER="/etc/systemd/system/ru-bypass.timer"
if [ ! -f "$BYPASS_TIMER" ]; then
cat > "$BYPASS_TIMER" <<'EOF'
[Unit]
Description=Daily update of .ru IP routes (ru-bypass)
[Timer]
OnCalendar=daily
Persistent=true
Unit=ru-bypass.service
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now ru-bypass.timer
echo "Timer ru-bypass.timer установлен (ежесуточное обновление RIPE)."
fi
# NetworkManager dispatcher — авто-перезапуск когда amn0 поднимается (однократно)
NM_DISPATCHER="/etc/NetworkManager/dispatcher.d/99-ru-bypass"
if [ ! -f "$NM_DISPATCHER" ]; then
cat > "$NM_DISPATCHER" <<'EOF'
#!/bin/bash
[ "$1" = "amn0" ] && [ "$2" = "up" ] && exec /usr/local/bin/ru-bypass.sh
EOF
chmod +x "$NM_DISPATCHER"
echo "NetworkManager dispatcher установлен."
fi
# --- Локальные хосты (фиксируем IP для доменов, которые должны резолвиться локально) ---
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
# и трафик пойдёт через VPN вместо прямого соединения.
HOSTS_MARKER="# ru-bypass: local hosts"
# Удаляем старые записи по маркеру (чтобы не копились дубли)
sed -i "/$HOSTS_MARKER/d" /etc/hosts
# Добавляем актуальные
cat >> /etc/hosts <<_HOSTS
$HOSTS_MARKER
# Eltex corporate services (*.eltex.loc, mattermost, elph)
172.16.0.3 eltex.loc
172.16.5.103 intdocs.eltex.loc
172.16.5.251 red.eltex.loc
172.16.1.17 gitlab.eltex.loc
172.16.1.106 pixso.eltex.loc
172.16.1.94 mcpe-builder.eltex.loc
172.16.5.63 proxy.eltex.loc
10.80.0.16 ssw.eltex.loc
172.16.5.78 nexus.eltex.loc
172.16.1.149 cpe-worker.eltex.loc
172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru
_HOSTS
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
# --- Обновляем RIPE-список (кэш 24ч) ---
if [ ! -f "$CACHE" ] || [ $(( $(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0) )) -gt 86400 ]; then
echo "Обновляем список .ru IP-блоков из RIPE..."
if curl -fsS -o "$CACHE.tmp" "$RIPE_URL"; then
mv "$CACHE.tmp" "$CACHE"
else
echo "Предупреждение: не удалось скачать RIPE-список"
if [ ! -f "$CACHE" ]; then exit 1; fi
echo "Используем старый кэш от $(date -r "$CACHE")"
fi
fi
# --- Создаём/обновляем ipset ---
echo "Обновляем ipset $SETNAME..."
# create -exist: не падает если уже есть (UFW на него ссылается, destroy ломает цепочку)
ipset create "$SETNAME" hash:net -exist
# flush: очищаем записи, но сохраняем сам set (iptables-правило остаётся валидным)
ipset flush "$SETNAME"
python3 -c "
import ipaddress
entries = 0
with open('$CACHE') as f_in:
for line in f_in:
parts = line.strip().split('|')
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
continue
ip_str, count = parts[3], int(parts[4])
first = ipaddress.IPv4Address(ip_str)
last = first + count - 1
for net in ipaddress.summarize_address_range(first, last):
print(f'add $SETNAME {net}')
entries += 1
import sys; print(f'# entries: {entries}', file=sys.stderr)
" 2>/tmp/ru-ipset-count.txt | ipset restore -exist -quiet
ENTRIES=$(ipset list "$SETNAME" 2>/dev/null | grep -c '/')
echo "ipset обновлён: $ENTRIES записей"
# --- Исключения для kill switch ---
# AMNEZIA_SERVER — IP/домены серверов Amnezia (нужны для поднятия VPN при активном kill switch)
# KILL_SWITCH_EXCEPTIONS — дополнительные IP/домены, доступные напрямую даже при kill switch
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
if [ -n "${ALL_EXC// }" ]; then
for item in $ALL_EXC; do
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ipset add ru-direct "$ip" -exist 2>/dev/null || true
echo "Исключение kill switch: $item$ip (ipset)"
done
done
fi
ipset save ru-direct > /etc/ipset.conf
echo "ipset сохранён в /etc/ipset.conf"
# --- Добавляем маршруты ---
echo "Добавляем маршруты..."
rm -f /tmp/ru-routes.batch
python3 -c "
import ipaddress
with open('$CACHE') as f, open('/tmp/ru-routes.batch', 'w') as out:
count = 0
for line in f:
parts = line.strip().split('|')
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
continue
ip_str, n = parts[3], int(parts[4])
first = ipaddress.IPv4Address(ip_str)
last = first + n - 1
for net in ipaddress.summarize_address_range(first, last):
out.write(f'route replace {net} via $GATEWAY dev $DEV\n')
count += 1
print(f'Маршрутов: {count}')
"
ip -force -batch /tmp/ru-routes.batch 2>/dev/null
# --- Маршруты для локальных сетей (*.loc, RFC1918) ---
LOCAL_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
echo "Добавляем маршруты для локальных сетей (*.loc / RFC1918)..."
for net in $LOCAL_NETS; do
ip route replace "$net" via "$GATEWAY" dev "$DEV" 2>/dev/null
done
# Маршруты для исключений kill switch
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
if [ -n "${ALL_EXC// }" ]; then
for item in $ALL_EXC; do
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null
echo "Маршрут для исключения: $item$ip"
done
done
fi
# --- DNS для *.loc через LOCAL_DNS (если задан) ---
if [ -n "$LOCAL_DNS" ]; then
if command -v resolvectl >/dev/null 2>&1; then
resolvectl dns "$DEV" "$LOCAL_DNS" 2>/dev/null && \
resolvectl domain "$DEV" "~loc" 2>/dev/null && \
echo "DNS для *.loc → $LOCAL_DNS (интерфейс $DEV)"
else
echo "Предупреждение: resolvectl не найден, LOCAL_DNS=$LOCAL_DNS не применён"
fi
fi
# --- Правила в UFW before.rules (обновляются при каждом запуске) ---
# Маркеры используются для идентификации правил; DEV всегда актуальный.
UFW_IPSET_MARKER="ru-bypass: ipset $SETNAME"
UFW_LOCAL_MARKER="ru-bypass: local-nets-bypass"
echo "Обновляем правила UFW before.rules..."
# Удаляем старые правила (если есть) — и в новом, и в старом формате маркеров
sed -i "/# $UFW_IPSET_MARKER/d; /# \.ru bypass (ipset $SETNAME)/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -m set --match-set $SETNAME dst/d" "$UFW_BEFORE"
sed -i "/# $UFW_LOCAL_MARKER/d; /# local nets bypass (local-nets-bypass)/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 10\.0\.0\.0\/8 -o/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 172\.16\.0\.0\/12 -o/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 192\.168\.0\.0\/16 -o/d" "$UFW_BEFORE"
# Добавляем правила заново с актуальным DEV
sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_IPSET_MARKER\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE"
sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_LOCAL_MARKER\n-A ufw-before-output -d 10.0.0.0\/8 -o $DEV -j ACCEPT\n-A ufw-before-output -d 172.16.0.0\/12 -o $DEV -j ACCEPT\n-A ufw-before-output -d 192.168.0.0\/16 -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE"
echo "UFW before.rules обновлён (ipset + локальные сети, DEV=$DEV)."
# --- Исправляем MANAGE_BUILTINS (должен быть yes, иначе before.rules не вызывается) ---
if grep -q '^MANAGE_BUILTINS=no' /etc/default/ufw 2>/dev/null; then
sed -i 's/^MANAGE_BUILTINS=no/MANAGE_BUILTINS=yes/' /etc/default/ufw
echo "UFW: MANAGE_BUILTINS исправлен (no → yes)."
fi
# --- Настройка UFW default deny + allow amn0 (однократно) ---
ufw default deny outgoing >/dev/null 2>&1 || true
ufw allow out on amn0 >/dev/null 2>&1 || true
if grep -qE "$UFW_IPSET_MARKER|$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then
if ufw status | grep -qE "активен|active"; then
ufw reload
fi
fi
# --- Прямые правила iptables (гарантия работы даже при MANAGE_BUILTINS=no) ---
echo "Добавляем прямые правила iptables..."
# Правило для ipset ru-direct (RU-IP + исключения kill switch)
iptables -C OUTPUT -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT 2>/dev/null || \
iptables -I OUTPUT 1 -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT
# Правила для локальных сетей (RFC1918)
for _net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do
iptables -C OUTPUT -d "$_net" -o "$DEV" -j ACCEPT 2>/dev/null || \
iptables -I OUTPUT 1 -d "$_net" -o "$DEV" -j ACCEPT
done
echo "iptables: прямые правила добавлены."
echo ""
echo "Готово."
RU_EXAMPLE=$(dig +short ya.ru A 2>/dev/null | head -1)
echo " ip route get 8.8.8.8 -> dev amn0 (через VPN)"
echo " ip route get ${RU_EXAMPLE:-<ya.ru ip>} -> dev $DEV (напрямую .ru)"
echo " ip route get 10.10.0.1 -> dev $DEV (напрямую *.loc / RFC1918)"
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
printf '%s [ru-bypass] GATEWAY=%s DEV=%s, блоков: %s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" "$GATEWAY" "$DEV" "$ENTRIES" >> "$_log_file" 2>/dev/null || true