- AMNEZIA_SERVER — только IP/домены серверов Amnezia (для поднятия VPN) - KILL_SWITCH_EXCEPTIONS — дополнительные исключения (git, etc.) - Обе переменные поддерживают IP и домены (DNS-резолвинг) - setup.sh: раздельные промпты в меню Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
292 lines
10 KiB
Bash
292 lines
10 KiB
Bash
#!/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:-}"
|
|
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS:-}"
|
|
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 записей"
|
|
|
|
|
|
|
|
|
|
# --- Исключения для 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=$(dig +short "$item" A 2>/dev/null)
|
|
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=$(dig +short "$item" A 2>/dev/null)
|
|
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 (однократно, после создания 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
|