Files
ai-setup/scripts/ru-bypass.sh
Виталий Никитенко 398e57c648 fix: kill switch не блокировал не-.ru трафик + Amnezia не могла подключиться
- ru-bypass.sh: добавлен ufw default deny outgoing (раньше нигде не выполнялся)
- ru-bypass.sh: добавлен ufw allow out on amn0 (разрешён трафик через VPN)
- ru-bypass.sh: поддержка AMNEZIA_SERVER — IP добавляется в ipset и маршруты
- ks-on.sh: default deny + allow amn0 при восстановлении kill switch
- setup.sh: меню запрашивает/сохраняет/передаёт AMNEZIA_SERVER
- test_network.sh: DEV читается из конфига вместо жёсткого wl[pi]

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

262 lines
9.9 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
GATEWAY="${GATEWAY:-192.168.1.1}"
DEV="${DEV:-wlp1s0}"
LOCAL_DNS="${LOCAL_DNS:-}"
AMNEZIA_SERVER="${AMNEZIA_SERVER:-}"
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
# --- Обновляем 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 записей"
# --- Сервер Amnezia в исключения (чтобы мог подключиться при kill switch) ---
if [ -n "$AMNEZIA_SERVER" ]; then
ipset add "$SETNAME" "$AMNEZIA_SERVER" -exist 2>/dev/null || true
echo "Сервер Amnezia $AMNEZIA_SERVER добавлен в ipset $SETNAME"
fi
# Сохраняем ipset на диск (с учётом сервера Amnezia)
ipset save "$SETNAME" > "$IPSET_SAVE"
echo "ipset сохранён в $IPSET_SAVE"
# --- Добавляем маршруты ---
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
# Маршрут для сервера Amnezia (чтобы мог подключиться при kill switch)
if [ -n "$AMNEZIA_SERVER" ]; then
ip route replace "$AMNEZIA_SERVER/32" via "$GATEWAY" dev "$DEV" 2>/dev/null
echo "Маршрут для сервера Amnezia $AMNEZIA_SERVER добавлен"
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 (однократно, после создания ipset) ---
UFW_MARKER="match-set $SETNAME"
if ! grep -q "$UFW_MARKER" "$UFW_BEFORE" 2>/dev/null; then
echo "Добавляем правило в UFW before.rules..."
sed -i "0,/^COMMIT/{s/^COMMIT/# .ru bypass (ipset $SETNAME)\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE"
echo "UFW обновлён (.ru ipset)."
fi
UFW_LOCAL_MARKER="local-nets-bypass"
if ! grep -q "$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then
echo "Добавляем правила UFW для локальных сетей..."
sed -i "0,/^COMMIT/{s/^COMMIT/# local nets bypass ($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 обновлён (локальные сети)."
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_MARKER|$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then
if ufw status | grep -qE "активен|active"; then
ufw reload
fi
fi
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